diff --git a/scripts/build.ps1 b/scripts/build.ps1 index facf34749c..f70328c71a 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -1,2 +1,2 @@ -.\scripts\env.sh +.\scripts\env.ps1 ninja -Cbuild diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 6ae05bd220..ac9b00aef1 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -5273,13 +5273,52 @@ pub const NodeFS = struct { pub fn writeFileWithPathBuffer(pathbuf: *[bun.MAX_PATH_BYTES]u8, args: Arguments.WriteFile) Maybe(Return.WriteFile) { var path: [:0]const u8 = undefined; + var pathbuf2: [bun.MAX_PATH_BYTES]u8 = undefined; const fd = switch (args.file) { .path => brk: { - path = args.file.path.sliceZ(pathbuf); + // On Windows, we potentially mutate the path in posixToPlatformInPlace + // We cannot mutate JavaScript strings in-place. That will break many things. + // So we must always copy the path string on Windows. + path = args.file.path.sliceZWithForceCopy(pathbuf, Environment.isWindows); + bun.path.posixToPlatformInPlace(u8, @constCast(path)); + + var is_dirfd_different = false; + var dirfd = args.dirfd; + if (Environment.isWindows) { + while (std.mem.startsWith(u8, path, "..\\")) { + is_dirfd_different = true; + var buffer: bun.WPathBuffer = undefined; + const dirfd_path_len = std.os.windows.kernel32.GetFinalPathNameByHandleW(args.dirfd.cast(), &buffer, buffer.len, 0); + const dirfd_path = buffer[0..dirfd_path_len]; + const parent_path = bun.Dirname.dirname(u16, dirfd_path).?; + if (std.mem.startsWith(u16, parent_path, &bun.windows.nt_maxpath_prefix)) @constCast(parent_path)[1] = '?'; + const newdirfd = switch (bun.sys.openDirAtWindows(bun.invalid_fd, parent_path, false, true)) { + .result => |fd| fd, + .err => |err| { + return .{ .err = err.withPath(path) }; + }, + }; + path = path[3..]; + dirfd = newdirfd; + } + } + defer if (is_dirfd_different) { + var d = dirfd.asDir(); + d.close(); + }; + if (Environment.isWindows) { + // windows openat does not support path traversal, fix it here. + // use pathbuf2 here since without it 'panic: @memcpy arguments alias' triggers + if (std.mem.indexOf(u8, path, "\\.\\") != null or std.mem.indexOf(u8, path, "\\..\\") != null) { + const fixed_path = bun.path.normalizeStringWindows(path, &pathbuf2, false, false); + pathbuf2[fixed_path.len] = 0; + path = pathbuf2[0..fixed_path.len :0]; + } + } const open_result = Syscall.openat( - args.dirfd, + dirfd, path, @intFromEnum(args.flag) | os.O.NOCTTY, args.mode, diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 07336d146d..aa1a247b4e 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -657,12 +657,17 @@ pub const PathLike = union(enum) { pub fn sliceZWithForceCopy(this: PathLike, buf: *[bun.MAX_PATH_BYTES]u8, comptime force: bool) [:0]const u8 { const sliced = this.slice(); + if (Environment.isWindows) { + if (std.fs.path.isAbsolute(sliced)) { + return resolve_path.PosixToWinNormalizer.resolveCWDWithExternalBufZ(buf, sliced) catch @panic("Error while resolving path."); + } + } + if (sliced.len == 0) return ""; if (comptime !force) { if (sliced[sliced.len - 1] == 0) { - var sliced_ptr = sliced.ptr; - return sliced_ptr[0 .. sliced.len - 1 :0]; + return sliced[0 .. sliced.len - 1 :0]; } } @@ -672,13 +677,6 @@ pub const PathLike = union(enum) { } pub inline fn sliceZ(this: PathLike, buf: *[bun.MAX_PATH_BYTES]u8) [:0]const u8 { - if (Environment.isWindows) { - const data = this.slice(); - if (!std.fs.path.isAbsolute(data)) { - return sliceZWithForceCopy(this, buf, false); - } - return resolve_path.PosixToWinNormalizer.resolveCWDWithExternalBufZ(buf, data) catch @panic("Error while resolving path."); - } return sliceZWithForceCopy(this, buf, false); } diff --git a/src/bun.zig b/src/bun.zig index 353c466b62..168f8cd9b5 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -42,6 +42,7 @@ pub const resolver = @import("./resolver//resolver.zig"); pub const DirIterator = @import("./bun.js/node/dir_iterator.zig"); pub const PackageJSON = @import("./resolver/package_json.zig").PackageJSON; pub const fmt = @import("./fmt.zig"); +pub const allocators = @import("./allocators.zig"); pub const shell = struct { pub usingnamespace @import("./shell/shell.zig"); @@ -552,6 +553,16 @@ pub inline fn isSliceInBuffer(slice: []const u8, buffer: []const u8) bool { return slice.len > 0 and @intFromPtr(buffer.ptr) <= @intFromPtr(slice.ptr) and ((@intFromPtr(slice.ptr) + slice.len) <= (@intFromPtr(buffer.ptr) + buffer.len)); } +pub inline fn sliceInBuffer(stable: string, value: string) string { + if (allocators.sliceRange(stable, value)) |_| { + return value; + } + if (strings.indexOf(stable, value)) |index| { + return stable[index..][0..value.len]; + } + return value; +} + pub fn rangeOfSliceInBuffer(slice: []const u8, buffer: []const u8) ?[2]u32 { if (!isSliceInBuffer(slice, buffer)) return null; const r = [_]u32{ diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 6e633099a2..e144ce5430 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -666,6 +666,7 @@ pub const BundleV2 = struct { } _ = @atomicRmw(usize, &this.graph.parse_pending, .Add, 1, .Monotonic); const source_index = Index.source(this.graph.input_files.len); + if (path.pretty.ptr == path.text.ptr) { // TODO: outbase const rel = bun.path.relative(this.bundler.fs.top_level_dir, path.text); @@ -674,6 +675,8 @@ pub const BundleV2 = struct { } } path.* = try path.dupeAlloc(this.graph.allocator); + // TODO: this shouldn't be necessary + path.pretty = bun.sliceInBuffer(path.text, path.pretty); entry.value_ptr.* = source_index.get(); this.graph.ast.append(bun.default_allocator, JSAst.empty) catch unreachable; @@ -1115,9 +1118,7 @@ pub const BundleV2 = struct { }, }, .size = source.contents.len, - .output_path = std.fmt.allocPrint(bun.default_allocator, "{}", .{ - template, - }) catch unreachable, + .output_path = std.fmt.allocPrint(bun.default_allocator, "{}", .{template}) catch unreachable, .input_path = bun.default_allocator.dupe(u8, source.path.text) catch unreachable, .input_loader = .file, .output_kind = .asset, @@ -2029,6 +2030,8 @@ pub const BundleV2 = struct { } path.* = path.dupeAlloc(this.graph.allocator) catch @panic("Ran out of memory"); + // TODO: this shouldn't be necessary + path.pretty = bun.sliceInBuffer(path.text, path.pretty); import_record.path = path.*; debug("created ParseTask: {s}", .{path.text}); @@ -9027,11 +9030,20 @@ const LinkerContext = struct { chunk.template.placeholder.hash = chunk.isolated_hash; const rel_path = std.fmt.allocPrint(c.allocator, "{any}", .{chunk.template}) catch unreachable; + bun.path.platformToPosixInPlace(u8, rel_path); + if ((try path_names_map.getOrPut(rel_path)).found_existing) { try c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "Multiple files share the same output path: {s}", .{rel_path}); return error.DuplicateOutputPath; } - + // resolve any /./ and /../ occurrences + // use resolvePosix since we asserted above all seps are '/' + if (Environment.isWindows and std.mem.indexOf(u8, rel_path, "/./") != null) { + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const rel_path_fixed = c.allocator.dupe(u8, bun.path.normalizeBuf(rel_path, &buf, .posix)) catch unreachable; + chunk.final_rel_path = rel_path_fixed; + continue; + } chunk.final_rel_path = rel_path; } } @@ -9377,7 +9389,7 @@ const LinkerContext = struct { defer max_heap_allocator.reset(); const rel_path = chunk.final_rel_path; - if (std.fs.path.dirname(rel_path)) |rel_parent| { + if (std.fs.path.dirnamePosix(rel_path)) |rel_parent| { if (rel_parent.len > 0) { root_dir.makePath(rel_parent) catch |err| { c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{s} creating outdir {} while saving chunk {}", .{ @@ -11082,7 +11094,7 @@ pub const Chunk = struct { shifts.appendAssumeCapacity(shift); var count: usize = 0; - var from_chunk_dir = std.fs.path.dirname(chunk.final_rel_path) orelse ""; + var from_chunk_dir = std.fs.path.dirnamePosix(chunk.final_rel_path) orelse ""; if (strings.eqlComptime(from_chunk_dir, ".")) from_chunk_dir = ""; @@ -11103,7 +11115,7 @@ pub const Chunk = struct { if (from_chunk_dir.len == 0) file_path else - bun.path.relative(from_chunk_dir, file_path), + bun.path.relativePlatform(from_chunk_dir, file_path, .posix, false), ); count += cheap_normalizer[0].len + cheap_normalizer[1].len; }, @@ -11160,7 +11172,7 @@ pub const Chunk = struct { if (from_chunk_dir.len == 0) file_path else - bun.path.relative(from_chunk_dir, file_path), + bun.path.relativePlatform(from_chunk_dir, file_path, .posix, false), ); if (cheap_normalizer[0].len > 0) { @@ -11250,7 +11262,7 @@ pub const Chunk = struct { var count: usize = 0; const file_path_buf: [4096]u8 = undefined; _ = file_path_buf; - var from_chunk_dir = std.fs.path.dirname(chunk.final_rel_path) orelse ""; + var from_chunk_dir = std.fs.path.dirnamePosix(chunk.final_rel_path) orelse ""; if (strings.eqlComptime(from_chunk_dir, ".")) from_chunk_dir = ""; @@ -11274,7 +11286,8 @@ pub const Chunk = struct { .chunk => chunks[index].final_rel_path, else => unreachable, }; - + // normalize windows paths to '/' + bun.path.platformToPosixInPlace(u8, @constCast(file_path)); const cheap_normalizer = cheapPrefixNormalizer( import_prefix, if (from_chunk_dir.len == 0) @@ -11315,12 +11328,14 @@ pub const Chunk = struct { .chunk => chunks[index].final_rel_path, else => unreachable, }; + // normalize windows paths to '/' + bun.path.platformToPosixInPlace(u8, @constCast(file_path)); const cheap_normalizer = cheapPrefixNormalizer( import_prefix, if (from_chunk_dir.len == 0) file_path else - bun.path.relative(from_chunk_dir, file_path), + bun.path.relativePlatform(from_chunk_dir, file_path, .posix, false), ); if (cheap_normalizer[0].len > 0) { diff --git a/src/fs.zig b/src/fs.zig index 973e54ce6a..f0ecdb18eb 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -18,7 +18,7 @@ const Semaphore = sync.Semaphore; const Fs = @This(); const path_handler = @import("./resolver/resolve_path.zig"); const PathString = bun.PathString; -const allocators = @import("./allocators.zig"); +const allocators = bun.allocators; const MAX_PATH_BYTES = bun.MAX_PATH_BYTES; const PathBuffer = bun.PathBuffer; @@ -1643,7 +1643,7 @@ pub const Path = struct { // This duplicates but only when strictly necessary // This will skip allocating if it's already in FilenameStore or DirnameStore pub fn dupeAlloc(this: *const Path, allocator: std.mem.Allocator) !Fs.Path { - if (this.text.ptr == this.pretty.ptr and this.text.len == this.text.len) { + if (this.text.ptr == this.pretty.ptr and this.text.len == this.pretty.len) { if (FileSystem.FilenameStore.instance.exists(this.text) or FileSystem.DirnameStore.instance.exists(this.text)) { return this.*; } @@ -1663,12 +1663,12 @@ pub const Path = struct { new_path.namespace = this.namespace; new_path.is_symlink = this.is_symlink; return new_path; - } else if (allocators.sliceRange(this.pretty, this.text)) |start_end| { + } else if (allocators.sliceRange(this.pretty, this.text)) |start_len| { if (FileSystem.FilenameStore.instance.exists(this.text) or FileSystem.DirnameStore.instance.exists(this.text)) { return this.*; } var new_path = Fs.Path.init(try FileSystem.FilenameStore.instance.append([]const u8, this.text)); - new_path.pretty = this.text[start_end[0]..start_end[1]]; + new_path.pretty = this.text[start_len[0]..][0..start_len[1]]; new_path.namespace = this.namespace; new_path.is_symlink = this.is_symlink; return new_path; diff --git a/src/http/url_path.zig b/src/http/url_path.zig index e511acf3ba..d1e50072b3 100644 --- a/src/http/url_path.zig +++ b/src/http/url_path.zig @@ -71,12 +71,7 @@ pub fn parse(possibly_encoded_pathname_: string) !URLPath { bun.copy(u8, possibly_encoded_pathname, possibly_encoded_pathname_[0..possibly_encoded_pathname.len]); const clone = possibly_encoded_pathname[0..possibly_encoded_pathname.len]; - var fbs = std.io.fixedBufferStream( - // This is safe because: - // - this comes from a non-const buffer - // - percent *decoding* will always be <= length of the original string (no buffer overflow) - @constCast(possibly_encoded_pathname), - ); + var fbs = std.io.fixedBufferStream(possibly_encoded_pathname); const writer = fbs.writer(); decoded_pathname = possibly_encoded_pathname[0..try PercentEncoding.decodeFaultTolerant(@TypeOf(writer), writer, clone, &needs_redirect, true)]; diff --git a/src/options.zig b/src/options.zig index dc3a9b802c..99d727c5fc 100644 --- a/src/options.zig +++ b/src/options.zig @@ -2589,6 +2589,7 @@ pub const PathTemplate = struct { pub fn format(self: PathTemplate, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { var remain = self.data; + bun.path.posixToPlatformInPlace(u8, @constCast(remain)); while (strings.indexOfChar(remain, '[')) |j| { try writer.writeAll(remain[0..j]); remain = remain[j + 1 ..]; diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index d644b3ad9e..625cf0700f 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -1948,3 +1948,19 @@ export fn ResolvePath__joinAbsStringBufCurrentPlatformBunString( return bun.String.createUTF8(out_slice); } + +pub fn platformToPosixInPlace(comptime T: type, path_buffer: []T) void { + if (std.fs.path.sep == '/') return; + var idx: usize = 0; + while (std.mem.indexOfScalarPos(T, path_buffer, idx, std.fs.path.sep)) |index| : (idx = index) { + path_buffer[index] = '/'; + } +} + +pub fn posixToPlatformInPlace(comptime T: type, path_buffer: []T) void { + if (std.fs.path.sep == '/') return; + var idx: usize = 0; + while (std.mem.indexOfScalarPos(T, path_buffer, idx, '/')) |index| : (idx = index) { + path_buffer[index] = std.fs.path.sep; + } +} diff --git a/src/string_immutable.zig b/src/string_immutable.zig index c73bcc83b8..abf716e0f7 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -1726,7 +1726,7 @@ pub const toNTDir = toNTPath; pub fn toExtendedPathNormalized(wbuf: []u16, utf8: []const u8) [:0]const u16 { std.debug.assert(wbuf.len > 4); - wbuf[0..4].* = [_]u16{ '\\', '\\', '?', '\\' }; + wbuf[0..4].* = bun.windows.nt_maxpath_prefix; return wbuf[0 .. toWPathNormalized(wbuf[4..], utf8).len + 4 :0]; } diff --git a/src/windows.zig b/src/windows.zig index ab4da2601b..be35bc0eb6 100644 --- a/src/windows.zig +++ b/src/windows.zig @@ -64,6 +64,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_maxpath_prefix = [4]u16{ '\\', '\\', '?', '\\' }; const std = @import("std"); pub const HANDLE = win32.HANDLE; diff --git a/test/bundler/bundler_naming.test.ts b/test/bundler/bundler_naming.test.ts index 5bdc1fbce4..2004056d87 100644 --- a/test/bundler/bundler_naming.test.ts +++ b/test/bundler/bundler_naming.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: panic "TODO on Windows" import assert from "assert"; import dedent from "dedent"; import { ESBUILD, itBundled, testForFile } from "./expectBundled"; @@ -270,4 +269,37 @@ describe("bundler", () => { file: "/out/_.._/hello/file.js", }, }); + itBundled("naming/WithPathTraversal", { + files: { + "/a/hello/entry.js": /* js */ ` + import data from '../dependency' + console.log(data); + `, + "/a/dependency.js": /* js */ ` + export default 1; + `, + "/a/hello/world/entry.js": /* js */ ` + console.log(2); + `, + "/a/hello/world/a/a/a/a/a/a/a/entry.js": /* js */ ` + console.log(3); + `, + }, + entryNaming: "foo/../bar/[dir]/file.[ext]", + entryPointsRaw: ["./a/hello/entry.js", "./a/hello/world/entry.js", "./a/hello/world/a/a/a/a/a/a/a/entry.js"], + run: [ + { + file: "/out/bar/file.js", + stdout: "1", + }, + { + file: "/out/bar/world/file.js", + stdout: "2", + }, + { + file: "/out/bar/world/a/a/a/a/a/a/a/file.js", + stdout: "3", + }, + ], + }); }); diff --git a/test/bundler/esbuild/splitting.test.ts b/test/bundler/esbuild/splitting.test.ts index 0a7907ae5d..cb74494cbc 100644 --- a/test/bundler/esbuild/splitting.test.ts +++ b/test/bundler/esbuild/splitting.test.ts @@ -1,8 +1,10 @@ -// @known-failing-on-windows: panic "TODO on Windows" import assert from "assert"; import { readdirSync } from "fs"; import { itBundled, testForFile } from "../expectBundled"; var { describe, test, expect } = testForFile(import.meta.path); +import process from "node:process"; + +const isWindows = process.platform === "win32"; // Tests ported from: // https://github.com/evanw/esbuild/blob/main/internal/bundler_tests/bundler_splitting_test.go @@ -276,7 +278,9 @@ describe("bundler", () => { { file: "/out/b.js", stdout: "[null]" }, ], bundleWarnings: { - "/common.js": [`Import "missing" will always be undefined because there is no matching export in "empty.js"`], + [isWindows ? "\\common.js" : "/common.js"]: [ + `Import "missing" will always be undefined because there is no matching export in "empty.js"`, + ], }, }); itBundled("splitting/ReExportESBuildIssue273", { diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index 32b60bedb9..83bcc0e2a9 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -445,7 +445,7 @@ function expectBundled( backend = plugins !== undefined ? "api" : "cli"; } - const root = path.join(outBase, id.replaceAll("/", path.sep)); + const root = path.join(outBase, id); if (DEBUG) console.log("root:", root); const entryPaths = entryPoints.map(file => path.join(root, file)); @@ -1176,7 +1176,7 @@ for (const [key, blob] of build.outputs) { // check reference if (matchesReference) { const { ref } = matchesReference; - const theirRoot = path.join(outBase, ref.id.replaceAll("/", path.sep)); + const theirRoot = path.join(outBase, ref.id); if (!existsSync(theirRoot)) { expectBundled(ref.id, ref.options, false, true); if (!existsSync(theirRoot)) { @@ -1287,7 +1287,7 @@ for (const [key, blob] of build.outputs) { if (typeof run.stdout === "string") { const expected = dedent(run.stdout).trim(); if (expected !== result) { - console.log(`runtime failed file=${file}`); + console.log(`runtime failed file: ${file}`); console.log(`reference stdout:`); console.log(result); console.log(`---`); @@ -1295,7 +1295,7 @@ for (const [key, blob] of build.outputs) { expect(result).toBe(expected); } else { if (!run.stdout.test(result)) { - console.log(`runtime failed file=${file}`); + console.log(`runtime failed file: ${file}`); console.log(`reference stdout:`); console.log(result); console.log(`---`); @@ -1330,7 +1330,7 @@ export function itBundled( ): BundlerTestRef { if (typeof opts === "function") { const fn = opts; - opts = opts({ root: path.join(outBase, id.replaceAll("/", path.sep)), getConfigRef }); + opts = opts({ root: path.join(outBase, id), getConfigRef }); // @ts-expect-error opts._referenceFn = fn; }