diff options
author | 2021-10-08 23:25:56 -0700 | |
---|---|---|
committer | 2021-10-08 23:25:56 -0700 | |
commit | 18ba2096e612101b97204a7449c3c652e7e078aa (patch) | |
tree | 0ce32810ad4ef53f860f87eb54151d8f8ecb1aa8 | |
parent | 7e2c297013e89e26da2db27d7edab886761c203e (diff) | |
download | bun-jarred/improve-testing.tar.gz bun-jarred/improve-testing.tar.zst bun-jarred/improve-testing.zip |
Implement a filesystem for testsjarred/improve-testing
-rw-r--r-- | src/fs.zig | 853 | ||||
-rw-r--r-- | src/http.zig | 5 | ||||
-rw-r--r-- | src/linker.zig | 2 | ||||
-rw-r--r-- | src/options.zig | 2 | ||||
-rw-r--r-- | src/resolver/resolver.zig | 5 |
5 files changed, 583 insertions, 284 deletions
diff --git a/src/fs.zig b/src/fs.zig index dc4d1a382..bb004d61a 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -340,8 +340,66 @@ pub const FileSystem = struct { return Implementation.cwd(); } + pub fn bustEntriesCache(rfs: *FileSystem, file_path: string) void { + rfs.entries.remove(file_path); + } + pub fn readDirectory(fs: *FileSystem, _dir: string, _handle: ?FileDescriptorType) !*EntriesOption { - return @call(.{ .modifier = .always_inline }, Implementation.readDirectory, .{ &fs.fs, _dir, _handle }); + var dir = _dir; + var cache_result: ?allocators.Result = null; + if (comptime FeatureFlags.enable_entry_cache) { + fs.entries_mutex.lock(); + } + defer { + if (comptime FeatureFlags.enable_entry_cache) { + fs.entries_mutex.unlock(); + } + } + + if (comptime FeatureFlags.enable_entry_cache) { + cache_result = try fs.entries.getOrPut(dir); + + if (cache_result.?.hasCheckedIfExists()) { + if (fs.entries.atIndex(cache_result.?.index)) |cached_result| { + return cached_result; + } + } + } + + var handle = Dir{ .fd = _handle orelse try openFileAbsolute(dir, .{}) }; + + defer { + if (_handle == null and fs.needToCloseFiles()) { + handle.close(); + } + } + + // if we get this far, it's a real directory, so we can just store the dir name. + if (_handle == null) { + dir = try FileSystem.DirnameStore.instance.append(string, _dir); + } + + // Cache miss: read the directory entries + var entries = fs.fs.readdir( + dir, + handle, + ) catch |err| { + return fs.readDirectoryError(dir, err) catch unreachable; + }; + + if (comptime FeatureFlags.enable_entry_cache) { + const result = EntriesOption{ + .entries = entries, + }; + + var out = try fs.entries.put(&cache_result.?, result); + + return out; + } + + temp_entries_option = EntriesOption{ .entries = entries }; + + return &temp_entries_option; } pub fn openDirectory(_dir: string, flags: std.fs.Dir.OpenDirOptions) !Dir { @@ -366,24 +424,67 @@ pub const FileSystem = struct { fs: *FileSystem, path: string, _size: ?usize, - file: FileDescriptorType, + fd: FileDescriptorType, comptime use_shared_buffer: bool, shared_buffer: *MutableString, ) !string { - return @call( - .{ - .modifier = .always_inline, - }, - Implementation.readFileWithHandle, - .{ - &fs.fs, - path, - _size, - file, - comptime use_shared_buffer, - shared_buffer, - }, - ); + const file = File{ .handle = fd }; + FileSystem.setMaxFd(file.handle); + + if (comptime FeatureFlags.disable_filesystem_cache) { + _ = std.os.fcntl(file.handle, std.os.F_NOCACHE, 1) catch 0; + } + + // Skip the extra file.stat() call when possible + var size = _size orelse try file.getEndPos(); + + // Skip the pread call for empty files + // Otherwise will get out of bounds errors + // plus it's an unnecessary syscall + if (size == 0) { + if (comptime use_shared_buffer) { + shared_buffer.reset(); + return shared_buffer.list.items; + } else { + return ""; + } + } + + // When we're serving a JavaScript-like file over HTTP, we do not want to cache the contents in memory + // This imposes a performance hit because not reading from disk is faster than reading from disk + // Part of that hit is allocating a temporary buffer to store the file contents in + // As a mitigation, we can just keep one buffer forever and re-use it for the parsed files + if (comptime use_shared_buffer) { + shared_buffer.reset(); + try shared_buffer.growBy(size); + shared_buffer.list.expandToCapacity(); + // We use pread to ensure if the file handle was open, it doesn't seek from the last position + var read_count = try file.preadAll(shared_buffer.list.items, 0); + shared_buffer.list.items = shared_buffer.list.items[0..read_count]; + return shared_buffer.list.items; + } else { + // We use pread to ensure if the file handle was open, it doesn't seek from the last position + var buf = try fs.allocator.alloc(u8, size); + var read_count = try file.preadAll(buf, 0); + + return buf[0..read_count]; + } + } + + fn readDirectoryError(fs: *FileSystem, dir: string, err: anyerror) !*EntriesOption { + if (comptime FeatureFlags.enable_entry_cache) { + var get_or_put_result = try fs.entries.getOrPut(dir); + var opt = try fs.entries.put(&get_or_put_result, EntriesOption{ + .err = DirEntry.Err{ .original_err = err, .canonical_error = err }, + }); + + return opt; + } + + temp_entries_option = EntriesOption{ + .err = DirEntry.Err{ .original_err = err, .canonical_error = err }, + }; + return &temp_entries_option; } pub fn openFileInDir( @@ -458,7 +559,7 @@ pub const FileSystem = struct { path: string, flags: std.fs.File.CreateFlags, ) !File { - return File{ .handle = try createFileAbsolute(path, flags) }; + return File{ .handle = try Implementation.createFileAbsolute(path, flags) }; } pub inline fn getFileSize( @@ -570,15 +671,15 @@ pub const FileSystem = struct { // pub fn readDir(fs: *FileSystemEntry, path: string) ?[]string { // } - pub fn normalize(f: *@This(), str: string) string { + pub fn normalize(f: *const @This(), str: string) string { return @call(.{ .modifier = .always_inline }, path_handler.normalizeString, .{ str, true, .auto }); } - pub fn normalizeBuf(f: *@This(), buf: []u8, str: string) string { + pub fn normalizeBuf(f: *const @This(), buf: []u8, str: string) string { return @call(.{ .modifier = .always_inline }, path_handler.normalizeStringBuf, .{ str, buf, false, .auto, false }); } - pub fn join(f: *@This(), parts: anytype) string { + pub fn join(f: *const @This(), parts: anytype) string { return @call(.{ .modifier = .always_inline }, path_handler.joinStringBuf, .{ &join_buf, parts, @@ -586,7 +687,7 @@ pub const FileSystem = struct { }); } - pub fn joinBuf(f: *@This(), parts: anytype, buf: []u8) string { + pub fn joinBuf(f: *const @This(), parts: anytype, buf: []u8) string { return @call(.{ .modifier = .always_inline }, path_handler.joinStringBuf, .{ buf, parts, @@ -594,14 +695,14 @@ pub const FileSystem = struct { }); } - pub fn relative(f: *@This(), from: string, to: string) string { + pub fn relative(f: *const @This(), from: string, to: string) string { return @call(.{ .modifier = .always_inline }, path_handler.relative, .{ from, to, }); } - pub fn relativeAlloc(f: *@This(), allocator: *std.mem.Allocator, from: string, to: string) string { + pub fn relativeAlloc(f: *const @This(), allocator: *std.mem.Allocator, from: string, to: string) string { return @call(.{ .modifier = .always_inline }, path_handler.relativeAlloc, .{ alloc, from, @@ -609,21 +710,21 @@ pub const FileSystem = struct { }); } - pub fn relativeTo(f: *@This(), to: string) string { + pub fn relativeTo(f: *const @This(), to: string) string { return @call(.{ .modifier = .always_inline }, path_handler.relative, .{ f.top_level_dir, to, }); } - pub fn relativeFrom(f: *@This(), from: string) string { + pub fn relativeFrom(f: *const @This(), from: string) string { return @call(.{ .modifier = .always_inline }, path_handler.relative, .{ from, f.top_level_dir, }); } - pub fn relativeToAlloc(f: *@This(), allocator: *std.mem.Allocator, to: string) string { + pub fn relativeToAlloc(f: *const @This(), allocator: *std.mem.Allocator, to: string) string { return @call(.{ .modifier = .always_inline }, path_handler.relativeAlloc, .{ allocator, f.top_level_dir, @@ -631,7 +732,7 @@ pub const FileSystem = struct { }); } - pub fn absAlloc(f: *@This(), allocator: *std.mem.Allocator, parts: anytype) !string { + pub fn absAlloc(f: *const @This(), allocator: *std.mem.Allocator, parts: anytype) !string { const joined = path_handler.joinAbsString( f.top_level_dir, parts, @@ -640,7 +741,7 @@ pub const FileSystem = struct { return try allocator.dupe(u8, joined); } - pub fn abs(f: *@This(), parts: anytype) string { + pub fn abs(f: *const @This(), parts: anytype) string { return path_handler.joinAbsString( f.top_level_dir, parts, @@ -760,6 +861,17 @@ pub const File = struct { return try FSType.pread(self.handle, buffer, offset); } + pub inline fn realpath(path: string, buf: *[std.fs.MAX_PATH_BYTES]u8) anyerror!string { + return try FSType.realpath(path, buf); + } + + pub inline fn getPath( + self: File, + buffer: *[std.fs.MAX_PATH_BYTES]u8, + ) !string { + return try FSType.getPath(self.handle, buffer); + } + pub fn preadAll(self: File, buffer: []u8, offset: u64) PReadError!usize { var index: usize = 0; while (index != buffer.len) { @@ -1080,6 +1192,71 @@ pub const Path = struct { } }; +pub const ModKeyError = error{ + Unusable, +}; +pub const ModKey = struct { + inode: std.fs.File.INode = 0, + size: u64 = 0, + mtime: i128 = 0, + mode: std.fs.File.Mode = 0, + + threadlocal var hash_bytes: [32]u8 = undefined; + threadlocal var hash_name_buf: [1024]u8 = undefined; + + pub fn hashName( + this: *const ModKey, + basename: string, + ) !string { + + // We shouldn't just read the contents of the ModKey into memory + // The hash should be deterministic across computers and operating systems. + // inode is non-deterministic across volumes within the same compuiter + // so if we're not going to do a full content hash, we should use mtime and size. + // even mtime is debatable. + var hash_bytes_remain: []u8 = hash_bytes[0..]; + std.mem.writeIntNative(@TypeOf(this.size), hash_bytes_remain[0..@sizeOf(@TypeOf(this.size))], this.size); + hash_bytes_remain = hash_bytes_remain[@sizeOf(@TypeOf(this.size))..]; + std.mem.writeIntNative(@TypeOf(this.mtime), hash_bytes_remain[0..@sizeOf(@TypeOf(this.mtime))], this.mtime); + + return try std.fmt.bufPrint( + &hash_name_buf, + "{s}-{x}", + .{ + basename, + @truncate(u32, std.hash.Wyhash.hash(1, &hash_bytes)), + }, + ); + } + + pub fn generate(path: string, file: File) anyerror!ModKey { + const stat_ = try file.stat(); + + const seconds = @divTrunc(stat_.mtime, @as(@TypeOf(stat_.mtime), std.time.ns_per_s)); + + // We can't detect changes if the file system zeros out the modification time + if (seconds == 0 and std.time.ns_per_s == 0) { + return error.Unusable; + } + + // Don't generate a modification key if the file is too new + const now = std.time.nanoTimestamp(); + const now_seconds = @divTrunc(now, std.time.ns_per_s); + if (seconds > seconds or (seconds == now_seconds and stat_.mtime > now)) { + return error.Unusable; + } + + return ModKey{ + .inode = stat_.inode, + .size = stat_.size, + .mtime = stat_.mtime, + .mode = stat_.mode, + // .uid = stat. + }; + } + pub const SafetyGap = 3; +}; + pub const RealFS = struct { entries: *EntriesOption.Map, allocator: *std.mem.Allocator, @@ -1182,10 +1359,6 @@ pub const RealFS = struct { return !(rfs.file_limit > 254 and rfs.file_limit > (FileSystem.max_fd + 1) * 2); } - pub fn bustEntriesCache(rfs: *RealFS, file_path: string) void { - rfs.entries.remove(file_path); - } - // Always try to max out how many files we can keep open pub fn adjustUlimit() !usize { const LIMITS = [_]std.os.rlimit_resource{ std.os.rlimit_resource.STACK, std.os.rlimit_resource.NOFILE }; @@ -1220,71 +1393,6 @@ pub const RealFS = struct { }; } - pub const ModKeyError = error{ - Unusable, - }; - pub const ModKey = struct { - inode: std.fs.File.INode = 0, - size: u64 = 0, - mtime: i128 = 0, - mode: std.fs.File.Mode = 0, - - threadlocal var hash_bytes: [32]u8 = undefined; - threadlocal var hash_name_buf: [1024]u8 = undefined; - - pub fn hashName( - this: *const ModKey, - basename: string, - ) !string { - - // We shouldn't just read the contents of the ModKey into memory - // The hash should be deterministic across computers and operating systems. - // inode is non-deterministic across volumes within the same compuiter - // so if we're not going to do a full content hash, we should use mtime and size. - // even mtime is debatable. - var hash_bytes_remain: []u8 = hash_bytes[0..]; - std.mem.writeIntNative(@TypeOf(this.size), hash_bytes_remain[0..@sizeOf(@TypeOf(this.size))], this.size); - hash_bytes_remain = hash_bytes_remain[@sizeOf(@TypeOf(this.size))..]; - std.mem.writeIntNative(@TypeOf(this.mtime), hash_bytes_remain[0..@sizeOf(@TypeOf(this.mtime))], this.mtime); - - return try std.fmt.bufPrint( - &hash_name_buf, - "{s}-{x}", - .{ - basename, - @truncate(u32, std.hash.Wyhash.hash(1, &hash_bytes)), - }, - ); - } - - pub fn generate(fs: *RealFS, path: string, file: File) anyerror!ModKey { - const stat_ = try file.stat(); - - const seconds = @divTrunc(stat_.mtime, @as(@TypeOf(stat_.mtime), std.time.ns_per_s)); - - // We can't detect changes if the file system zeros out the modification time - if (seconds == 0 and std.time.ns_per_s == 0) { - return error.Unusable; - } - - // Don't generate a modification key if the file is too new - const now = std.time.nanoTimestamp(); - const now_seconds = @divTrunc(now, std.time.ns_per_s); - if (seconds > seconds or (seconds == now_seconds and stat_.mtime > now)) { - return error.Unusable; - } - - return ModKey{ - .inode = stat_.inode, - .size = stat_.size, - .mtime = stat_.mtime, - .mode = stat_.mode, - // .uid = stat. - }; - } - pub const SafetyGap = 3; - }; - pub fn modKeyWithFile(fs: *RealFS, path: string, file: anytype) anyerror!ModKey { return try ModKey.generate(fs, path, file); } @@ -1344,6 +1452,11 @@ pub const RealFS = struct { return try std.fs.File.getPos(std.fs.File{ .handle = fd }); } + pub fn openDirectory(path: string, flags: std.fs.Dir.OpenDirOptions) anyerror!Dir { + const dir = try std.fs.cwd().openDir(path, flags); + return Dir{ .fd = dir.fd }; + } + pub fn modKey(fs: *const RealFS, path: string) anyerror!ModKey { // fs.limiter.before(); // defer fs.limiter.after(); @@ -1356,48 +1469,29 @@ pub const RealFS = struct { return try fs.modKeyWithFile(path, file); } - // Limit the number of files open simultaneously to avoid ulimit issues - pub const Limiter = struct { - semaphore: Semaphore, - pub fn init(allocator: *std.mem.Allocator, limit: usize) Limiter { - return Limiter{ - .semaphore = Semaphore.init(limit), - // .counter = std.atomic.Int(u8).init(0), - // .lock = std.Thread.Mutex.init(), - }; - } - - // This will block if the number of open files is already at the limit - pub fn before(limiter: *Limiter) void { - limiter.semaphore.wait(); - // var added = limiter.counter.fetchAdd(1); - } - - pub fn after(limiter: *Limiter) void { - limiter.semaphore.post(); - // limiter.counter.decr(); - // if (limiter.held) |hold| { - // hold.release(); - // limiter.held = null; - // } - } - }; + pub inline fn getFileSize( + handle: FileDescriptorType, + ) !u64 { + const stat_ = try std.os.fstat(handle); + return @intCast(u64, stat_.size); + } - fn openDir(unsafe_dir_string: string) std.fs.File.OpenError!FileDescriptorType { - const fd = try std.fs.openDirAbsolute(unsafe_dir_string, std.fs.Dir.OpenDirOptions{ .iterate = true, .access_sub_paths = true, .no_follow = false }); + pub inline fn close(fd: FileDescriptorType) void { + std.os.close(fd); + } - return fd.fd; + pub inline fn realpath(path: string, buf: *[std.fs.MAX_PATH_BYTES]u8) !string { + return try std.os.realpath(path, buf); } - pub fn openDirectory(path: string, flags: std.fs.Dir.OpenDirOptions) anyerror!Dir { - const dir = try std.fs.cwd().openDir(path, flags); - return Dir{ .fd = dir.fd }; + pub inline fn getPath(handle: FileDescriptorType, buf: *[std.fs.MAX_PATH_BYTES]u8) !string { + return try std.os.getFdPath(handle, buf); } - fn readdir( + pub fn readdir( fs: *RealFS, _dir: string, - handle: std.fs.Dir, + handle: Dir, ) !DirEntry { // fs.limiter.before(); // defer fs.limiter.after(); @@ -1418,158 +1512,14 @@ pub const RealFS = struct { return dir; } - fn readDirectoryError(fs: *RealFS, dir: string, err: anyerror) !*EntriesOption { - if (comptime FeatureFlags.enable_entry_cache) { - var get_or_put_result = try fs.entries.getOrPut(dir); - var opt = try fs.entries.put(&get_or_put_result, EntriesOption{ - .err = DirEntry.Err{ .original_err = err, .canonical_error = err }, - }); - - return opt; - } - - temp_entries_option = EntriesOption{ - .err = DirEntry.Err{ .original_err = err, .canonical_error = err }, - }; - return &temp_entries_option; - } - threadlocal var temp_entries_option: EntriesOption = undefined; - pub fn readDirectory(fs: *RealFS, _dir: string, _handle: ?FileDescriptorType) !*EntriesOption { - var dir = _dir; - var cache_result: ?allocators.Result = null; - if (comptime FeatureFlags.enable_entry_cache) { - fs.parent_fs.entries_mutex.lock(); - } - defer { - if (comptime FeatureFlags.enable_entry_cache) { - fs.parent_fs.entries_mutex.unlock(); - } - } - - if (comptime FeatureFlags.enable_entry_cache) { - cache_result = try fs.entries.getOrPut(dir); - - if (cache_result.?.hasCheckedIfExists()) { - if (fs.entries.atIndex(cache_result.?.index)) |cached_result| { - return cached_result; - } - } - } - - var handle = std.fs.Dir{ .fd = _handle orelse try openDir(dir) }; - - defer { - if (_handle == null and fs.needToCloseFiles()) { - handle.close(); - } - } - - // if we get this far, it's a real directory, so we can just store the dir name. - if (_handle == null) { - dir = try FileSystem.DirnameStore.instance.append(string, _dir); - } - - // Cache miss: read the directory entries - var entries = fs.readdir( - dir, - handle, - ) catch |err| { - return fs.readDirectoryError(dir, err) catch unreachable; - }; - - if (comptime FeatureFlags.enable_entry_cache) { - const result = EntriesOption{ - .entries = entries, - }; - - var out = try fs.entries.put(&cache_result.?, result); - - return out; - } - - temp_entries_option = EntriesOption{ .entries = entries }; - - return &temp_entries_option; - } - fn readFileError(fs: *RealFS, path: string, err: anyerror) void {} pub inline fn stat(fd: FileDescriptorType) anyerror!Stat { return try std.fs.File.stat(.{ .handle = fd }); } - pub inline fn getFileSize( - handle: FileDescriptorType, - ) !u64 { - const stat_ = try std.os.fstat(handle); - return @intCast(u64, stat_.size); - } - - pub fn readFileWithHandle( - fs: *RealFS, - path: string, - _size: ?usize, - handle: FileDescriptorType, - comptime use_shared_buffer: bool, - shared_buffer: *MutableString, - ) !string { - const file = std.fs.File{ .handle = handle }; - FileSystem.setMaxFd(file.handle); - - if (comptime FeatureFlags.disable_filesystem_cache) { - _ = std.os.fcntl(file.handle, std.os.F_NOCACHE, 1) catch 0; - } - - // Skip the extra file.stat() call when possible - var size = _size orelse (file.getEndPos() catch |err| { - fs.readFileError(path, err); - return err; - }); - - // Skip the pread call for empty files - // Otherwise will get out of bounds errors - // plus it's an unnecessary syscall - if (size == 0) { - if (comptime use_shared_buffer) { - shared_buffer.reset(); - return shared_buffer.list.items; - } else { - return ""; - } - } - - // When we're serving a JavaScript-like file over HTTP, we do not want to cache the contents in memory - // This imposes a performance hit because not reading from disk is faster than reading from disk - // Part of that hit is allocating a temporary buffer to store the file contents in - // As a mitigation, we can just keep one buffer forever and re-use it for the parsed files - if (use_shared_buffer) { - shared_buffer.reset(); - try shared_buffer.growBy(size); - shared_buffer.list.expandToCapacity(); - // We use pread to ensure if the file handle was open, it doesn't seek from the last position - var read_count = file.preadAll(shared_buffer.list.items, 0) catch |err| { - fs.readFileError(path, err); - return err; - }; - shared_buffer.list.items = shared_buffer.list.items[0..read_count]; - return shared_buffer.list.items; - } else { - // We use pread to ensure if the file handle was open, it doesn't seek from the last position - var buf = try fs.allocator.alloc(u8, size); - var read_count = file.preadAll(buf, 0) catch |err| { - fs.readFileError(path, err); - return err; - }; - return buf[0..read_count]; - } - } - - pub inline fn close(fd: FileDescriptorType) void { - std.os.close(fd); - } - pub fn kind(fs: *RealFS, _dir: string, base: string, existing_fd: StoredFileDescriptorType) !Entry.Cache { var dir = _dir; var combo = [2]string{ dir, base }; @@ -1622,6 +1572,12 @@ pub const RealFS = struct { return cache; } + fn openDir(unsafe_dir_string: string) std.fs.File.OpenError!FileDescriptorType { + const fd = try std.fs.openDirAbsolute(unsafe_dir_string, std.fs.Dir.OpenDirOptions{ .iterate = true, .access_sub_paths = true, .no_follow = false }); + + return fd.fd; + } + // // Stores the file entries for directories we've listed before // entries_mutex: std.Mutex // entries map[string]entriesOrErr @@ -1630,7 +1586,311 @@ pub const RealFS = struct { // doNotCacheEntries bool }; -pub const TestFS = struct {}; +pub const TestFS = struct { + pub const TestFile = struct { + bytes: std.ArrayList(u8), + kind: std.fs.File.Kind = .File, + + pub fn toDirEntry(this: TestFile, name: string) std.fs.Dir.Entry { + return std.fs.Dir.Entry{ .name = name, .kind = this.kind }; + } + }; + const Handle = struct { + pos: usize = 0, + path: string = "", + }; + entries: *EntriesOption.Map, + allocator: *std.mem.Allocator, + parent_fs: *FileSystem = undefined, + + pub var map: std.StringHashMap(TestFile) = undefined; + pub var handles: std.AutoArrayHashMap(FileDescriptorType, Handle) = undefined; + pub var max_fd: FileDescriptorType = 1; + pub var loaded = false; + pub fn init( + allocator: *std.mem.Allocator, + cwd_: string, + entries: *EntriesOption.Map, + ) TestFS { + if (!loaded) { + map = std.StringHashMap(TestFile).init(allocator); + loaded = true; + handles = std.AutoArrayHashMap(FileDescriptorType, Handle).init(allocator); + } + + return TestFS{ + .entries = entries, + .allocator = allocator, + }; + } + + pub var cwd_path: string = "/bun-testfs"; + + pub fn close(fd: FileDescriptorType) void { + const path = handles.get(fd) orelse @panic("tried to close file without descriptor"); + std.debug.assert(handles.swapRemove(fd)); + } + + pub fn create(allocator: *std.mem.Allocator, data: anytype) void { + _ = FileSystem.init1(allocator, cwd_path) catch unreachable; + make(allocator, data); + } + + pub fn make(allocator: *std.mem.Allocator, data: anytype) void { + const Data = @TypeOf(data); + _ = map.getOrPutValue(cwd_path, TestFile{ .kind = .Directory, .bytes = std.ArrayList(u8).init(map.allocator) }) catch unreachable; + _ = handles.getOrPutValue(0, .{ .path = cwd_path }) catch unreachable; + + const fields: []const std.builtin.TypeInfo.StructField = comptime std.meta.fields(Data); + inline for (fields) |field| { + var file = TestFile{ .bytes = std.ArrayList(u8).init(map.allocator) }; + const value = @field(data, field.name); + var had_bytes = false; + const ValueType = @TypeOf(value); + const file_data: []const std.builtin.TypeInfo.StructField = std.meta.fields(ValueType); + inline for (file_data) |file_field| { + const file_value = @field(value, file_field.name); + if (comptime strings.eql(file_field.name, "data")) { + file.bytes = std.ArrayList(u8).init(allocator); + file.bytes.insertSlice(0, std.mem.span(file_value)) catch unreachable; + had_bytes = true; + } else if (comptime strings.eql(file_field.name, "kind")) { + file.kind = @field(file_field, file_field.name); + had_bytes = file.kind == .File or had_bytes; + } + } + + if (!had_bytes) { + file.kind = .File; + } + + var parent = std.fs.path.dirname(field.name); + while (parent) |_parent| { + _ = map.getOrPutValue(_parent, .{ + .bytes = std.ArrayList(u8).init(map.allocator), + .kind = .Directory, + }) catch unreachable; + parent = std.fs.path.dirname(_parent); + } + map.put(field.name, file) catch unreachable; + } + } + + pub fn kind(fs: *TestFS, _dir: string, base: string, existing_fd: StoredFileDescriptorType) !Entry.Cache { + var parts = [2]string{ _dir, base }; + var pathbuf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + var joined = path_handler.joinAbsStringBuf(cwd, &pathbuf, &parts, .posix); + var fd = try openFileAbsolute(joined, .{}); + var handle = handles.get(fd).?; + var file = map.get(handle).?; + return Entry.Cache{ + .kind = if (file.kind == .File) .file else .dir, + .symlink = false, + .fd = fd, + }; + } + + pub fn modKeyWithFile(fs: *RealFS, path: string, file: anytype) anyerror!ModKey { + return try ModKey.generate(fs, path, file); + } + + pub fn cwd() Dir { + return Dir{ .fd = handles.get(0).? }; + } + + pub fn read(fd: FileDescriptorType, buf: []u8) File.ReadError!usize { + var handle = handles.getPtr(fd) orelse return error.NotOpenForReading; + const file = map.getPtr(handle.path) orelse return error.Unexpected; + if (handle.pos >= file.bytes.items.len) return 0; + + const bytes = file.bytes.items[handle.pos..]; + const len = std.math.min(buf.len, bytes.len); + std.mem.copy(u8, buf, bytes[0..len]); + handle.pos += len; + handle.pos = std.math.min(file.bytes.items.len, handle.pos); + return len; + } + + pub inline fn write(fd: FileDescriptorType, buf: []const u8) File.WriteError!usize { + var handle = handles.getPtr(fd) orelse return error.NotOpenForWriting; + const file = map.getPtr(handle.path) orelse return error.Unexpected; + + if (handle.pos >= file.bytes.capacity) { + file.bytes.ensureUnusedCapacity(buf.len) catch return error.DiskQuota; + } + + file.bytes.insertSlice(handle.pos, buf) catch return error.DiskQuota; + handle.pos += buf.len; + return buf.len; + } + + pub inline fn pwrite(fd: FileDescriptorType, buf: []const u8, offset: usize) File.PWriteError!usize { + const handle = handles.get(fd) orelse return error.NotOpenForWriting; + const file = map.getPtr(handle.path) orelse return error.Unexpected; + + file.bytes.insertSlice(offset, buf) catch return error.DiskQuota; + return buf.len; + } + + pub inline fn pread(fd: FileDescriptorType, buf: []u8, offset: usize) File.PReadError!usize { + var handle = handles.getPtr(fd) orelse return error.NotOpenForWriting; + const file = map.getPtr(handle.path) orelse return error.Unexpected; + if (offset >= file.bytes.items.len) return 0; + var remainder = file.bytes.items[offset..]; + remainder = remainder[0..std.math.min(buf.len, remainder.len)]; + std.mem.copy(u8, buf, remainder); + return remainder.len; + } + + pub inline fn openFileInDir(dir: FileDescriptorType, subpath: string, flags: FileOpenFlags) !FileDescriptorType { + const fd = max_fd; + max_fd += 1; + const handle = handles.get(dir) orelse return error.NotOpenForWriting; + var parts = [_]string{ dir_path, subpath }; + var outbuf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + var path = path_handler.joinAbsStringBuf(cwd_path, &outbuf, &parts, .posix); + if (!map.contains(handle.path)) return error.FileNotFound; + try handles.put(fd, .{ .path = try map.allocator.dupe(u8, path) }); + return fd; + } + + pub inline fn createFileInDir(dir: FileDescriptorType, subpath: string, flags: std.fs.File.CreateFlags) !FileDescriptorType { + const fd = max_fd; + max_fd += 1; + const dir_path = handles.get(dir) orelse return error.NotOpenForWriting; + var parts = [_]string{ dir_path.path, subpath }; + var outbuf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + var path = path_handler.joinAbsStringBuf(cwd_path, &outbuf, &parts, .posix); + return createFileAbsolute(path, flags); + } + + pub inline fn openFileAbsolute(path: string, flags: FileOpenFlags) !FileDescriptorType { + if (!map.contains(path)) return error.FileNotFound; + const fd = max_fd; + max_fd += 1; + + try handles.put(fd, .{ .path = try map.allocator.dupe(u8, path) }); + return fd; + } + + pub inline fn openFileAbsoluteZ(path_z: stringZ, flags: FileOpenFlags) !FileDescriptorType { + const path = std.mem.span(path_z); + return try openFileAbsolute(path, flags); + } + + pub inline fn createFileAbsolute(path: string, flags: std.fs.File.CreateFlags) !FileDescriptorType { + const fd = max_fd; + max_fd += 1; + + var entry = try map.getOrPutValue( + try map.allocator.dupe( + u8, + path, + ), + TestFile{ .kind = .File, .bytes = std.ArrayList(u8).init(map.allocator) }, + ); + + if (flags.truncate) { + entry.value_ptr.bytes.clearRetainingCapacity(); + } + + try handles.put(fd, .{ .path = try map.allocator.dupe(u8, path) }); + return fd; + } + + pub inline fn seekTo(fd: FileDescriptorType, offset: usize) !void { + var handle = handles.getPtr(fd) orelse @panic("bad file"); + handle.pos = offset; + } + + pub inline fn getPos( + fd: FileDescriptorType, + ) !usize { + const handle = handles.get(fd) orelse @panic("bad file"); + return handle.pos; + } + + pub fn needToCloseFiles(fs: *TestFS) bool { + return false; + } + + pub fn openDirectory(path: string, flags: std.fs.Dir.OpenDirOptions) anyerror!Dir { + var entry = try map.getOrPutValue( + try map.allocator.dupe( + u8, + path, + ), + TestFile{ .kind = .Directory, .bytes = std.ArrayList(u8).init(map.allocator) }, + ); + const fd = max_fd; + max_fd += 1; + try handles.put(fd, .{ .path = try map.allocator.dupe(u8, path) }); + return fd; + } + + pub fn modKey(fs: *const TestFS, path: string) anyerror!ModKey { + return ModKey{}; + } + + pub inline fn realpath(path: string, buf: *[std.fs.MAX_PATH_BYTES]u8) anyerror!string { + return path; + } + + pub inline fn getPath( + fd: FileDescriptorType, + buffer: *[std.fs.MAX_PATH_BYTES]u8, + ) !string { + return handles.get(fd).?.path; + } + + pub inline fn getFileSize( + fd: FileDescriptorType, + ) !u64 { + const handle = handles.get(fd) orelse @panic("bad file"); + const file = map.get(handle.path) orelse @panic("bad file"); + return file.bytes.items.len; + } + + pub fn readdir( + fs: *TestFS, + _dir: string, + dir_: Dir, + ) !DirEntry { + var handle = handles.getPtr(dir_.fd) orelse @panic("bad dir"); + const dir = map.get(handle.path) orelse @panic("bad dir"); + + var dirent = DirEntry.init(_dir, fs.allocator); + + var iter = map.keyIterator(); + while (iter.next()) |key_| { + const key = key_.*; + if (strings.eql(key, _dir)) continue; + + if (strings.indexOf(key, handle.path)) |i| { + if (i == 0) { + const basename = std.fs.path.basename(key); + if (std.fs.path.dirname(basename) != null) continue; + + try dirent.addEntry(map.get(key).?.toDirEntry(basename)); + } + } + } + + return dirent; + } + + pub inline fn stat(fd: FileDescriptorType) anyerror!Stat { + const handle = handles.get(fd) orelse @panic("bad file"); + const file = map.get(handle.path) orelse @panic("bad file"); + + return Stat{ + .size = file.bytes.items.len, + .mode = 0x666, + .kind = file.kind, + .inode = @truncate(Stat.INode, std.hash.Wyhash.hash(10, handle.path)), + }; + } +}; const FSType = switch (FSImpl.choice) { FSImpl.Test => TestFS, @@ -1648,4 +1908,43 @@ test "PathName.init" { try std.testing.expectEqualStrings(res.ext, ".ext"); } -test {} +const expectEqual = std.testing.expectEqual; + +test "TestFS.init" { + var instance = try FileSystem.init1(default_allocator, null); + const fs = FileSystem; + var file = try fs.createFile("/hiiiiiii", .{ .truncate = true }); + try file.writeAll("wow"); + try file.seekTo(0); + var buf: [1024]u8 = undefined; + const read = try file.readAll(&buf); + try expectEqual(read, 3); + try expect(strings.eql(buf[0..read], "wow")); + + try expectEqual(read, try file.getPos()); + try file.seekTo(0); +} + +test "TestFS.make" { + var instance = try FileSystem.init1(default_allocator, null); + const fs = FileSystem; + TestFS.make(default_allocator, .{ + .@"/foo/bar/wow" = .{ + .data = "hello", + }, + .@"/foo/bar/hi" = .{ + .data = "greetings", + }, + .@"/foo/bar/salutations" = .{ + .data = "welcome", + }, + }); + var file = try fs.openFile("/foo/bar/hi", .{}); + var buf: [1024]u8 = undefined; + var data = buf[0..try file.readAll(&buf)]; + try std.testing.expectEqualStrings(data, "greetings"); + + var entries = try instance.readDirectory("/foo/bar", null); + var wow = entries.entries.get("wow").?; + try std.testing.expectEqualStrings(wow.entry.base(), "wow"); +} diff --git a/src/http.zig b/src/http.zig index 1479ad539..406bf9b35 100644 --- a/src/http.zig +++ b/src/http.zig @@ -388,8 +388,7 @@ pub const RequestContext = struct { if (stat.kind == .SymLink) { _ = FileSystem.openFileAbsolute(absolute_path, .{ .read = true }) catch return null; - absolute_path = std.os.getFdPath( - file.handle, + absolute_path = file.getPath( &Bundler.tmp_buildfile_buf, ) catch return null; @@ -2567,7 +2566,7 @@ pub const Server = struct { } }, .directory => { - rfs.bustEntriesCache(file_path); + FileSystem.instance.bustEntriesCache(file_path); ctx.bundler.resolver.dir_cache.remove(file_path); // if (event.op.delete or event.op.rename) diff --git a/src/linker.zig b/src/linker.zig index 7657c6665..4fb84d77f 100644 --- a/src/linker.zig +++ b/src/linker.zig @@ -96,7 +96,7 @@ pub const Linker = struct { } var file = try FileSystem.openFileZ(file_path.textZ(), .{ .read = true }); - var modkey = try Fs.RealFS.ModKey.generate(&this.fs.fs, file_path.text, file); + var modkey = try Fs.ModKey.generate(file_path.text, file); const hash_name = try modkey.hashName(file_path.name.base); if (Bundler.isCacheEnabled) { diff --git a/src/options.zig b/src/options.zig index 5c6949e0e..d06896b6e 100644 --- a/src/options.zig +++ b/src/options.zig @@ -1221,7 +1221,7 @@ pub const BundleOptions = struct { if (!static_dir_set) { chosen_dir = choice: { - if (fs.fs.readDirectory(fs.top_level_dir, null)) |dir_| { + if (fs.readDirectory(fs.top_level_dir, null)) |dir_| { const dir: *const Fs.EntriesOption = dir_; switch (dir.*) { .entries => { diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 5283ce812..198c11b73 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -28,6 +28,7 @@ const Path = Fs.Path; const NodeModuleBundle = @import("../node_module_bundle.zig").NodeModuleBundle; const FileSystem = Fs.FileSystem; +const File = Fs.File; pub fn isPackagePath(path: string) bool { // this could probably be flattened into something more optimized @@ -562,14 +563,14 @@ pub const Resolver = struct { var parts = [_]string{ FileSystem.instance.top_level_dir, std.fs.path.sep_str, route_dir }; const abs = FileSystem.instance.join(&parts); // must end in trailing slash - break :brk (std.os.realpath(abs, &buf) catch continue); + break :brk (File.realpath(abs, &buf) catch continue); } return error.MissingRouteDir; } else { var parts = [_]string{ FileSystem.instance.top_level_dir, std.fs.path.sep_str, pair.router.dir }; const abs = FileSystem.instance.join(&parts); // must end in trailing slash - break :brk std.os.realpath(abs, &buf) catch return error.MissingRouteDir; + break :brk File.realpath(abs, &buf) catch return error.MissingRouteDir; } }; |