From 5af782344f20b0ab44fbd21bd0190a43d65e7e40 Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Fri, 13 Sep 2024 22:41:33 -0700 Subject: [PATCH] fix(watch): use case insensitive path comparison (#13909) Co-authored-by: Jarred Sumner --- src/bun.js/node/node_fs.zig | 1 + src/fs.zig | 14 ++++------- src/resolver/resolve_path.zig | 10 ++++++-- src/string_immutable.zig | 23 +++++++++++++++++-- src/windows.zig | 9 +++++++- .../fixtures/bundler-reloader-script.ts | 4 ++++ test/cli/hot/watch.test.ts | 3 ++- test/cli/watch/watch.test.ts | 1 + test/harness.ts | 2 +- test/js/node/fs/fs.test.ts | 6 +++-- 10 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index ca3339b259..c8468290fb 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -5051,6 +5051,7 @@ pub const NodeFS = struct { if (Environment.isWindows) { var req: uv.fs_t = uv.fs_t.uninitialized; + defer req.deinit(); const rc = uv.uv_fs_mkdtemp(bun.Async.Loop.get(), &req, @ptrCast(prefix_buf.ptr), null); if (rc.errno()) |errno| { return .{ .err = .{ .errno = errno, .syscall = .mkdtemp, .path = prefix_buf[0 .. len + 6] } }; diff --git a/src/fs.zig b/src/fs.zig index 7eb3eaf91a..20c9317d6f 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -19,6 +19,7 @@ const Fs = @This(); const path_handler = @import("./resolver/resolve_path.zig"); const PathString = bun.PathString; const allocators = bun.allocators; +const OOM = bun.OOM; const MAX_PATH_BYTES = bun.MAX_PATH_BYTES; const PathBuffer = bun.PathBuffer; @@ -36,7 +37,7 @@ pub const Preallocate = struct { }; pub const FileSystem = struct { - top_level_dir: string = if (Environment.isWindows) "C:\\" else "/", + top_level_dir: string, // used on subsequent updates top_level_dir_buf: bun.PathBuffer = undefined, @@ -46,8 +47,6 @@ pub const FileSystem = struct { dirname_store: *DirnameStore, filename_store: *FilenameStore, - _tmpdir: ?std.fs.Dir = null, - threadlocal var tmpdir_handle: ?std.fs.Dir = null; pub fn topLevelDirWithoutTrailingSlash(this: *const FileSystem) []const u8 { @@ -136,7 +135,6 @@ pub const FileSystem = struct { }; instance_loaded = true; - instance.fs.parent_fs = &instance; _ = DirEntry.EntryStore.init(allocator); } @@ -536,7 +534,6 @@ pub const FileSystem = struct { entries_mutex: Mutex = .{}, entries: *EntriesOption.Map, cwd: string, - parent_fs: *FileSystem = undefined, file_limit: usize = 32, file_quota: usize = 32, @@ -617,7 +614,7 @@ pub const FileSystem = struct { var existing = this.entries.atIndex(index) orelse return null; if (existing.* == .entries) { if (existing.entries.generation < generation) { - var handle = bun.openDirA(std.fs.cwd(), existing.entries.dir) catch |err| { + var handle = bun.openDirForIteration(std.fs.cwd(), existing.entries.dir) catch |err| { existing.entries.data.clearAndFree(bun.fs_allocator); return this.readDirectoryError(existing.entries.dir, err) catch unreachable; @@ -991,7 +988,7 @@ pub const FileSystem = struct { return dir; } - fn readDirectoryError(fs: *RealFS, dir: string, err: anyerror) !*EntriesOption { + fn readDirectoryError(fs: *RealFS, dir: string, err: anyerror) OOM!*EntriesOption { if (comptime FeatureFlags.enable_entry_cache) { var get_or_put_result = try fs.entries.getOrPut(dir); const opt = try fs.entries.put(&get_or_put_result, EntriesOption{ @@ -1092,8 +1089,7 @@ pub const FileSystem = struct { iterator, ) catch |err| { if (in_place) |existing| existing.data.clearAndFree(bun.fs_allocator); - - return fs.readDirectoryError(dir, err) catch bun.outOfMemory(); + return try fs.readDirectoryError(dir, err); }; if (comptime FeatureFlags.enable_entry_cache) { diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index 01c87e1728..6a9f7fb992 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -74,7 +74,13 @@ pub fn isParentOrEqual(parent_: []const u8, child: []const u8) ParentEqual { while (parent.len > 0 and isSepAny(parent[parent.len - 1])) { parent = parent[0 .. parent.len - 1]; } - if (std.mem.indexOf(u8, child, parent) != 0) return .unrelated; + + const contains = if (comptime !bun.Environment.isLinux) + strings.containsCaseInsensitiveASCII + else + strings.contains; + if (!contains(child, parent)) return .unrelated; + if (child.len == parent.len) return .equal; if (isSepAny(child[parent.len])) return .parent; return .unrelated; @@ -681,7 +687,7 @@ pub fn windowsFilesystemRootT(comptime T: type, path: []const T) []const T { { if (bun.strings.indexAnyComptimeT(T, path[3..], "/\\")) |idx| { if (bun.strings.indexAnyComptimeT(T, path[4 + idx ..], "/\\")) |idx_second| { - return path[0 .. idx + idx_second + 4]; + return path[0 .. idx + idx_second + 4 + 1]; // +1 to skip second separator } return path[0..]; } diff --git a/src/string_immutable.zig b/src/string_immutable.zig index db72ec1b5e..3fd2aa4728 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -36,6 +36,17 @@ pub inline fn containsT(comptime T: type, self: []const T, str: []const T) bool return indexOfT(T, self, str) != null; } +pub inline fn containsCaseInsensitiveASCII(self: string, str: string) bool { + var start: usize = 0; + while (start + str.len <= self.len) { + if (eqlCaseInsensitiveASCIIIgnoreLength(self[start..][0..str.len], str)) { + return true; + } + start += 1; + } + return false; +} + pub inline fn removeLeadingDotSlash(slice: []const u8) []const u8 { if (slice.len >= 2) { if ((@as(u16, @bitCast(slice[0..2].*)) == comptime std.mem.readInt(u16, "./", .little)) or @@ -1731,8 +1742,16 @@ pub fn toNTPath(wbuf: []u16, utf8: []const u8) [:0]const u16 { return toWPathNormalized(wbuf, utf8); } - wbuf[0..4].* = bun.windows.nt_object_prefix; - return wbuf[0 .. toWPathNormalized(wbuf[4..], utf8).len + 4 :0]; + // UNC absolute path, replace leading '\\' with '\??\UNC\' + if (strings.hasPrefixComptime(utf8, "\\\\")) { + const prefix = bun.windows.nt_unc_object_prefix; + wbuf[0..prefix.len].* = prefix; + return wbuf[0 .. toWPathNormalized(wbuf[prefix.len..], utf8[2..]).len + prefix.len :0]; + } + + const prefix = bun.windows.nt_object_prefix; + wbuf[0..prefix.len].* = prefix; + return wbuf[0 .. toWPathNormalized(wbuf[prefix.len..], utf8).len + prefix.len :0]; } pub fn addNTPathPrefix(wbuf: []u16, utf16: []const u16) [:0]const u16 { diff --git a/src/windows.zig b/src/windows.zig index 0c64a824e3..08573d4b30 100644 --- a/src/windows.zig +++ b/src/windows.zig @@ -69,6 +69,7 @@ pub const advapi32 = windows.advapi32; pub const INVALID_FILE_ATTRIBUTES: u32 = std.math.maxInt(u32); pub const nt_object_prefix = [4]u16{ '\\', '?', '?', '\\' }; +pub const nt_unc_object_prefix = [8]u16{ '\\', '?', '?', '\\', 'U', 'N', 'C', '\\' }; pub const nt_maxpath_prefix = [4]u16{ '\\', '\\', '?', '\\' }; const std = @import("std"); @@ -3420,8 +3421,14 @@ pub fn GetFinalPathNameByHandle( bun.sys.syslog("GetFinalPathNameByHandleW({*p}) = {}", .{ hFile, bun.fmt.utf16(ret) }); - if (ret.len > 4 and std.mem.eql(u16, ret[0..4], &.{ '\\', '\\', '?', '\\' })) { + if (bun.strings.hasPrefixComptimeType(u16, ret, nt_maxpath_prefix)) { + // '\\?\C:\absolute\path' -> 'C:\absolute\path' ret = ret[4..]; + if (bun.strings.hasPrefixComptimeUTF16(ret, "UNC\\")) { + // '\\?\UNC\absolute\path' -> '\\absolute\path' + ret[2] = '\\'; + ret = ret[2..]; + } } return ret; diff --git a/test/bundler/fixtures/bundler-reloader-script.ts b/test/bundler/fixtures/bundler-reloader-script.ts index 051bd27cd2..e901067a02 100644 --- a/test/bundler/fixtures/bundler-reloader-script.ts +++ b/test/bundler/fixtures/bundler-reloader-script.ts @@ -18,11 +18,15 @@ try { } catch (e) {} await Bun.write(input, "import value from './mutate.js';\n" + `export default value;` + "\n"); +await Bun.sleep(1000); + await Bun.build({ entrypoints: [input], }); await Bun.write(mutate, "export default 1;\n"); +await Bun.sleep(1000); + const maxfd = openSync(process.execPath, 0); closeSync(maxfd); const { outputs: second } = await Bun.build({ diff --git a/test/cli/hot/watch.test.ts b/test/cli/hot/watch.test.ts index 094d916707..65c336c57d 100644 --- a/test/cli/hot/watch.test.ts +++ b/test/cli/hot/watch.test.ts @@ -5,13 +5,14 @@ import { writeFile } from "node:fs/promises"; import { join } from "node:path"; describe("--watch works", async () => { - for (const watchedFile of ["tmp.js", "entry.js"]) { + for (const watchedFile of ["entry.js", "tmp.js"]) { test(`with ${watchedFile}`, async () => { const tmpdir_ = tempDirWithFiles("watch-fixture", { "tmp.js": "console.log('hello #1')", "entry.js": "import './tmp.js'", "package.json": JSON.stringify({ name: "foo", version: "0.0.1" }), }); + await Bun.sleep(1000); const tmpfile = join(tmpdir_, "tmp.js"); const process = spawn({ cmd: [bunExe(), "--watch", join(tmpdir_, watchedFile)], diff --git a/test/cli/watch/watch.test.ts b/test/cli/watch/watch.test.ts index 49b7896b21..b30dfd1436 100644 --- a/test/cli/watch/watch.test.ts +++ b/test/cli/watch/watch.test.ts @@ -18,6 +18,7 @@ for (const dir of ["dir", "©️"]) { let i = 0; await updateFile(i); + await Bun.sleep(1000); watchee = spawn({ cwd, cmd: [bunExe(), "--watch", "watchee.js"], diff --git a/test/harness.ts b/test/harness.ts index 8b5877a51a..d28882adfb 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -969,7 +969,7 @@ export async function runBunInstall( }); expect(stdout).toBeDefined(); expect(stderr).toBeDefined(); - let err = (await new Response(stderr).text()).replace(/warn: Slow filesystem/g, ""); + let err = (await new Response(stderr).text()).replace(/warn: Slow filesystem.*/g, ""); expect(err).not.toContain("panic:"); if (!options?.allowErrors) { expect(err).not.toContain("error:"); diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index 11187ce2d2..2b64c59115 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -1883,8 +1883,10 @@ describe("fs.WriteStream", () => { }); stream.on("finish", () => { - expect(readFileSync(path, "utf8")).toBe("Test file written successfully"); - done(); + Bun.sleep(1000).then(() => { + expect(readFileSync(path, "utf8")).toBe("Test file written successfully"); + done(); + }); }); });