From 25ef201acea78ddc7ef0c53483a3bfce507daaea Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Fri, 6 Mar 2026 01:54:12 -0800 Subject: [PATCH] Use / as path separator when writing tar files The tar format expects `/`, although some untar implementations do seem to handle Windows-style `\` path separators (7-Zip at least). The tar.Writer API can't really enforce this, though, as doing so would effectively make `\` an illegal character when it's really not. So, it's up to the user to provide paths with the correct path separators. `Build/WebServer.zig` will still output tars with `\` as a path separator on Windows, but that's currently only used during fuzzing which is not yet implemented on Windows. --- lib/compiler/std-docs.zig | 15 ++++++++++++++- lib/std/Build/WebServer.zig | 4 ++++ lib/std/tar/Writer.zig | 13 ++++++++++++- src/Compilation.zig | 14 +++++++++++++- src/Package/Fetch.zig | 4 ++++ 5 files changed, 47 insertions(+), 3 deletions(-) diff --git a/lib/compiler/std-docs.zig b/lib/compiler/std-docs.zig index c9ce170051..5660e6e56d 100644 --- a/lib/compiler/std-docs.zig +++ b/lib/compiler/std-docs.zig @@ -208,6 +208,9 @@ fn serveSourcesTar(request: *std.http.Server.Request, context: *Context) !void { var archiver: std.tar.Writer = .{ .underlying_writer = &response.writer }; archiver.prefix = "std"; + var path_buf: std.ArrayList(u8) = .empty; + defer path_buf.deinit(gpa); + while (try walker.next(io)) |entry| { switch (entry.kind) { .file => { @@ -227,7 +230,17 @@ fn serveSourcesTar(request: *std.http.Server.Request, context: *Context) !void { .interface = Io.File.Reader.initInterface(&.{}), .size = stat.size, }; - try archiver.writeFileTimestamp(entry.path, &file_reader, stat.mtime); + + const posix_path = if (comptime std.fs.path.sep == std.fs.path.sep_posix) + entry.path + else blk: { + path_buf.clearRetainingCapacity(); + try path_buf.appendSlice(gpa, entry.path); + std.mem.replaceScalar(u8, path_buf.items, std.fs.path.sep, std.fs.path.sep_posix); + break :blk path_buf.items; + }; + + try archiver.writeFileTimestamp(posix_path, &file_reader, stat.mtime); } { diff --git a/lib/std/Build/WebServer.zig b/lib/std/Build/WebServer.zig index 9775916903..a54479f667 100644 --- a/lib/std/Build/WebServer.zig +++ b/lib/std/Build/WebServer.zig @@ -526,6 +526,10 @@ pub fn serveTarFile(ws: *WebServer, request: *http.Server.Request, paths: []cons // resulting in modules named "" and "src". The compiler needs to tell the build system // about the module graph so that the build system can correctly encode this information in // the tar file. + // + // Additionally, this needs to ensure that all path separators for both prefix and + // sub_path are using the POSIX-style `/` on platforms that don't use it as their native + // path separator. archiver.prefix = path.root_dir.path orelse graph.cache.cwd; try archiver.writeFile(path.sub_path, &file_reader, @intCast(stat.mtime.toSeconds())); } diff --git a/lib/std/tar/Writer.zig b/lib/std/tar/Writer.zig index aa4d9b1bbb..8bcd87161a 100644 --- a/lib/std/tar/Writer.zig +++ b/lib/std/tar/Writer.zig @@ -17,6 +17,7 @@ pub const Options = struct { }; underlying_writer: *Io.Writer, +/// Assumed to use `/` for any path separators. prefix: []const u8 = "", const Error = error{ @@ -26,6 +27,7 @@ const Error = error{ }; /// Sets prefix for all other write* method paths. +/// `root` is assumed to use `/` for any path separators. pub fn setRoot(w: *Writer, root: []const u8) Error!void { if (root.len > 0) try w.writeDir(root, .{}); @@ -41,6 +43,7 @@ pub const WriteFileError = Io.Writer.FileError || Error || Io.File.Reader.SizeEr pub fn writeFileTimestamp( w: *Writer, + /// Assumed to use `/` for any path separators. sub_path: []const u8, file_reader: *Io.File.Reader, mtime: Io.Timestamp, @@ -50,6 +53,7 @@ pub fn writeFileTimestamp( pub fn writeFile( w: *Writer, + /// Assumed to use `/` for any path separators. sub_path: []const u8, file_reader: *Io.File.Reader, /// If you want to match the file format's expectations, it wants number of @@ -76,6 +80,7 @@ pub const WriteFileStreamError = Error || Io.Reader.StreamError; /// from `reader`, or returns `error.EndOfStream`. pub fn writeFileStream( w: *Writer, + /// Assumed to use `/` for any path separators. sub_path: []const u8, size: u64, reader: *Io.Reader, @@ -87,7 +92,13 @@ pub fn writeFileStream( } /// Writes file using bytes buffer `content` for size and file content. -pub fn writeFileBytes(w: *Writer, sub_path: []const u8, content: []const u8, options: Options) Error!void { +pub fn writeFileBytes( + w: *Writer, + /// Assumed to use `/` for all path separators. + sub_path: []const u8, + content: []const u8, + options: Options, +) Error!void { try w.writeHeader(.regular, sub_path, "", content.len, options); try w.underlying_writer.writeAll(content); try w.writePadding(content.len); diff --git a/src/Compilation.zig b/src/Compilation.zig index 6a4fab500b..521de0b427 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -5440,6 +5440,9 @@ fn docsCopyModule( var archiver: std.tar.Writer = .{ .underlying_writer = &tar_file_writer.interface }; archiver.prefix = name; + var path_buf: std.ArrayList(u8) = .empty; + defer path_buf.deinit(comp.gpa); + var buffer: [1024]u8 = undefined; while (try walker.next(io)) |entry| { @@ -5460,7 +5463,16 @@ fn docsCopyModule( const stat = try file.stat(io); var file_reader: Io.File.Reader = .initSize(file, io, &buffer, stat.size); - archiver.writeFileTimestamp(entry.path, &file_reader, stat.mtime) catch |err| { + const posix_path = if (comptime std.fs.path.sep == std.fs.path.sep_posix) + entry.path + else blk: { + path_buf.clearRetainingCapacity(); + try path_buf.appendSlice(comp.gpa, entry.path); + std.mem.replaceScalar(u8, path_buf.items, std.fs.path.sep, std.fs.path.sep_posix); + break :blk path_buf.items; + }; + + archiver.writeFileTimestamp(posix_path, &file_reader, stat.mtime) catch |err| { return comp.lockAndSetMiscFailure(.docs_copy, "unable to archive {f}{s}: {t}", .{ root.fmt(comp), entry.path, err, }); diff --git a/src/Package/Fetch.zig b/src/Package/Fetch.zig index 7226a49721..fe57f487b8 100644 --- a/src/Package/Fetch.zig +++ b/src/Package/Fetch.zig @@ -402,6 +402,10 @@ pub const JobQueue = struct { }, } const entry_path = try arena.dupe(u8, entry.path); + // If necessary, normalize path separators to POSIX-style since the tar format requires that. + if (comptime std.fs.path.sep != std.fs.path.sep_posix) { + std.mem.replaceScalar(u8, entry_path, std.fs.path.sep, std.fs.path.sep_posix); + } try scanned_files.append(gpa, entry_path); }