From 4f98336f86477d5541f77c0375a931bec2abff45 Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Wed, 31 Jan 2024 22:44:23 -0800 Subject: [PATCH] fix(windows): fix installing non-ascii paths and make `normalizeBuf` generic (#8608) * comptime type normalizeStringBuf * delete * revert * revert revert * revert * remove unused * remove unnecessary assert * add comment * remove normalize, need ../ * use Output.err * update error message * generic T suffix * fix windows build * more fix build * more fix build * mkdiratZ * update test * [autofix.ci] apply automated fixes * update snapshot again * fix merge --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/install/migration.zig | 13 +- src/install/resolution.zig | 1 + src/libarchive/libarchive-bindings.zig | 2 +- src/libarchive/libarchive.zig | 77 +++-- src/resolver/resolve_path.zig | 301 ++++++++++++++---- src/string_immutable.zig | 82 +++-- src/sys.zig | 2 +- test/bundler/expectBundled.ts | 2 +- .../__snapshots__/inspect-error.test.js.snap | 14 +- test/js/bun/util/inspect-error.test.js | 18 +- 10 files changed, 372 insertions(+), 140 deletions(-) diff --git a/src/install/migration.zig b/src/install/migration.zig index 5a058963f3..8c404bce59 100644 --- a/src/install/migration.zig +++ b/src/install/migration.zig @@ -387,9 +387,20 @@ pub fn migrateNPMLockfile(this: *Lockfile, allocator: Allocator, log: *logger.Lo try this.workspace_paths.ensureTotalCapacity(allocator, wksp.map.unmanaged.entries.len); try this.workspace_versions.ensureTotalCapacity(allocator, wksp.map.unmanaged.entries.len); + var path_buf: if (Environment.isWindows) bun.PathBuffer else void = undefined; for (wksp.map.keys(), wksp.map.values()) |k, v| { const name_hash = stringHash(v.name); - this.workspace_paths.putAssumeCapacity(name_hash, builder.append(String, k)); + + this.workspace_paths.putAssumeCapacity( + name_hash, + builder.append( + String, + if (comptime Environment.isWindows) + bun.path.normalizeBuf(k, &path_buf, .windows) + else + k, + ), + ); if (v.version) |version_string| { const sliced_version = Semver.SlicedString.init(version_string, version_string); diff --git a/src/install/resolution.zig b/src/install/resolution.zig index 040d2c0fda..bc8f14eed4 100644 --- a/src/install/resolution.zig +++ b/src/install/resolution.zig @@ -9,6 +9,7 @@ const ExtractTarball = @import("./extract_tarball.zig"); const strings = @import("../string_immutable.zig"); const VersionedURL = @import("./versioned_url.zig").VersionedURL; const bun = @import("root").bun; +const Path = bun.path; pub const Resolution = extern struct { tag: Tag = .uninitialized, diff --git a/src/libarchive/libarchive-bindings.zig b/src/libarchive/libarchive-bindings.zig index 73a6cc8cb7..33b0766b29 100644 --- a/src/libarchive/libarchive-bindings.zig +++ b/src/libarchive/libarchive-bindings.zig @@ -1,5 +1,5 @@ const bun = @import("root").bun; -pub const wchar_t = c_int; +pub const wchar_t = u16; pub const la_int64_t = i64; pub const la_ssize_t = isize; pub const struct_archive = opaque {}; diff --git a/src/libarchive/libarchive.zig b/src/libarchive/libarchive.zig index 7f3bfeccee..d5d00754e0 100644 --- a/src/libarchive/libarchive.zig +++ b/src/libarchive/libarchive.zig @@ -489,7 +489,7 @@ pub const Archive = struct { var count: u32 = 0; const dir_fd = dir.fd; - var w_path: if (Environment.isWindows) bun.WPathBuffer else void = undefined; + var w_path_buf: if (Environment.isWindows) bun.WPathBuffer else void = undefined; loop: while (true) { const r = @as(Status, @enumFromInt(lib.archive_read_next_header(archive, &entry))); @@ -499,19 +499,40 @@ pub const Archive = struct { Status.retry => continue :loop, Status.failed, Status.fatal => return error.Fail, else => { - var pathname: [:0]const u8 = bun.sliceTo(lib.archive_entry_pathname(entry).?, 0); + // TODO: + // Due to path separator replacement and other copies that happen internally, libarchive changes the + // storage type of paths on windows to wide character strings. Using `archive_entry_pathname` or `archive_entry_pathname_utf8` + // on an wide character string will return null if there are non-ascii characters. + // (this can be seen by installing @fastify/send, which has a path "@fastify\send\test\fixtures\snow ☃") + // + // Ideally, we find a way to tell libarchive to not convert the strings to wide characters and also to not + // replace path separators. We can do both of these with our own normalization and utf8/utf16 string conversion code. + var pathname: bun.OSPathSliceZ = if (comptime Environment.isWindows) brk: { + const normalized = bun.path.normalizeBufT( + u16, + std.mem.span(lib.archive_entry_pathname_w(entry)), + &w_path_buf, + .windows, + ); + w_path_buf[normalized.len] = 0; + break :brk w_path_buf[0..normalized.len :0]; + } else std.mem.sliceTo(lib.archive_entry_pathname(entry), 0); if (comptime ContextType != void and @hasDecl(std.meta.Child(ContextType), "onFirstDirectoryName")) { if (appender.needs_first_dirname) { - appender.onFirstDirectoryName(strings.withoutTrailingSlash(bun.asByteSlice(pathname))); + if (comptime Environment.isWindows) { + const list = std.ArrayList(u8).init(default_allocator); + var result = try strings.toUTF8ListWithType(list, []const u16, pathname[0..pathname.len]); + // onFirstDirectoryName copies the contents of pathname to another buffer, safe to free + defer result.deinit(); + appender.onFirstDirectoryName(strings.withoutTrailingSlash(result.items)); + } else { + appender.onFirstDirectoryName(strings.withoutTrailingSlash(bun.asByteSlice(pathname))); + } } } - var tokenizer = if (comptime Environment.isWindows) - // TODO(dylan-conway): I think this should only be '/' - std.mem.tokenizeAny(u8, bun.asByteSlice(pathname), "/\\") - else - std.mem.tokenizeScalar(u8, bun.asByteSlice(pathname), '/'); + var tokenizer = std.mem.tokenizeScalar(bun.OSPathChar, pathname, std.fs.path.sep); comptime var depth_i: usize = 0; inline while (depth_i < depth_to_skip) : (depth_i += 1) { @@ -519,15 +540,15 @@ pub const Archive = struct { } const pathname_ = tokenizer.rest(); - pathname = @as([*]const u8, @ptrFromInt(@intFromPtr(pathname_.ptr)))[0..pathname_.len :0]; + pathname = @as([*]const bun.OSPathChar, @ptrFromInt(@intFromPtr(pathname_.ptr)))[0..pathname_.len :0]; if (pathname.len == 0) continue; const kind = C.kindFromMode(lib.archive_entry_filetype(entry)); - const slice = bun.asByteSlice(pathname); + const path_slice: bun.OSPathSlice = pathname.ptr[0..pathname.len]; if (comptime log) { - Output.prettyln(" {s}", .{pathname}); + Output.prettyln(" {}", .{bun.fmt.fmtOSPath(path_slice)}); } count += 1; @@ -546,15 +567,15 @@ pub const Archive = struct { mode |= 0o1; if (comptime Environment.isWindows) { - std.os.mkdirat(dir_fd, pathname, @as(u32, @intCast(mode))) catch |err| { + std.os.mkdiratW(dir_fd, pathname, @as(u32, @intCast(mode))) catch |err| { if (err == error.PathAlreadyExists or err == error.NotDir) break; - try bun.makePath(dir, std.fs.path.dirname(slice) orelse return err); - try std.os.mkdirat(dir_fd, pathname, 0o777); + try bun.MakePath.makePath(u16, dir, bun.Dirname.dirname(u16, path_slice) orelse return err); + try std.os.mkdiratW(dir_fd, pathname, 0o777); }; } else { std.os.mkdiratZ(dir_fd, pathname, @as(u32, @intCast(mode))) catch |err| { if (err == error.PathAlreadyExists or err == error.NotDir) break; - try bun.makePath(dir, std.fs.path.dirname(slice) orelse return err); + try bun.makePath(dir, std.fs.path.dirname(path_slice) orelse return err); try std.os.mkdiratZ(dir_fd, pathname, 0o777); }; } @@ -567,7 +588,7 @@ pub const Archive = struct { std.os.symlinkatZ(link_target, dir_fd, pathname) catch |err| brk: { switch (err) { error.AccessDenied, error.FileNotFound => { - dir.makePath(std.fs.path.dirname(slice) orelse return err) catch {}; + dir.makePath(std.fs.path.dirname(path_slice) orelse return err) catch {}; break :brk try std.os.symlinkatZ(link_target, dir_fd, pathname); }, else => { @@ -582,13 +603,12 @@ pub const Archive = struct { const file_handle_native = brk: { if (Environment.isWindows) { const flags = std.os.O.WRONLY | std.os.O.CREAT | std.os.O.TRUNC; - const os_path = bun.strings.toWPathNormalized(&w_path, slice); - switch (bun.sys.openatWindows(bun.toFD(dir_fd), os_path, flags)) { + switch (bun.sys.openatWindows(bun.toFD(dir_fd), pathname, flags)) { .result => |fd| break :brk fd, .err => |e| switch (e.errno) { @intFromEnum(bun.C.E.PERM), @intFromEnum(bun.C.E.NOENT) => { - dir.makePath(std.fs.path.dirname(slice) orelse return bun.errnoToZigErr(e.errno)) catch {}; - break :brk try bun.sys.openatWindows(bun.toFD(dir_fd), os_path, flags).unwrap(); + bun.MakePath.makePath(u16, dir, bun.Dirname.dirname(u16, path_slice) orelse return bun.errnoToZigErr(e.errno)) catch {}; + break :brk try bun.sys.openatWindows(bun.toFD(dir_fd), pathname, flags).unwrap(); }, else => { return bun.errnoToZigErr(e.errno); @@ -599,7 +619,7 @@ pub const Archive = struct { break :brk (dir.createFileZ(pathname, .{ .truncate = true, .mode = mode }) catch |err| { switch (err) { error.AccessDenied, error.FileNotFound => { - dir.makePath(std.fs.path.dirname(slice) orelse return err) catch {}; + dir.makePath(std.fs.path.dirname(path_slice) orelse return err) catch {}; break :brk (try dir.createFileZ(pathname, .{ .truncate = true, .mode = mode, @@ -636,14 +656,14 @@ pub const Archive = struct { if (size > 0) { if (ctx) |ctx_| { const hash: u64 = if (ctx_.pluckers.len > 0) - bun.hash(slice) + bun.hash(std.mem.sliceAsBytes(path_slice)) else @as(u64, 0); if (comptime ContextType != void and @hasDecl(std.meta.Child(ContextType), "appendMutable")) { const result = ctx.?.all_files.getOrPutAdapted(hash, Context.U64Context{}) catch unreachable; if (!result.found_existing) { - result.value_ptr.* = (try appender.appendMutable(@TypeOf(slice), slice)).ptr; + result.value_ptr.* = (try appender.appendMutable(@TypeOf(path_slice), path_slice)).ptr; } } @@ -678,13 +698,20 @@ pub const Archive = struct { lib.ARCHIVE_OK => break :possibly_retry, lib.ARCHIVE_RETRY => { if (comptime log) { - Output.prettyErrorln("[libarchive] Error extracting {s}, retry {d} / {d}", .{ pathname_, retries_remaining, 5 }); + Output.err("libarchive error", "extracting {}, retry {d} / {d}", .{ + bun.fmt.fmtOSPath(path_slice), + retries_remaining, + 5, + }); } }, else => { if (comptime log) { const archive_error = std.mem.span(lib.archive_error_string(archive)); - Output.prettyErrorln("[libarchive] Error extracting {s}: {s}", .{ pathname_, archive_error }); + Output.err("libarchive error", "extracting {}: {s}", .{ + bun.fmt.fmtOSPath(path_slice), + archive_error, + }); } return error.Fail; }, diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index 625cf0700f..663acd1b2d 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -34,12 +34,19 @@ inline fn nqlAtIndexCaseInsensitive(comptime string_count: comptime_int, index: } const IsSeparatorFunc = fn (char: u8) bool; +const IsSeparatorFuncT = fn (comptime T: type, char: anytype) bool; const LastSeparatorFunction = fn (slice: []const u8) ?usize; +const LastSeparatorFunctionT = fn (comptime T: type, slice: anytype) ?usize; inline fn @"is .."(slice: []const u8) bool { return slice.len >= 2 and @as(u16, @bitCast(slice[0..2].*)) == comptime std.mem.readInt(u16, "..", .little); } +inline fn @"is .. with type"(comptime T: type, slice: []const T) bool { + if (comptime T == u8) return @"is .."(slice); + return slice.len >= 2 and slice[0] == '.' and slice[1] == '.'; +} + inline fn isDotSlash(slice: []const u8) bool { return @as(u16, @bitCast(slice[0..2].*)) == comptime std.mem.readInt(u16, "./", .little); } @@ -517,6 +524,9 @@ pub fn relativeAlloc(allocator: std.mem.Allocator, from: []const u8, to: []const // https://cs.opensource.google/go/go/+/refs/tags/go1.17.6:src/path/filepath/path_windows.go;l=57 // volumeNameLen returns length of the leading volume name on Windows. fn windowsVolumeNameLen(path: []const u8) struct { usize, usize } { + return windowsVolumeNameLenT(u8, path); +} +fn windowsVolumeNameLenT(comptime T: type, path: []const T) struct { usize, usize } { if (path.len < 2) return .{ 0, 0 }; // with drive letter const c = path[0]; @@ -527,18 +537,32 @@ fn windowsVolumeNameLen(path: []const u8) struct { usize, usize } { } // UNC if (path.len >= 5 and - Platform.windows.isSeparator(path[0]) and - Platform.windows.isSeparator(path[1]) and - !Platform.windows.isSeparator(path[2]) and + Platform.windows.isSeparatorT(T, path[0]) and + Platform.windows.isSeparatorT(T, path[1]) and + !Platform.windows.isSeparatorT(T, path[2]) and path[2] != '.') { - if (strings.indexOfAny(path[3..], "/\\")) |idx| { - // TODO: handle input "//abc//def" should be picked up as a unc path - if (path.len > idx + 4 and !Platform.windows.isSeparator(path[idx + 4])) { - if (strings.indexOfAny(path[idx + 4 ..], "/\\")) |idx2| { - return .{ idx + idx2 + 4, idx + 3 }; - } else { - return .{ path.len, idx + 3 }; + if (T == u8) { + if (strings.indexOfAny(path[3..], "/\\")) |idx| { + // TODO: handle input "//abc//def" should be picked up as a unc path + if (path.len > idx + 4 and !Platform.windows.isSeparatorT(T, path[idx + 4])) { + if (strings.indexOfAny(path[idx + 4 ..], "/\\")) |idx2| { + return .{ idx + idx2 + 4, idx + 3 }; + } else { + return .{ path.len, idx + 3 }; + } + } + } + } else { + // TODO(dylan-conway): use strings.indexOfAny instead of std + if (std.mem.indexOfAny(T, path[3..], comptime strings.literal(T, "/\\"))) |idx| { + // TODO: handle input "//abc//def" should be picked up as a unc path + if (path.len > idx + 4 and !Platform.windows.isSeparatorT(T, path[idx + 4])) { + if (std.mem.indexOfAny(T, path[idx + 4 ..], comptime strings.literal(T, "/\\"))) |idx2| { + return .{ idx + idx2 + 4, idx + 3 }; + } else { + return .{ path.len, idx + 3 }; + } } } } @@ -550,30 +574,40 @@ pub fn windowsVolumeName(path: []const u8) []const u8 { return path[0..@call(.always_inline, windowsVolumeNameLen, .{path})[0]]; } -// path.relative lets you do relative across different share drives pub fn windowsFilesystemRoot(path: []const u8) []const u8 { + return windowsFilesystemRootT(u8, path); +} + +// path.relative lets you do relative across different share drives +pub fn windowsFilesystemRootT(comptime T: type, path: []const T) []const T { if (path.len < 3) - return if (isSepAny(path[0])) path[0..1] else path[0..0]; + return if (isSepAnyT(T, path[0])) path[0..1] else path[0..0]; // with drive letter const c = path[0]; - if (path[1] == ':' and isSepAny(path[2])) { + if (path[1] == ':' and isSepAnyT(T, path[2])) { if ('a' <= c and c <= 'z' or 'A' <= c and c <= 'Z') { return path[0..3]; } } // UNC if (path.len >= 5 and - Platform.windows.isSeparator(path[0]) and - Platform.windows.isSeparator(path[1]) and - !Platform.windows.isSeparator(path[2]) and + Platform.windows.isSeparatorT(T, path[0]) and + Platform.windows.isSeparatorT(T, path[1]) and + !Platform.windows.isSeparatorT(T, path[2]) and path[2] != '.') { - if (strings.indexOfAny(path[3..], "/\\")) |idx| { - // TODO: handle input "//abc//def" should be picked up as a unc path - return path[0 .. idx + 4]; + if (comptime T == u8) { + if (strings.indexOfAny(path[3..], "/\\")) |idx| { + // TODO: handle input "//abc//def" should be picked up as a unc path + return path[0 .. idx + 4]; + } + } else { + if (std.mem.indexOfAny(T, path[3..], "/\\")) |idx| { + return path[0 .. idx + 4]; + } } } - if (isSepAny(path[0])) return path[0..1]; + if (isSepAnyT(T, path[0])) return path[0..1]; return path[0..0]; } @@ -585,24 +619,34 @@ pub fn normalizeStringGeneric( comptime allow_above_root: bool, comptime separator: u8, comptime isSeparator: anytype, - _: anytype, comptime preserve_trailing_slash: bool, ) []u8 { - const isWindows = comptime separator == std.fs.path.sep_windows; + return normalizeStringGenericT(u8, path_, buf, allow_above_root, separator, isSeparator, preserve_trailing_slash); +} +pub fn normalizeStringGenericT( + comptime T: type, + path_: []const T, + buf: []T, + comptime allow_above_root: bool, + comptime separator: T, + comptime isSeparatorT: anytype, + comptime preserve_trailing_slash: bool, +) []T { + const isWindows, const sep_str = comptime .{ separator == std.fs.path.sep_windows, &[_]u8{separator} }; if (isWindows and bun.Environment.isDebug) { // this is here to catch a potential mistake by the caller // // since it is theoretically possible to get here in release // we will not do this check in release. - std.debug.assert(!strings.startsWith(path_, ":\\")); + std.debug.assert(!strings.hasPrefixComptimeType(T, path_, comptime strings.literal(T, ":\\"))); } var buf_i: usize = 0; var dotdot: usize = 0; const volLen, const indexOfThirdUNCSlash = if (isWindows and !allow_above_root) - windowsVolumeNameLen(path_) + windowsVolumeNameLenT(T, path_) else .{ 0, 0 }; @@ -610,7 +654,7 @@ pub fn normalizeStringGeneric( if (volLen > 0) { if (path_[1] != ':') { // UNC paths - buf[0..2].* = [_]u8{ separator, separator }; + buf[0..2].* = comptime strings.literalBuf(T, sep_str ++ sep_str); @memcpy(buf[2 .. indexOfThirdUNCSlash + 1], path_[2 .. indexOfThirdUNCSlash + 1]); buf[indexOfThirdUNCSlash] = separator; @memcpy( @@ -630,7 +674,7 @@ pub fn normalizeStringGeneric( buf_i = 2; dotdot = buf_i; } - } else if (path_.len > 0 and isSeparator(path_[0])) { + } else if (path_.len > 0 and isSeparatorT(T, path_[0])) { buf[buf_i] = separator; buf_i += 1; dotdot = 1; @@ -655,7 +699,7 @@ pub fn normalizeStringGeneric( if (isWindows and (allow_above_root or volLen > 0)) { // consume leading slashes on windows - if (r < n and isSeparator(path[r])) { + if (r < n and isSeparatorT(T, path[r])) { r += 1; buf[buf_i] = separator; buf_i += 1; @@ -666,31 +710,31 @@ pub fn normalizeStringGeneric( // empty path element // or // . element - if (isSeparator(path[r])) { + if (isSeparatorT(T, path[r])) { r += 1; continue; } - if (path[r] == '.' and (r + 1 == n or isSeparator(path[r + 1]))) { + if (path[r] == '.' and (r + 1 == n or isSeparatorT(T, path[r + 1]))) { // skipping two is a windows-specific bugfix r += 1; continue; } - if (@"is .."(path[r..]) and (r + 2 == n or isSeparator(path[r + 2]))) { + if (@"is .. with type"(T, path[r..]) and (r + 2 == n or isSeparatorT(T, path[r + 2]))) { r += 2; // .. element: remove to last separator if (buf_i > dotdot) { buf_i -= 1; - while (buf_i > dotdot and !isSeparator(buf[buf_i])) { + while (buf_i > dotdot and !isSeparatorT(T, buf[buf_i])) { buf_i -= 1; } } else if (allow_above_root) { if (buf_i > buf_start) { - buf[buf_i..][0..3].* = [_]u8{ separator, '.', '.' }; + buf[buf_i..][0..3].* = comptime strings.literalBuf(T, sep_str ++ ".."); buf_i += 3; } else { - buf[buf_i..][0..2].* = [_]u8{ '.', '.' }; + buf[buf_i..][0..2].* = comptime strings.literalBuf(T, ".."); buf_i += 2; } dotdot = buf_i; @@ -701,13 +745,13 @@ pub fn normalizeStringGeneric( // real path element. // add slash if needed - if (buf_i != buf_start and !isSeparator(buf[buf_i - 1])) { + if (buf_i != buf_start and !isSeparatorT(T, buf[buf_i - 1])) { buf[buf_i] = separator; buf_i += 1; } const from = r; - while (r < n and !isSeparator(path[r])) : (r += 1) {} + while (r < n and !isSeparatorT(T, path[r])) : (r += 1) {} const count = r - from; @memcpy(buf[buf_i..][0..count], path[from..][0..count]); buf_i += count; @@ -731,7 +775,7 @@ pub fn normalizeStringGeneric( const result = buf[0..buf_i]; if (bun.Environment.allow_assert and isWindows) { - std.debug.assert(!strings.startsWith(result, "\\:\\")); + std.debug.assert(!strings.hasPrefixComptimeType(T, result, comptime strings.literal(T, "\\:\\"))); } return result; @@ -744,12 +788,20 @@ pub const Platform = enum { posix, pub fn isAbsolute(comptime platform: Platform, path: []const u8) bool { + return isAbsoluteT(platform, u8, path); + } + + pub fn isAbsoluteT(comptime platform: Platform, comptime T: type, path: []const T) bool { + if (comptime T != u8 and T != u16) @compileError("Unsupported type given to isAbsoluteT"); return switch (comptime platform) { - .auto => (comptime platform.resolve()).isAbsolute(path), + .auto => (comptime platform.resolve()).isAbsoluteT(T, path), .posix => path.len > 0 and path[0] == '/', .windows, .loose, - => std.fs.path.isAbsoluteWindows(path), + => if (T == u8) + std.fs.path.isAbsoluteWindows(path) + else + std.fs.path.isAbsoluteWindowsWTF16(path), }; } @@ -789,6 +841,21 @@ pub const Platform = enum { } } + pub fn getSeparatorFuncT(comptime _platform: Platform) IsSeparatorFuncT { + switch (comptime _platform.resolve()) { + .auto => comptime unreachable, + .loose => { + return isSepAnyT; + }, + .windows => { + return isSepAnyT; + }, + .posix => { + return isSepPosixT; + }, + } + } + pub fn getLastSeparatorFunc(comptime _platform: Platform) LastSeparatorFunction { switch (comptime _platform.resolve()) { .auto => comptime unreachable, @@ -804,17 +871,36 @@ pub const Platform = enum { } } - pub inline fn isSeparator(comptime _platform: Platform, char: u8) bool { + pub fn getLastSeparatorFuncT(comptime _platform: Platform) LastSeparatorFunctionT { switch (comptime _platform.resolve()) { .auto => comptime unreachable, .loose => { - return isSepAny(char); + return lastIndexOfSeparatorLooseT; }, .windows => { - return isSepAny(char); + return lastIndexOfSeparatorWindowsT; }, .posix => { - return isSepPosix(char); + return lastIndexOfSeparatorPosixT; + }, + } + } + + pub inline fn isSeparator(comptime _platform: Platform, char: u8) bool { + return isSeparatorT(_platform, u8, char); + } + + pub inline fn isSeparatorT(comptime _platform: Platform, comptime T: type, char: T) bool { + switch (comptime _platform.resolve()) { + .auto => comptime unreachable, + .loose => { + return isSepAnyT(T, char); + }, + .windows => { + return isSepAnyT(T, char); + }, + .posix => { + return isSepPosixT(T, char); }, } } @@ -888,35 +974,57 @@ pub fn normalizeString(str: []const u8, comptime allow_above_root: bool, comptim } pub fn normalizeBuf(str: []const u8, buf: []u8, comptime _platform: Platform) []u8 { + return normalizeBufT(u8, str, buf, _platform); +} + +pub fn normalizeBufT(comptime T: type, str: []const T, buf: []T, comptime _platform: Platform) []T { if (str.len == 0) { buf[0] = '.'; return buf[0..1]; } - const is_absolute = _platform.isAbsolute(str); + const is_absolute = _platform.isAbsoluteT(T, str); - const trailing_separator = _platform.getLastSeparatorFunc()(str) == str.len - 1; + const trailing_separator = _platform.getLastSeparatorFuncT()(T, str) == str.len - 1; if (is_absolute and trailing_separator) - return normalizeStringBuf(str, buf, true, _platform, true); + return normalizeStringBufT(T, str, buf, true, _platform, true); if (is_absolute and !trailing_separator) - return normalizeStringBuf(str, buf, true, _platform, false); + return normalizeStringBufT(T, str, buf, true, _platform, false); if (!is_absolute and !trailing_separator) - return normalizeStringBuf(str, buf, false, _platform, false); + return normalizeStringBufT(T, str, buf, false, _platform, false); - return normalizeStringBuf(str, buf, false, _platform, true); + return normalizeStringBufT(T, str, buf, false, _platform, true); } -pub fn normalizeStringBuf(str: []const u8, buf: []u8, comptime allow_above_root: bool, comptime _platform: Platform, comptime preserve_trailing_slash: anytype) []u8 { +pub fn normalizeStringBuf( + str: []const u8, + buf: []u8, + comptime allow_above_root: bool, + comptime _platform: Platform, + comptime preserve_trailing_slash: anytype, +) []u8 { + return normalizeStringBufT(u8, str, buf, allow_above_root, _platform, preserve_trailing_slash); +} + +pub fn normalizeStringBufT( + comptime T: type, + str: []const T, + buf: []T, + comptime allow_above_root: bool, + comptime _platform: Platform, + comptime preserve_trailing_slash: anytype, +) []T { const platform = comptime _platform.resolve(); switch (comptime platform) { .auto => @compileError("unreachable"), .windows => { - return normalizeStringWindows( + return normalizeStringWindowsT( + T, str, buf, allow_above_root, @@ -924,7 +1032,8 @@ pub fn normalizeStringBuf(str: []const u8, buf: []u8, comptime allow_above_root: ); }, .posix => { - return normalizeStringLooseBuf( + return normalizeStringLooseBufT( + T, str, buf, allow_above_root, @@ -933,7 +1042,8 @@ pub fn normalizeStringBuf(str: []const u8, buf: []u8, comptime allow_above_root: }, .loose => { - return normalizeStringLooseBuf( + return normalizeStringLooseBufT( + T, str, buf, allow_above_root, @@ -1248,7 +1358,7 @@ fn _joinAbsStringBufWindows( // skip over volume name const volume = part[0..windowsVolumeNameLen(part)[0]]; - if (volume.len > 0 and !strings.eql(volume, root)) + if (volume.len > 0 and !strings.eqlLong(volume, root, true)) continue; const part_without_vol = part[volume.len..]; @@ -1278,23 +1388,48 @@ fn _joinAbsStringBufWindows( } pub fn isSepPosix(char: u8) bool { + return isSepPosixT(u8, char); +} + +pub fn isSepPosixT(comptime T: type, char: anytype) bool { + if (comptime @TypeOf(char) != T) @compileError("Incorrect type passed to isSepPosixT"); return char == std.fs.path.sep_posix; } pub fn isSepWin32(char: u8) bool { + return isSepWin32T(u8, char); +} + +pub fn isSepWin32T(comptime T: type, char: anytype) bool { + if (comptime @TypeOf(char) != T) @compileError("Incorrect type passed to isSepWin32T"); return char == std.fs.path.sep_windows; } pub fn isSepAny(char: u8) bool { - return @call(.always_inline, isSepPosix, .{char}) or @call(.always_inline, isSepWin32, .{char}); + return isSepAnyT(u8, char); +} + +pub fn isSepAnyT(comptime T: type, char: anytype) bool { + if (comptime @TypeOf(char) != T) @compileError("Incorrect type passed to isSepAnyT"); + return @call(.always_inline, isSepPosixT, .{ T, char }) or @call(.always_inline, isSepWin32T, .{ T, char }); } pub fn lastIndexOfSeparatorWindows(slice: []const u8) ?usize { - return std.mem.lastIndexOfAny(u8, slice, "\\/"); + return lastIndexOfSeparatorWindowsT(u8, slice); +} + +pub fn lastIndexOfSeparatorWindowsT(comptime T: type, slice: anytype) ?usize { + if (comptime std.meta.Child(@TypeOf(slice)) != T) @compileError("Invalid type passed to lastIndexOfSeparatorWindowsT"); + return std.mem.lastIndexOfAny(T, slice, comptime strings.literal(T, "\\/")); } pub fn lastIndexOfSeparatorPosix(slice: []const u8) ?usize { - return std.mem.lastIndexOfScalar(u8, slice, std.fs.path.sep_posix); + return lastIndexOfSeparatorPosixT(u8, slice); +} + +pub fn lastIndexOfSeparatorPosixT(comptime T: type, slice: anytype) ?usize { + if (comptime std.meta.Child(@TypeOf(slice)) != T) @compileError("Invalid type passed to lastIndexOfSeparatorPosixT"); + return std.mem.lastIndexOfScalar(T, slice, std.fs.path.sep_posix); } pub fn lastIndexOfNonSeparatorPosix(slice: []const u8) ?u32 { @@ -1309,7 +1444,12 @@ pub fn lastIndexOfNonSeparatorPosix(slice: []const u8) ?u32 { } pub fn lastIndexOfSeparatorLoose(slice: []const u8) ?usize { - return lastIndexOfSep(slice); + return lastIndexOfSeparatorLooseT(u8, slice); +} + +pub fn lastIndexOfSeparatorLooseT(comptime T: type, slice: anytype) ?usize { + if (comptime std.meta.Child(@TypeOf(slice)) != T) @compileError("Invalid type passed to lastIndexOfSeparatorLooseT"); + return lastIndexOfSepT(T, slice); } pub fn normalizeStringLooseBuf( @@ -1318,13 +1458,23 @@ pub fn normalizeStringLooseBuf( comptime allow_above_root: bool, comptime preserve_trailing_slash: bool, ) []u8 { - return normalizeStringGeneric( + return normalizeStringLooseBufT(u8, str, buf, allow_above_root, preserve_trailing_slash); +} + +pub fn normalizeStringLooseBufT( + comptime T: type, + str: []const T, + buf: []T, + comptime allow_above_root: bool, + comptime preserve_trailing_slash: bool, +) []T { + return normalizeStringGenericT( + T, str, buf, allow_above_root, std.fs.path.sep_posix, - isSepAny, - lastIndexOfSeparatorLoose, + isSepAnyT, preserve_trailing_slash, ); } @@ -1335,13 +1485,23 @@ pub fn normalizeStringWindows( comptime allow_above_root: bool, comptime preserve_trailing_slash: bool, ) []u8 { - return normalizeStringGeneric( + return normalizeStringWindowsT(u8, str, buf, allow_above_root, preserve_trailing_slash); +} + +pub fn normalizeStringWindowsT( + comptime T: type, + str: []const T, + buf: []T, + comptime allow_above_root: bool, + comptime preserve_trailing_slash: bool, +) []T { + return normalizeStringGenericT( + T, str, buf, allow_above_root, std.fs.path.sep_windows, - isSepAny, - lastIndexOfSeparatorWindows, + isSepAnyT, preserve_trailing_slash, ); } @@ -1368,16 +1528,14 @@ pub fn normalizeStringNode( buf_, true, comptime platform.resolve().separator(), - comptime platform.getSeparatorFunc(), - comptime platform.getLastSeparatorFunc(), + comptime platform.getSeparatorFuncT(), false, ) else normalizeStringGeneric( str, buf_, false, comptime platform.resolve().separator(), - comptime platform.getSeparatorFunc(), - comptime platform.getLastSeparatorFunc(), + comptime platform.getSeparatorFuncT(), false, ); @@ -1765,12 +1923,17 @@ pub fn basename(path: []const u8) []const u8 { return path[start_index + 1 .. end_index]; } + pub fn lastIndexOfSep(path: []const u8) ?usize { + return lastIndexOfSepT(u8, path); +} + +pub fn lastIndexOfSepT(comptime T: type, path: []const T) ?usize { if (comptime !bun.Environment.isWindows) { - return strings.lastIndexOfChar(path, '/'); + return strings.lastIndexOfCharT(T, path, '/'); } - return std.mem.lastIndexOfAny(u8, path, "/\\"); + return std.mem.lastIndexOfAny(T, path, "/\\"); } pub fn nextDirname(path_: []const u8) ?[]const u8 { diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 8ec698fc1e..47ad783768 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -40,34 +40,51 @@ pub inline fn w(comptime str: []const u8) [:0]const u16 { } pub fn toUTF16Literal(comptime str: []const u8) []const u16 { - return comptime brk: { - comptime var output: [str.len]u16 = undefined; - - for (str, 0..) |c, i| { - output[i] = c; - } - - const Static = struct { - pub const literal: []const u16 = output[0..]; - }; - break :brk Static.literal; - }; + return comptime literal(u16, str); } -pub fn toUTF16LiteralZ(comptime str: []const u8) [:0]const u16 { - return comptime brk: { - comptime var output: [str.len + 1]u16 = undefined; +pub inline fn literal(comptime T: type, comptime str: string) []const T { + if (!@inComptime()) @compileError("strings.literal() should be called in a comptime context"); + comptime var output: [str.len]T = undefined; - for (str, 0..) |c, i| { - output[i] = c; - } - output[str.len] = 0; + for (str, 0..) |c, i| { + // TODO(dylan-conway): should we check for non-ascii characters like JSC does with operator""_s + output[i] = c; + } - const Static = struct { - pub const literal: [:0]const u16 = output[0..str.len :0]; - }; - break :brk Static.literal; + const Static = struct { + pub const literal: []const T = output[0..]; }; + return Static.literal; +} + +pub inline fn literalBuf(comptime T: type, comptime str: string) [str.len]T { + if (!@inComptime()) @compileError("strings.literalBuf() should be called in a comptime context"); + comptime var output: [str.len]T = undefined; + + for (str, 0..) |c, i| { + // TODO(dylan-conway): should we check for non-ascii characters like JSC does with operator""_s + output[i] = c; + } + + const Static = struct { + pub const literal: [str.len]T = output; + }; + return Static.literal; +} + +pub inline fn toUTF16LiteralZ(comptime str: []const u8) [:0]const u16 { + comptime var output: [str.len + 1]u16 = undefined; + + for (str, 0..) |c, i| { + output[i] = c; + } + output[str.len] = 0; + + const Static = struct { + pub const literal: [:0]const u16 = output[0..str.len :0]; + }; + return Static.literal; } pub const OptionalUsize = std.meta.Int(.unsigned, @bitSizeOf(usize) - 1); @@ -227,8 +244,12 @@ pub fn indexOfSigned(self: string, str: string) i32 { return @as(i32, @intCast(i)); } -pub inline fn lastIndexOfChar(self: string, char: u8) ?usize { - return std.mem.lastIndexOfScalar(u8, self, char); +pub inline fn lastIndexOfChar(self: []const u8, char: u8) ?usize { + return lastIndexOfCharT(u8, self, char); +} + +pub inline fn lastIndexOfCharT(comptime T: type, self: []const T, char: T) ?usize { + return std.mem.lastIndexOfScalar(T, self, char); } pub inline fn lastIndexOf(self: string, str: string) ?usize { @@ -860,13 +881,10 @@ pub fn hasPrefixComptimeType(comptime T: type, self: []const T, comptime alt: an const rhs = comptime switch (T) { u8 => alt, u16 => switch (std.meta.Child(@TypeOf(alt))) { - u8 => w(alt), - else => |t| switch (std.meta.Child(t)) { - u8 => w(alt), - else => alt, - }, + u16 => alt, + else => w(alt), }, - else => unreachable, + else => @compileError("Unsupported type given to hasPrefixComptimeType"), }; return self.len >= alt.len and eqlComptimeCheckLenWithType(T, self[0..rhs.len], rhs, false); } @@ -1713,7 +1731,7 @@ pub fn addNTPathPrefix(wbuf: []u16, utf16: []const u16) [:0]const u16 { } pub fn addNTPathPrefixIfNeeded(wbuf: []u16, utf16: []const u16) [:0]const u16 { - if (hasPrefixComptimeType(u16, utf16, &bun.windows.nt_object_prefix)) { + if (hasPrefixComptimeType(u16, utf16, bun.windows.nt_object_prefix)) { @memcpy(wbuf[0..utf16.len], utf16); wbuf[utf16.len] = 0; return wbuf[0..utf16.len :0]; diff --git a/src/sys.zig b/src/sys.zig index 537c126980..5a436707ea 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -598,7 +598,7 @@ pub fn ntCreateFile( // file specification, provided that the floppy driver and overlying file system are already // loaded. For more information, see File Names, Paths, and Namespaces. .ObjectName = &nt_name, - .RootDirectory = if (bun.strings.hasPrefixComptimeType(u16, path, &windows.nt_object_prefix)) + .RootDirectory = if (bun.strings.hasPrefixComptimeType(u16, path, windows.nt_object_prefix)) null else if (dir == bun.invalid_fd) std.fs.cwd().fd diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index 83bcc0e2a9..2cf4dff3ce 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -789,7 +789,7 @@ function expectBundled( const warningText = stderr!.toUnixString(); const allWarnings = warnParser(warningText).map(([error, source]) => { const [_str2, fullFilename, line, col] = source.match(/bun-build-tests[\/\\](.*):(\d+):(\d+)/)!; - const file = fullFilename.slice(id.length + path.basename(outBase).length + 1); + const file = fullFilename.slice(id.length + path.basename(outBase).length + 1).replaceAll("\\", "/"); return { error, file, line, col }; }); const expectedWarnings = bundleWarnings diff --git a/test/js/bun/util/__snapshots__/inspect-error.test.js.snap b/test/js/bun/util/__snapshots__/inspect-error.test.js.snap index 82555bf5c5..5bf59a64d0 100644 --- a/test/js/bun/util/__snapshots__/inspect-error.test.js.snap +++ b/test/js/bun/util/__snapshots__/inspect-error.test.js.snap @@ -23,15 +23,15 @@ error: error 1 `; exports[`Error 1`] = ` -" 6 | const err2 = new Error("error 2", { cause: err }); - 7 | expect(Bun.inspect(err2).replaceAll(import.meta.dir, "[dir]")).toMatchSnapshot(); - 8 | }); - 9 | -10 | test("Error", () => { -11 | const err = new Error("my message"); +"10 | .replaceAll("//", "/"), +11 | ).toMatchSnapshot(); +12 | }); +13 | +14 | test("Error", () => { +15 | const err = new Error("my message"); ^ error: my message - at [dir]/inspect-error.test.js:11:15 + at [dir]/inspect-error.test.js:15:15 " `; diff --git a/test/js/bun/util/inspect-error.test.js b/test/js/bun/util/inspect-error.test.js index f9f8feb701..56f72e4aea 100644 --- a/test/js/bun/util/inspect-error.test.js +++ b/test/js/bun/util/inspect-error.test.js @@ -4,12 +4,20 @@ import { test, expect } from "bun:test"; test("error.cause", () => { const err = new Error("error 1"); const err2 = new Error("error 2", { cause: err }); - expect(Bun.inspect(err2).replaceAll(import.meta.dir, "[dir]")).toMatchSnapshot(); + expect( + Bun.inspect(err2) + .replaceAll(import.meta.dir, "[dir]") + .replaceAll("\\", "/"), + ).toMatchSnapshot(); }); test("Error", () => { const err = new Error("my message"); - expect(Bun.inspect(err).replaceAll(import.meta.dir, "[dir]")).toMatchSnapshot(); + expect( + Bun.inspect(err) + .replaceAll(import.meta.dir, "[dir]") + .replaceAll("\\", "/"), + ).toMatchSnapshot(); }); test("BuildMessage", async () => { @@ -17,6 +25,10 @@ test("BuildMessage", async () => { await import("./inspect-error-fixture-bad.js"); expect.unreachable(); } catch (e) { - expect(Bun.inspect(e).replaceAll(import.meta.dir, "[dir]")).toMatchSnapshot(); + expect( + Bun.inspect(e) + .replaceAll(import.meta.dir, "[dir]") + .replaceAll("\\", "/"), + ).toMatchSnapshot(); } });