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.
This commit is contained in:
Ryan Liptak 2026-03-06 01:54:12 -08:00
parent 46658257f4
commit 25ef201ace
5 changed files with 47 additions and 3 deletions

View file

@ -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);
}
{

View file

@ -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()));
}

View file

@ -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);

View file

@ -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,
});

View file

@ -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);
}