aboutsummaryrefslogtreecommitdiff
path: root/lib/std/tar
diff options
context:
space:
mode:
authorRue <78876133+IOKG04@users.noreply.github.com>2025-07-28 14:54:52 +0200
committerGitHub <noreply@github.com>2025-07-28 14:54:52 +0200
commit5381e7891dcdd7b6a9e74250cdcce221fe464cdc (patch)
tree4c74744ed84120dccae6dc9811ce945911108a17 /lib/std/tar
parent84ae54fbe64a15301317716e7f901d81585332d5 (diff)
parentdea3ed7f59347e87a1b8fa237202873988084ae8 (diff)
downloadzig-5381e7891dcdd7b6a9e74250cdcce221fe464cdc.tar.gz
zig-5381e7891dcdd7b6a9e74250cdcce221fe464cdc.zip
Merge branch 'ziglang:master' into some-documentation-updates-0
Diffstat (limited to 'lib/std/tar')
-rw-r--r--lib/std/tar/Writer.zig462
-rw-r--r--lib/std/tar/test.zig350
-rw-r--r--lib/std/tar/writer.zig497
3 files changed, 635 insertions, 674 deletions
diff --git a/lib/std/tar/Writer.zig b/lib/std/tar/Writer.zig
new file mode 100644
index 0000000000..61ae00b24e
--- /dev/null
+++ b/lib/std/tar/Writer.zig
@@ -0,0 +1,462 @@
+const std = @import("std");
+const assert = std.debug.assert;
+const testing = std.testing;
+const Writer = @This();
+
+const block_size = @sizeOf(Header);
+
+/// Options for writing file/dir/link. If left empty 0o664 is used for
+/// file mode and current time for mtime.
+pub const Options = struct {
+ /// File system permission mode.
+ mode: u32 = 0,
+ /// File system modification time.
+ mtime: u64 = 0,
+};
+
+underlying_writer: *std.Io.Writer,
+prefix: []const u8 = "",
+mtime_now: u64 = 0,
+
+const Error = error{
+ WriteFailed,
+ OctalOverflow,
+ NameTooLong,
+};
+
+/// Sets prefix for all other write* method paths.
+pub fn setRoot(w: *Writer, root: []const u8) Error!void {
+ if (root.len > 0)
+ try w.writeDir(root, .{});
+
+ w.prefix = root;
+}
+
+pub fn writeDir(w: *Writer, sub_path: []const u8, options: Options) Error!void {
+ try w.writeHeader(.directory, sub_path, "", 0, options);
+}
+
+pub const WriteFileError = std.Io.Writer.FileError || Error || std.fs.File.GetEndPosError;
+
+pub fn writeFile(
+ w: *Writer,
+ sub_path: []const u8,
+ file_reader: *std.fs.File.Reader,
+ stat_mtime: i128,
+) WriteFileError!void {
+ const size = try file_reader.getSize();
+ const mtime: u64 = @intCast(@divFloor(stat_mtime, std.time.ns_per_s));
+
+ var header: Header = .{};
+ try w.setPath(&header, sub_path);
+ try header.setSize(size);
+ try header.setMtime(mtime);
+ try header.updateChecksum();
+
+ try w.underlying_writer.writeAll(@ptrCast((&header)[0..1]));
+ _ = try w.underlying_writer.sendFileAll(file_reader, .unlimited);
+ try w.writePadding64(size);
+}
+
+pub const WriteFileStreamError = Error || std.Io.Reader.StreamError;
+
+/// Writes file reading file content from `reader`. Reads exactly `size` bytes
+/// from `reader`, or returns `error.EndOfStream`.
+pub fn writeFileStream(
+ w: *Writer,
+ sub_path: []const u8,
+ size: u64,
+ reader: *std.Io.Reader,
+ options: Options,
+) WriteFileStreamError!void {
+ try w.writeHeader(.regular, sub_path, "", size, options);
+ try reader.streamExact64(w.underlying_writer, size);
+ try w.writePadding64(size);
+}
+
+/// 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 {
+ try w.writeHeader(.regular, sub_path, "", content.len, options);
+ try w.underlying_writer.writeAll(content);
+ try w.writePadding(content.len);
+}
+
+pub fn writeLink(w: *Writer, sub_path: []const u8, link_name: []const u8, options: Options) Error!void {
+ try w.writeHeader(.symbolic_link, sub_path, link_name, 0, options);
+}
+
+fn writeHeader(
+ w: *Writer,
+ typeflag: Header.FileType,
+ sub_path: []const u8,
+ link_name: []const u8,
+ size: u64,
+ options: Options,
+) Error!void {
+ var header = Header.init(typeflag);
+ try w.setPath(&header, sub_path);
+ try header.setSize(size);
+ try header.setMtime(options.mtime);
+ if (options.mode != 0)
+ try header.setMode(options.mode);
+ if (typeflag == .symbolic_link)
+ header.setLinkname(link_name) catch |err| switch (err) {
+ error.NameTooLong => try w.writeExtendedHeader(.gnu_long_link, &.{link_name}),
+ else => return err,
+ };
+ try header.write(w.underlying_writer);
+}
+
+/// Writes path in posix header, if don't fit (in name+prefix; 100+155
+/// bytes) writes it in gnu extended header.
+fn setPath(w: *Writer, header: *Header, sub_path: []const u8) Error!void {
+ header.setPath(w.prefix, sub_path) catch |err| switch (err) {
+ error.NameTooLong => {
+ // write extended header
+ const buffers: []const []const u8 = if (w.prefix.len == 0)
+ &.{sub_path}
+ else
+ &.{ w.prefix, "/", sub_path };
+ try w.writeExtendedHeader(.gnu_long_name, buffers);
+ },
+ else => return err,
+ };
+}
+
+/// Writes gnu extended header: gnu_long_name or gnu_long_link.
+fn writeExtendedHeader(w: *Writer, typeflag: Header.FileType, buffers: []const []const u8) Error!void {
+ var len: usize = 0;
+ for (buffers) |buf| len += buf.len;
+
+ var header: Header = .init(typeflag);
+ try header.setSize(len);
+ try header.write(w.underlying_writer);
+ for (buffers) |buf|
+ try w.underlying_writer.writeAll(buf);
+ try w.writePadding(len);
+}
+
+fn writePadding(w: *Writer, bytes: usize) std.Io.Writer.Error!void {
+ return writePaddingPos(w, bytes % block_size);
+}
+
+fn writePadding64(w: *Writer, bytes: u64) std.Io.Writer.Error!void {
+ return writePaddingPos(w, @intCast(bytes % block_size));
+}
+
+fn writePaddingPos(w: *Writer, pos: usize) std.Io.Writer.Error!void {
+ if (pos == 0) return;
+ try w.underlying_writer.splatByteAll(0, block_size - pos);
+}
+
+/// According to the specification, tar should finish with two zero blocks, but
+/// "reasonable system must not assume that such a block exists when reading an
+/// archive". Therefore, the Zig standard library recommends to not call this
+/// function.
+pub fn finishPedantically(w: *Writer) std.Io.Writer.Error!void {
+ try w.underlying_writer.splatByteAll(0, block_size * 2);
+}
+
+/// A struct that is exactly 512 bytes and matches tar file format. This is
+/// intended to be used for outputting tar files; for parsing there is
+/// `std.tar.Header`.
+pub const Header = extern struct {
+ // This struct was originally copied from
+ // https://github.com/mattnite/tar/blob/main/src/main.zig which is MIT
+ // licensed.
+ //
+ // The name, linkname, magic, uname, and gname are null-terminated character
+ // strings. All other fields are zero-filled octal numbers in ASCII. Each
+ // numeric field of width w contains w minus 1 digits, and a null.
+ // Reference: https://www.gnu.org/software/tar/manual/html_node/Standard.html
+ // POSIX header: byte offset
+ name: [100]u8 = [_]u8{0} ** 100, // 0
+ mode: [7:0]u8 = default_mode.file, // 100
+ uid: [7:0]u8 = [_:0]u8{0} ** 7, // unused 108
+ gid: [7:0]u8 = [_:0]u8{0} ** 7, // unused 116
+ size: [11:0]u8 = [_:0]u8{'0'} ** 11, // 124
+ mtime: [11:0]u8 = [_:0]u8{'0'} ** 11, // 136
+ checksum: [7:0]u8 = [_:0]u8{' '} ** 7, // 148
+ typeflag: FileType = .regular, // 156
+ linkname: [100]u8 = [_]u8{0} ** 100, // 157
+ magic: [6]u8 = [_]u8{ 'u', 's', 't', 'a', 'r', 0 }, // 257
+ version: [2]u8 = [_]u8{ '0', '0' }, // 263
+ uname: [32]u8 = [_]u8{0} ** 32, // unused 265
+ gname: [32]u8 = [_]u8{0} ** 32, // unused 297
+ devmajor: [7:0]u8 = [_:0]u8{0} ** 7, // unused 329
+ devminor: [7:0]u8 = [_:0]u8{0} ** 7, // unused 337
+ prefix: [155]u8 = [_]u8{0} ** 155, // 345
+ pad: [12]u8 = [_]u8{0} ** 12, // unused 500
+
+ pub const FileType = enum(u8) {
+ regular = '0',
+ symbolic_link = '2',
+ directory = '5',
+ gnu_long_name = 'L',
+ gnu_long_link = 'K',
+ };
+
+ const default_mode = struct {
+ const file = [_:0]u8{ '0', '0', '0', '0', '6', '6', '4' }; // 0o664
+ const dir = [_:0]u8{ '0', '0', '0', '0', '7', '7', '5' }; // 0o775
+ const sym_link = [_:0]u8{ '0', '0', '0', '0', '7', '7', '7' }; // 0o777
+ const other = [_:0]u8{ '0', '0', '0', '0', '0', '0', '0' }; // 0o000
+ };
+
+ pub fn init(typeflag: FileType) Header {
+ return .{
+ .typeflag = typeflag,
+ .mode = switch (typeflag) {
+ .directory => default_mode.dir,
+ .symbolic_link => default_mode.sym_link,
+ .regular => default_mode.file,
+ else => default_mode.other,
+ },
+ };
+ }
+
+ pub fn setSize(w: *Header, size: u64) error{OctalOverflow}!void {
+ try octal(&w.size, size);
+ }
+
+ fn octal(buf: []u8, value: u64) error{OctalOverflow}!void {
+ var remainder: u64 = value;
+ var pos: usize = buf.len;
+ while (remainder > 0 and pos > 0) {
+ pos -= 1;
+ const c: u8 = @as(u8, @intCast(remainder % 8)) + '0';
+ buf[pos] = c;
+ remainder /= 8;
+ if (pos == 0 and remainder > 0) return error.OctalOverflow;
+ }
+ }
+
+ pub fn setMode(w: *Header, mode: u32) error{OctalOverflow}!void {
+ try octal(&w.mode, mode);
+ }
+
+ // Integer number of seconds since January 1, 1970, 00:00 Coordinated Universal Time.
+ // mtime == 0 will use current time
+ pub fn setMtime(w: *Header, mtime: u64) error{OctalOverflow}!void {
+ try octal(&w.mtime, mtime);
+ }
+
+ pub fn updateChecksum(w: *Header) !void {
+ var checksum: usize = ' '; // other 7 w.checksum bytes are initialized to ' '
+ for (std.mem.asBytes(w)) |val|
+ checksum += val;
+ try octal(&w.checksum, checksum);
+ }
+
+ pub fn write(h: *Header, bw: *std.Io.Writer) error{ OctalOverflow, WriteFailed }!void {
+ try h.updateChecksum();
+ try bw.writeAll(std.mem.asBytes(h));
+ }
+
+ pub fn setLinkname(w: *Header, link: []const u8) !void {
+ if (link.len > w.linkname.len) return error.NameTooLong;
+ @memcpy(w.linkname[0..link.len], link);
+ }
+
+ pub fn setPath(w: *Header, prefix: []const u8, sub_path: []const u8) !void {
+ const max_prefix = w.prefix.len;
+ const max_name = w.name.len;
+ const sep = std.fs.path.sep_posix;
+
+ if (prefix.len + sub_path.len > max_name + max_prefix or prefix.len > max_prefix)
+ return error.NameTooLong;
+
+ // both fit into name
+ if (prefix.len > 0 and prefix.len + sub_path.len < max_name) {
+ @memcpy(w.name[0..prefix.len], prefix);
+ w.name[prefix.len] = sep;
+ @memcpy(w.name[prefix.len + 1 ..][0..sub_path.len], sub_path);
+ return;
+ }
+
+ // sub_path fits into name
+ // there is no prefix or prefix fits into prefix
+ if (sub_path.len <= max_name) {
+ @memcpy(w.name[0..sub_path.len], sub_path);
+ @memcpy(w.prefix[0..prefix.len], prefix);
+ return;
+ }
+
+ if (prefix.len > 0) {
+ @memcpy(w.prefix[0..prefix.len], prefix);
+ w.prefix[prefix.len] = sep;
+ }
+ const prefix_pos = if (prefix.len > 0) prefix.len + 1 else 0;
+
+ // add as much to prefix as you can, must split at /
+ const prefix_remaining = max_prefix - prefix_pos;
+ if (std.mem.lastIndexOf(u8, sub_path[0..@min(prefix_remaining, sub_path.len)], &.{'/'})) |sep_pos| {
+ @memcpy(w.prefix[prefix_pos..][0..sep_pos], sub_path[0..sep_pos]);
+ if ((sub_path.len - sep_pos - 1) > max_name) return error.NameTooLong;
+ @memcpy(w.name[0..][0 .. sub_path.len - sep_pos - 1], sub_path[sep_pos + 1 ..]);
+ return;
+ }
+
+ return error.NameTooLong;
+ }
+
+ comptime {
+ assert(@sizeOf(Header) == 512);
+ }
+
+ test "setPath" {
+ const cases = [_]struct {
+ in: []const []const u8,
+ out: []const []const u8,
+ }{
+ .{
+ .in = &.{ "", "123456789" },
+ .out = &.{ "", "123456789" },
+ },
+ // can fit into name
+ .{
+ .in = &.{ "prefix", "sub_path" },
+ .out = &.{ "", "prefix/sub_path" },
+ },
+ // no more both fits into name
+ .{
+ .in = &.{ "prefix", "0123456789/" ** 8 ++ "basename" },
+ .out = &.{ "prefix", "0123456789/" ** 8 ++ "basename" },
+ },
+ // put as much as you can into prefix the rest goes into name
+ .{
+ .in = &.{ "prefix", "0123456789/" ** 10 ++ "basename" },
+ .out = &.{ "prefix/" ++ "0123456789/" ** 9 ++ "0123456789", "basename" },
+ },
+
+ .{
+ .in = &.{ "prefix", "0123456789/" ** 15 ++ "basename" },
+ .out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/0123456789/basename" },
+ },
+ .{
+ .in = &.{ "prefix", "0123456789/" ** 21 ++ "basename" },
+ .out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/" ** 8 ++ "basename" },
+ },
+ .{
+ .in = &.{ "", "012345678/" ** 10 ++ "foo" },
+ .out = &.{ "012345678/" ** 9 ++ "012345678", "foo" },
+ },
+ };
+
+ for (cases) |case| {
+ var header = Header.init(.regular);
+ try header.setPath(case.in[0], case.in[1]);
+ try testing.expectEqualStrings(case.out[0], std.mem.sliceTo(&header.prefix, 0));
+ try testing.expectEqualStrings(case.out[1], std.mem.sliceTo(&header.name, 0));
+ }
+
+ const error_cases = [_]struct {
+ in: []const []const u8,
+ }{
+ // basename can't fit into name (106 characters)
+ .{ .in = &.{ "zig", "test/cases/compile_errors/regression_test_2980_base_type_u32_is_not_type_checked_properly_when_assigning_a_value_within_a_struct.zig" } },
+ // cant fit into 255 + sep
+ .{ .in = &.{ "prefix", "0123456789/" ** 22 ++ "basename" } },
+ // can fit but sub_path can't be split (there is no separator)
+ .{ .in = &.{ "prefix", "0123456789" ** 10 ++ "a" } },
+ .{ .in = &.{ "prefix", "0123456789" ** 14 ++ "basename" } },
+ };
+
+ for (error_cases) |case| {
+ var header = Header.init(.regular);
+ try testing.expectError(
+ error.NameTooLong,
+ header.setPath(case.in[0], case.in[1]),
+ );
+ }
+ }
+};
+
+test {
+ _ = Header;
+}
+
+test "write files" {
+ const files = [_]struct {
+ path: []const u8,
+ content: []const u8,
+ }{
+ .{ .path = "foo", .content = "bar" },
+ .{ .path = "a12345678/" ** 10 ++ "foo", .content = "a" ** 511 },
+ .{ .path = "b12345678/" ** 24 ++ "foo", .content = "b" ** 512 },
+ .{ .path = "c12345678/" ** 25 ++ "foo", .content = "c" ** 513 },
+ .{ .path = "d12345678/" ** 51 ++ "foo", .content = "d" ** 1025 },
+ .{ .path = "e123456789" ** 11, .content = "e" },
+ };
+
+ var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
+ var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
+
+ // with root
+ {
+ const root = "root";
+
+ var output: std.Io.Writer.Allocating = .init(testing.allocator);
+ var w: Writer = .{ .underlying_writer = &output.writer };
+ defer output.deinit();
+ try w.setRoot(root);
+ for (files) |file|
+ try w.writeFileBytes(file.path, file.content, .{});
+
+ var input: std.Io.Reader = .fixed(output.getWritten());
+ var it: std.tar.Iterator = .init(&input, .{
+ .file_name_buffer = &file_name_buffer,
+ .link_name_buffer = &link_name_buffer,
+ });
+
+ // first entry is directory with prefix
+ {
+ const actual = (try it.next()).?;
+ try testing.expectEqualStrings(root, actual.name);
+ try testing.expectEqual(std.tar.FileKind.directory, actual.kind);
+ }
+
+ var i: usize = 0;
+ while (try it.next()) |actual| {
+ defer i += 1;
+ const expected = files[i];
+ try testing.expectEqualStrings(root, actual.name[0..root.len]);
+ try testing.expectEqual('/', actual.name[root.len..][0]);
+ try testing.expectEqualStrings(expected.path, actual.name[root.len + 1 ..]);
+
+ var content: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer content.deinit();
+ try it.streamRemaining(actual, &content.writer);
+ try testing.expectEqualSlices(u8, expected.content, content.getWritten());
+ }
+ }
+ // without root
+ {
+ var output: std.Io.Writer.Allocating = .init(testing.allocator);
+ var w: Writer = .{ .underlying_writer = &output.writer };
+ defer output.deinit();
+ for (files) |file| {
+ var content: std.Io.Reader = .fixed(file.content);
+ try w.writeFileStream(file.path, file.content.len, &content, .{});
+ }
+
+ var input: std.Io.Reader = .fixed(output.getWritten());
+ var it: std.tar.Iterator = .init(&input, .{
+ .file_name_buffer = &file_name_buffer,
+ .link_name_buffer = &link_name_buffer,
+ });
+
+ var i: usize = 0;
+ while (try it.next()) |actual| {
+ defer i += 1;
+ const expected = files[i];
+ try testing.expectEqualStrings(expected.path, actual.name);
+
+ var content: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer content.deinit();
+ try it.streamRemaining(actual, &content.writer);
+ try testing.expectEqualSlices(u8, expected.content, content.getWritten());
+ }
+ try w.finishPedantically();
+ }
+}
diff --git a/lib/std/tar/test.zig b/lib/std/tar/test.zig
index 3bcb5af90c..3356baacb5 100644
--- a/lib/std/tar/test.zig
+++ b/lib/std/tar/test.zig
@@ -18,31 +18,72 @@ const Case = struct {
err: ?anyerror = null, // parsing should fail with this error
};
-const cases = [_]Case{
- .{
- .data = @embedFile("testdata/gnu.tar"),
- .files = &[_]Case.File{
- .{
- .name = "small.txt",
- .size = 5,
- .mode = 0o640,
- },
- .{
- .name = "small2.txt",
- .size = 11,
- .mode = 0o640,
- },
+const gnu_case: Case = .{
+ .data = @embedFile("testdata/gnu.tar"),
+ .files = &[_]Case.File{
+ .{
+ .name = "small.txt",
+ .size = 5,
+ .mode = 0o640,
},
- .chksums = &[_][]const u8{
- "e38b27eaccb4391bdec553a7f3ae6b2f",
- "c65bd2e50a56a2138bf1716f2fd56fe9",
+ .{
+ .name = "small2.txt",
+ .size = 11,
+ .mode = 0o640,
+ },
+ },
+ .chksums = &[_][]const u8{
+ "e38b27eaccb4391bdec553a7f3ae6b2f",
+ "c65bd2e50a56a2138bf1716f2fd56fe9",
+ },
+};
+
+const gnu_multi_headers_case: Case = .{
+ .data = @embedFile("testdata/gnu-multi-hdrs.tar"),
+ .files = &[_]Case.File{
+ .{
+ .name = "GNU2/GNU2/long-path-name",
+ .link_name = "GNU4/GNU4/long-linkpath-name",
+ .kind = .sym_link,
},
},
- .{
+};
+
+const trailing_slash_case: Case = .{
+ .data = @embedFile("testdata/trailing-slash.tar"),
+ .files = &[_]Case.File{
+ .{
+ .name = "123456789/" ** 30,
+ .kind = .directory,
+ },
+ },
+};
+
+const writer_big_long_case: Case = .{
+ // Size in gnu extended format, and name in pax attribute.
+ .data = @embedFile("testdata/writer-big-long.tar"),
+ .files = &[_]Case.File{
+ .{
+ .name = "longname/" ** 15 ++ "16gig.txt",
+ .size = 16 * 1024 * 1024 * 1024,
+ .mode = 0o644,
+ .truncated = true,
+ },
+ },
+};
+
+const fuzz1_case: Case = .{
+ .data = @embedFile("testdata/fuzz1.tar"),
+ .err = error.TarInsufficientBuffer,
+};
+
+test "run test cases" {
+ try testCase(gnu_case);
+ try testCase(.{
.data = @embedFile("testdata/sparse-formats.tar"),
.err = error.TarUnsupportedHeader,
- },
- .{
+ });
+ try testCase(.{
.data = @embedFile("testdata/star.tar"),
.files = &[_]Case.File{
.{
@@ -60,8 +101,8 @@ const cases = [_]Case{
"e38b27eaccb4391bdec553a7f3ae6b2f",
"c65bd2e50a56a2138bf1716f2fd56fe9",
},
- },
- .{
+ });
+ try testCase(.{
.data = @embedFile("testdata/v7.tar"),
.files = &[_]Case.File{
.{
@@ -79,8 +120,8 @@ const cases = [_]Case{
"e38b27eaccb4391bdec553a7f3ae6b2f",
"c65bd2e50a56a2138bf1716f2fd56fe9",
},
- },
- .{
+ });
+ try testCase(.{
.data = @embedFile("testdata/pax.tar"),
.files = &[_]Case.File{
.{
@@ -99,13 +140,13 @@ const cases = [_]Case{
.chksums = &[_][]const u8{
"3c382e8f5b6631aa2db52643912ffd4a",
},
- },
- .{
+ });
+ try testCase(.{
// pax attribute don't end with \n
.data = @embedFile("testdata/pax-bad-hdr-file.tar"),
.err = error.PaxInvalidAttributeEnd,
- },
- .{
+ });
+ try testCase(.{
// size is in pax attribute
.data = @embedFile("testdata/pax-pos-size-file.tar"),
.files = &[_]Case.File{
@@ -119,8 +160,8 @@ const cases = [_]Case{
.chksums = &[_][]const u8{
"0afb597b283fe61b5d4879669a350556",
},
- },
- .{
+ });
+ try testCase(.{
// has pax records which we are not interested in
.data = @embedFile("testdata/pax-records.tar"),
.files = &[_]Case.File{
@@ -128,8 +169,8 @@ const cases = [_]Case{
.name = "file",
},
},
- },
- .{
+ });
+ try testCase(.{
// has global records which we are ignoring
.data = @embedFile("testdata/pax-global-records.tar"),
.files = &[_]Case.File{
@@ -146,8 +187,8 @@ const cases = [_]Case{
.name = "file4",
},
},
- },
- .{
+ });
+ try testCase(.{
.data = @embedFile("testdata/nil-uid.tar"),
.files = &[_]Case.File{
.{
@@ -160,8 +201,8 @@ const cases = [_]Case{
.chksums = &[_][]const u8{
"08d504674115e77a67244beac19668f5",
},
- },
- .{
+ });
+ try testCase(.{
// has xattrs and pax records which we are ignoring
.data = @embedFile("testdata/xattrs.tar"),
.files = &[_]Case.File{
@@ -182,23 +223,14 @@ const cases = [_]Case{
"e38b27eaccb4391bdec553a7f3ae6b2f",
"c65bd2e50a56a2138bf1716f2fd56fe9",
},
- },
- .{
- .data = @embedFile("testdata/gnu-multi-hdrs.tar"),
- .files = &[_]Case.File{
- .{
- .name = "GNU2/GNU2/long-path-name",
- .link_name = "GNU4/GNU4/long-linkpath-name",
- .kind = .sym_link,
- },
- },
- },
- .{
+ });
+ try testCase(gnu_multi_headers_case);
+ try testCase(.{
// has gnu type D (directory) and S (sparse) blocks
.data = @embedFile("testdata/gnu-incremental.tar"),
.err = error.TarUnsupportedHeader,
- },
- .{
+ });
+ try testCase(.{
// should use values only from last pax header
.data = @embedFile("testdata/pax-multi-hdrs.tar"),
.files = &[_]Case.File{
@@ -208,8 +240,8 @@ const cases = [_]Case{
.kind = .sym_link,
},
},
- },
- .{
+ });
+ try testCase(.{
.data = @embedFile("testdata/gnu-long-nul.tar"),
.files = &[_]Case.File{
.{
@@ -217,8 +249,8 @@ const cases = [_]Case{
.mode = 0o644,
},
},
- },
- .{
+ });
+ try testCase(.{
.data = @embedFile("testdata/gnu-utf8.tar"),
.files = &[_]Case.File{
.{
@@ -226,8 +258,8 @@ const cases = [_]Case{
.mode = 0o644,
},
},
- },
- .{
+ });
+ try testCase(.{
.data = @embedFile("testdata/gnu-not-utf8.tar"),
.files = &[_]Case.File{
.{
@@ -235,33 +267,33 @@ const cases = [_]Case{
.mode = 0o644,
},
},
- },
- .{
+ });
+ try testCase(.{
// null in pax key
.data = @embedFile("testdata/pax-nul-xattrs.tar"),
.err = error.PaxNullInKeyword,
- },
- .{
+ });
+ try testCase(.{
.data = @embedFile("testdata/pax-nul-path.tar"),
.err = error.PaxNullInValue,
- },
- .{
+ });
+ try testCase(.{
.data = @embedFile("testdata/neg-size.tar"),
.err = error.TarHeader,
- },
- .{
+ });
+ try testCase(.{
.data = @embedFile("testdata/issue10968.tar"),
.err = error.TarHeader,
- },
- .{
+ });
+ try testCase(.{
.data = @embedFile("testdata/issue11169.tar"),
.err = error.TarHeader,
- },
- .{
+ });
+ try testCase(.{
.data = @embedFile("testdata/issue12435.tar"),
.err = error.TarHeaderChksum,
- },
- .{
+ });
+ try testCase(.{
// has magic with space at end instead of null
.data = @embedFile("testdata/invalid-go17.tar"),
.files = &[_]Case.File{
@@ -269,8 +301,8 @@ const cases = [_]Case{
.name = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/foo",
},
},
- },
- .{
+ });
+ try testCase(.{
.data = @embedFile("testdata/ustar-file-devs.tar"),
.files = &[_]Case.File{
.{
@@ -278,17 +310,9 @@ const cases = [_]Case{
.mode = 0o644,
},
},
- },
- .{
- .data = @embedFile("testdata/trailing-slash.tar"),
- .files = &[_]Case.File{
- .{
- .name = "123456789/" ** 30,
- .kind = .directory,
- },
- },
- },
- .{
+ });
+ try testCase(trailing_slash_case);
+ try testCase(.{
// Has size in gnu extended format. To represent size bigger than 8 GB.
.data = @embedFile("testdata/writer-big.tar"),
.files = &[_]Case.File{
@@ -299,120 +323,92 @@ const cases = [_]Case{
.mode = 0o640,
},
},
- },
- .{
- // Size in gnu extended format, and name in pax attribute.
- .data = @embedFile("testdata/writer-big-long.tar"),
- .files = &[_]Case.File{
- .{
- .name = "longname/" ** 15 ++ "16gig.txt",
- .size = 16 * 1024 * 1024 * 1024,
- .mode = 0o644,
- .truncated = true,
- },
- },
- },
- .{
- .data = @embedFile("testdata/fuzz1.tar"),
- .err = error.TarInsufficientBuffer,
- },
- .{
+ });
+ try testCase(writer_big_long_case);
+ try testCase(fuzz1_case);
+ try testCase(.{
.data = @embedFile("testdata/fuzz2.tar"),
.err = error.PaxSizeAttrOverflow,
- },
-};
-
-// used in test to calculate file chksum
-const Md5Writer = struct {
- h: std.crypto.hash.Md5 = std.crypto.hash.Md5.init(.{}),
-
- pub fn writeAll(self: *Md5Writer, buf: []const u8) !void {
- self.h.update(buf);
- }
-
- pub fn writeByte(self: *Md5Writer, byte: u8) !void {
- self.h.update(&[_]u8{byte});
- }
-
- pub fn chksum(self: *Md5Writer) [32]u8 {
- var s = [_]u8{0} ** 16;
- self.h.final(&s);
- return std.fmt.bytesToHex(s, .lower);
- }
-};
+ });
+}
-test "run test cases" {
+fn testCase(case: Case) !void {
var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
- for (cases) |case| {
- var fsb = std.io.fixedBufferStream(case.data);
- var iter = tar.iterator(fsb.reader(), .{
- .file_name_buffer = &file_name_buffer,
- .link_name_buffer = &link_name_buffer,
- });
- var i: usize = 0;
- while (iter.next() catch |err| {
- if (case.err) |e| {
- try testing.expectEqual(e, err);
- continue;
- } else {
- return err;
- }
- }) |actual| : (i += 1) {
- const expected = case.files[i];
- try testing.expectEqualStrings(expected.name, actual.name);
- try testing.expectEqual(expected.size, actual.size);
- try testing.expectEqual(expected.kind, actual.kind);
- try testing.expectEqual(expected.mode, actual.mode);
- try testing.expectEqualStrings(expected.link_name, actual.link_name);
+ var br: std.io.Reader = .fixed(case.data);
+ var it: tar.Iterator = .init(&br, .{
+ .file_name_buffer = &file_name_buffer,
+ .link_name_buffer = &link_name_buffer,
+ });
+ var i: usize = 0;
+ while (it.next() catch |err| {
+ if (case.err) |e| {
+ try testing.expectEqual(e, err);
+ return;
+ } else {
+ return err;
+ }
+ }) |actual| : (i += 1) {
+ const expected = case.files[i];
+ try testing.expectEqualStrings(expected.name, actual.name);
+ try testing.expectEqual(expected.size, actual.size);
+ try testing.expectEqual(expected.kind, actual.kind);
+ try testing.expectEqual(expected.mode, actual.mode);
+ try testing.expectEqualStrings(expected.link_name, actual.link_name);
- if (case.chksums.len > i) {
- var md5writer = Md5Writer{};
- try actual.writeAll(&md5writer);
- const chksum = md5writer.chksum();
- try testing.expectEqualStrings(case.chksums[i], &chksum);
- } else {
- if (expected.truncated) {
- iter.unread_file_bytes = 0;
- }
+ if (case.chksums.len > i) {
+ var aw: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer aw.deinit();
+ try it.streamRemaining(actual, &aw.writer);
+ const chksum = std.fmt.bytesToHex(std.crypto.hash.Md5.hashResult(aw.getWritten()), .lower);
+ try testing.expectEqualStrings(case.chksums[i], &chksum);
+ } else {
+ if (expected.truncated) {
+ it.unread_file_bytes = 0;
}
}
- try testing.expectEqual(case.files.len, i);
}
+ try testing.expectEqual(case.files.len, i);
}
test "pax/gnu long names with small buffer" {
+ try testLongNameCase(gnu_multi_headers_case);
+ try testLongNameCase(trailing_slash_case);
+ try testLongNameCase(.{
+ .data = @embedFile("testdata/fuzz1.tar"),
+ .err = error.TarInsufficientBuffer,
+ });
+}
+
+fn testLongNameCase(case: Case) !void {
// should fail with insufficient buffer error
var min_file_name_buffer: [256]u8 = undefined;
var min_link_name_buffer: [100]u8 = undefined;
- const long_name_cases = [_]Case{ cases[11], cases[25], cases[28] };
- for (long_name_cases) |case| {
- var fsb = std.io.fixedBufferStream(case.data);
- var iter = tar.iterator(fsb.reader(), .{
- .file_name_buffer = &min_file_name_buffer,
- .link_name_buffer = &min_link_name_buffer,
- });
+ var br: std.io.Reader = .fixed(case.data);
+ var iter: tar.Iterator = .init(&br, .{
+ .file_name_buffer = &min_file_name_buffer,
+ .link_name_buffer = &min_link_name_buffer,
+ });
- var iter_err: ?anyerror = null;
- while (iter.next() catch |err| brk: {
- iter_err = err;
- break :brk null;
- }) |_| {}
+ var iter_err: ?anyerror = null;
+ while (iter.next() catch |err| brk: {
+ iter_err = err;
+ break :brk null;
+ }) |_| {}
- try testing.expect(iter_err != null);
- try testing.expectEqual(error.TarInsufficientBuffer, iter_err.?);
- }
+ try testing.expect(iter_err != null);
+ try testing.expectEqual(error.TarInsufficientBuffer, iter_err.?);
}
test "insufficient buffer in Header name filed" {
var min_file_name_buffer: [9]u8 = undefined;
var min_link_name_buffer: [100]u8 = undefined;
- var fsb = std.io.fixedBufferStream(cases[0].data);
- var iter = tar.iterator(fsb.reader(), .{
+ var br: std.io.Reader = .fixed(gnu_case.data);
+ var iter: tar.Iterator = .init(&br, .{
.file_name_buffer = &min_file_name_buffer,
.link_name_buffer = &min_link_name_buffer,
});
@@ -466,21 +462,21 @@ test "should not overwrite existing file" {
// This ensures that file is not overwritten.
//
const data = @embedFile("testdata/overwrite_file.tar");
- var fsb = std.io.fixedBufferStream(data);
+ var r: std.io.Reader = .fixed(data);
// Unpack with strip_components = 1 should fail
var root = std.testing.tmpDir(.{});
defer root.cleanup();
try testing.expectError(
error.PathAlreadyExists,
- tar.pipeToFileSystem(root.dir, fsb.reader(), .{ .mode_mode = .ignore, .strip_components = 1 }),
+ tar.pipeToFileSystem(root.dir, &r, .{ .mode_mode = .ignore, .strip_components = 1 }),
);
// Unpack with strip_components = 0 should pass
- fsb.reset();
+ r = .fixed(data);
var root2 = std.testing.tmpDir(.{});
defer root2.cleanup();
- try tar.pipeToFileSystem(root2.dir, fsb.reader(), .{ .mode_mode = .ignore, .strip_components = 0 });
+ try tar.pipeToFileSystem(root2.dir, &r, .{ .mode_mode = .ignore, .strip_components = 0 });
}
test "case sensitivity" {
@@ -494,12 +490,12 @@ test "case sensitivity" {
// 18089/alacritty/Darkermatrix.yml
//
const data = @embedFile("testdata/18089.tar");
- var fsb = std.io.fixedBufferStream(data);
+ var r: std.io.Reader = .fixed(data);
var root = std.testing.tmpDir(.{});
defer root.cleanup();
- tar.pipeToFileSystem(root.dir, fsb.reader(), .{ .mode_mode = .ignore, .strip_components = 1 }) catch |err| {
+ tar.pipeToFileSystem(root.dir, &r, .{ .mode_mode = .ignore, .strip_components = 1 }) catch |err| {
// on case insensitive fs we fail on overwrite existing file
try testing.expectEqual(error.PathAlreadyExists, err);
return;
diff --git a/lib/std/tar/writer.zig b/lib/std/tar/writer.zig
deleted file mode 100644
index 4ced287eec..0000000000
--- a/lib/std/tar/writer.zig
+++ /dev/null
@@ -1,497 +0,0 @@
-const std = @import("std");
-const assert = std.debug.assert;
-const testing = std.testing;
-
-/// Creates tar Writer which will write tar content to the `underlying_writer`.
-/// Use setRoot to nest all following entries under single root. If file don't
-/// fit into posix header (name+prefix: 100+155 bytes) gnu extented header will
-/// be used for long names. Options enables setting file premission mode and
-/// mtime. Default is to use current time for mtime and 0o664 for file mode.
-pub fn writer(underlying_writer: anytype) Writer(@TypeOf(underlying_writer)) {
- return .{ .underlying_writer = underlying_writer };
-}
-
-pub fn Writer(comptime WriterType: type) type {
- return struct {
- const block_size = @sizeOf(Header);
- const empty_block: [block_size]u8 = [_]u8{0} ** block_size;
-
- /// Options for writing file/dir/link. If left empty 0o664 is used for
- /// file mode and current time for mtime.
- pub const Options = struct {
- /// File system permission mode.
- mode: u32 = 0,
- /// File system modification time.
- mtime: u64 = 0,
- };
- const Self = @This();
-
- underlying_writer: WriterType,
- prefix: []const u8 = "",
- mtime_now: u64 = 0,
-
- /// Sets prefix for all other write* method paths.
- pub fn setRoot(self: *Self, root: []const u8) !void {
- if (root.len > 0)
- try self.writeDir(root, .{});
-
- self.prefix = root;
- }
-
- /// Writes directory.
- pub fn writeDir(self: *Self, sub_path: []const u8, opt: Options) !void {
- try self.writeHeader(.directory, sub_path, "", 0, opt);
- }
-
- /// Writes file system file.
- pub fn writeFile(self: *Self, sub_path: []const u8, file: std.fs.File) !void {
- const stat = try file.stat();
- const mtime: u64 = @intCast(@divFloor(stat.mtime, std.time.ns_per_s));
-
- var header = Header{};
- try self.setPath(&header, sub_path);
- try header.setSize(stat.size);
- try header.setMtime(mtime);
- try header.write(self.underlying_writer);
-
- try self.underlying_writer.writeFile(file);
- try self.writePadding(stat.size);
- }
-
- /// Writes file reading file content from `reader`. Number of bytes in
- /// reader must be equal to `size`.
- pub fn writeFileStream(self: *Self, sub_path: []const u8, size: usize, reader: anytype, opt: Options) !void {
- try self.writeHeader(.regular, sub_path, "", @intCast(size), opt);
-
- var counting_reader = std.io.countingReader(reader);
- var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
- try fifo.pump(counting_reader.reader(), self.underlying_writer);
- if (counting_reader.bytes_read != size) return error.WrongReaderSize;
- try self.writePadding(size);
- }
-
- /// Writes file using bytes buffer `content` for size and file content.
- pub fn writeFileBytes(self: *Self, sub_path: []const u8, content: []const u8, opt: Options) !void {
- try self.writeHeader(.regular, sub_path, "", @intCast(content.len), opt);
- try self.underlying_writer.writeAll(content);
- try self.writePadding(content.len);
- }
-
- /// Writes symlink.
- pub fn writeLink(self: *Self, sub_path: []const u8, link_name: []const u8, opt: Options) !void {
- try self.writeHeader(.symbolic_link, sub_path, link_name, 0, opt);
- }
-
- /// Writes fs.Dir.WalkerEntry. Uses `mtime` from file system entry and
- /// default for entry mode .
- pub fn writeEntry(self: *Self, entry: std.fs.Dir.Walker.Entry) !void {
- switch (entry.kind) {
- .directory => {
- try self.writeDir(entry.path, .{ .mtime = try entryMtime(entry) });
- },
- .file => {
- var file = try entry.dir.openFile(entry.basename, .{});
- defer file.close();
- try self.writeFile(entry.path, file);
- },
- .sym_link => {
- var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
- const link_name = try entry.dir.readLink(entry.basename, &link_name_buffer);
- try self.writeLink(entry.path, link_name, .{ .mtime = try entryMtime(entry) });
- },
- else => {
- return error.UnsupportedWalkerEntryKind;
- },
- }
- }
-
- fn writeHeader(
- self: *Self,
- typeflag: Header.FileType,
- sub_path: []const u8,
- link_name: []const u8,
- size: u64,
- opt: Options,
- ) !void {
- var header = Header.init(typeflag);
- try self.setPath(&header, sub_path);
- try header.setSize(size);
- try header.setMtime(if (opt.mtime != 0) opt.mtime else self.mtimeNow());
- if (opt.mode != 0)
- try header.setMode(opt.mode);
- if (typeflag == .symbolic_link)
- header.setLinkname(link_name) catch |err| switch (err) {
- error.NameTooLong => try self.writeExtendedHeader(.gnu_long_link, &.{link_name}),
- else => return err,
- };
- try header.write(self.underlying_writer);
- }
-
- fn mtimeNow(self: *Self) u64 {
- if (self.mtime_now == 0)
- self.mtime_now = @intCast(std.time.timestamp());
- return self.mtime_now;
- }
-
- fn entryMtime(entry: std.fs.Dir.Walker.Entry) !u64 {
- const stat = try entry.dir.statFile(entry.basename);
- return @intCast(@divFloor(stat.mtime, std.time.ns_per_s));
- }
-
- /// Writes path in posix header, if don't fit (in name+prefix; 100+155
- /// bytes) writes it in gnu extended header.
- fn setPath(self: *Self, header: *Header, sub_path: []const u8) !void {
- header.setPath(self.prefix, sub_path) catch |err| switch (err) {
- error.NameTooLong => {
- // write extended header
- const buffers: []const []const u8 = if (self.prefix.len == 0)
- &.{sub_path}
- else
- &.{ self.prefix, "/", sub_path };
- try self.writeExtendedHeader(.gnu_long_name, buffers);
- },
- else => return err,
- };
- }
-
- /// Writes gnu extended header: gnu_long_name or gnu_long_link.
- fn writeExtendedHeader(self: *Self, typeflag: Header.FileType, buffers: []const []const u8) !void {
- var len: usize = 0;
- for (buffers) |buf|
- len += buf.len;
-
- var header = Header.init(typeflag);
- try header.setSize(len);
- try header.write(self.underlying_writer);
- for (buffers) |buf|
- try self.underlying_writer.writeAll(buf);
- try self.writePadding(len);
- }
-
- fn writePadding(self: *Self, bytes: u64) !void {
- const pos: usize = @intCast(bytes % block_size);
- if (pos == 0) return;
- try self.underlying_writer.writeAll(empty_block[pos..]);
- }
-
- /// Tar should finish with two zero blocks, but 'reasonable system must
- /// not assume that such a block exists when reading an archive' (from
- /// reference). In practice it is safe to skip this finish.
- pub fn finish(self: *Self) !void {
- try self.underlying_writer.writeAll(&empty_block);
- try self.underlying_writer.writeAll(&empty_block);
- }
- };
-}
-
-/// A struct that is exactly 512 bytes and matches tar file format. This is
-/// intended to be used for outputting tar files; for parsing there is
-/// `std.tar.Header`.
-const Header = extern struct {
- // This struct was originally copied from
- // https://github.com/mattnite/tar/blob/main/src/main.zig which is MIT
- // licensed.
- //
- // The name, linkname, magic, uname, and gname are null-terminated character
- // strings. All other fields are zero-filled octal numbers in ASCII. Each
- // numeric field of width w contains w minus 1 digits, and a null.
- // Reference: https://www.gnu.org/software/tar/manual/html_node/Standard.html
- // POSIX header: byte offset
- name: [100]u8 = [_]u8{0} ** 100, // 0
- mode: [7:0]u8 = default_mode.file, // 100
- uid: [7:0]u8 = [_:0]u8{0} ** 7, // unused 108
- gid: [7:0]u8 = [_:0]u8{0} ** 7, // unused 116
- size: [11:0]u8 = [_:0]u8{'0'} ** 11, // 124
- mtime: [11:0]u8 = [_:0]u8{'0'} ** 11, // 136
- checksum: [7:0]u8 = [_:0]u8{' '} ** 7, // 148
- typeflag: FileType = .regular, // 156
- linkname: [100]u8 = [_]u8{0} ** 100, // 157
- magic: [6]u8 = [_]u8{ 'u', 's', 't', 'a', 'r', 0 }, // 257
- version: [2]u8 = [_]u8{ '0', '0' }, // 263
- uname: [32]u8 = [_]u8{0} ** 32, // unused 265
- gname: [32]u8 = [_]u8{0} ** 32, // unused 297
- devmajor: [7:0]u8 = [_:0]u8{0} ** 7, // unused 329
- devminor: [7:0]u8 = [_:0]u8{0} ** 7, // unused 337
- prefix: [155]u8 = [_]u8{0} ** 155, // 345
- pad: [12]u8 = [_]u8{0} ** 12, // unused 500
-
- pub const FileType = enum(u8) {
- regular = '0',
- symbolic_link = '2',
- directory = '5',
- gnu_long_name = 'L',
- gnu_long_link = 'K',
- };
-
- const default_mode = struct {
- const file = [_:0]u8{ '0', '0', '0', '0', '6', '6', '4' }; // 0o664
- const dir = [_:0]u8{ '0', '0', '0', '0', '7', '7', '5' }; // 0o775
- const sym_link = [_:0]u8{ '0', '0', '0', '0', '7', '7', '7' }; // 0o777
- const other = [_:0]u8{ '0', '0', '0', '0', '0', '0', '0' }; // 0o000
- };
-
- pub fn init(typeflag: FileType) Header {
- return .{
- .typeflag = typeflag,
- .mode = switch (typeflag) {
- .directory => default_mode.dir,
- .symbolic_link => default_mode.sym_link,
- .regular => default_mode.file,
- else => default_mode.other,
- },
- };
- }
-
- pub fn setSize(self: *Header, size: u64) !void {
- try octal(&self.size, size);
- }
-
- fn octal(buf: []u8, value: u64) !void {
- var remainder: u64 = value;
- var pos: usize = buf.len;
- while (remainder > 0 and pos > 0) {
- pos -= 1;
- const c: u8 = @as(u8, @intCast(remainder % 8)) + '0';
- buf[pos] = c;
- remainder /= 8;
- if (pos == 0 and remainder > 0) return error.OctalOverflow;
- }
- }
-
- pub fn setMode(self: *Header, mode: u32) !void {
- try octal(&self.mode, mode);
- }
-
- // Integer number of seconds since January 1, 1970, 00:00 Coordinated Universal Time.
- // mtime == 0 will use current time
- pub fn setMtime(self: *Header, mtime: u64) !void {
- try octal(&self.mtime, mtime);
- }
-
- pub fn updateChecksum(self: *Header) !void {
- var checksum: usize = ' '; // other 7 self.checksum bytes are initialized to ' '
- for (std.mem.asBytes(self)) |val|
- checksum += val;
- try octal(&self.checksum, checksum);
- }
-
- pub fn write(self: *Header, output_writer: anytype) !void {
- try self.updateChecksum();
- try output_writer.writeAll(std.mem.asBytes(self));
- }
-
- pub fn setLinkname(self: *Header, link: []const u8) !void {
- if (link.len > self.linkname.len) return error.NameTooLong;
- @memcpy(self.linkname[0..link.len], link);
- }
-
- pub fn setPath(self: *Header, prefix: []const u8, sub_path: []const u8) !void {
- const max_prefix = self.prefix.len;
- const max_name = self.name.len;
- const sep = std.fs.path.sep_posix;
-
- if (prefix.len + sub_path.len > max_name + max_prefix or prefix.len > max_prefix)
- return error.NameTooLong;
-
- // both fit into name
- if (prefix.len > 0 and prefix.len + sub_path.len < max_name) {
- @memcpy(self.name[0..prefix.len], prefix);
- self.name[prefix.len] = sep;
- @memcpy(self.name[prefix.len + 1 ..][0..sub_path.len], sub_path);
- return;
- }
-
- // sub_path fits into name
- // there is no prefix or prefix fits into prefix
- if (sub_path.len <= max_name) {
- @memcpy(self.name[0..sub_path.len], sub_path);
- @memcpy(self.prefix[0..prefix.len], prefix);
- return;
- }
-
- if (prefix.len > 0) {
- @memcpy(self.prefix[0..prefix.len], prefix);
- self.prefix[prefix.len] = sep;
- }
- const prefix_pos = if (prefix.len > 0) prefix.len + 1 else 0;
-
- // add as much to prefix as you can, must split at /
- const prefix_remaining = max_prefix - prefix_pos;
- if (std.mem.lastIndexOf(u8, sub_path[0..@min(prefix_remaining, sub_path.len)], &.{'/'})) |sep_pos| {
- @memcpy(self.prefix[prefix_pos..][0..sep_pos], sub_path[0..sep_pos]);
- if ((sub_path.len - sep_pos - 1) > max_name) return error.NameTooLong;
- @memcpy(self.name[0..][0 .. sub_path.len - sep_pos - 1], sub_path[sep_pos + 1 ..]);
- return;
- }
-
- return error.NameTooLong;
- }
-
- comptime {
- assert(@sizeOf(Header) == 512);
- }
-
- test setPath {
- const cases = [_]struct {
- in: []const []const u8,
- out: []const []const u8,
- }{
- .{
- .in = &.{ "", "123456789" },
- .out = &.{ "", "123456789" },
- },
- // can fit into name
- .{
- .in = &.{ "prefix", "sub_path" },
- .out = &.{ "", "prefix/sub_path" },
- },
- // no more both fits into name
- .{
- .in = &.{ "prefix", "0123456789/" ** 8 ++ "basename" },
- .out = &.{ "prefix", "0123456789/" ** 8 ++ "basename" },
- },
- // put as much as you can into prefix the rest goes into name
- .{
- .in = &.{ "prefix", "0123456789/" ** 10 ++ "basename" },
- .out = &.{ "prefix/" ++ "0123456789/" ** 9 ++ "0123456789", "basename" },
- },
-
- .{
- .in = &.{ "prefix", "0123456789/" ** 15 ++ "basename" },
- .out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/0123456789/basename" },
- },
- .{
- .in = &.{ "prefix", "0123456789/" ** 21 ++ "basename" },
- .out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/" ** 8 ++ "basename" },
- },
- .{
- .in = &.{ "", "012345678/" ** 10 ++ "foo" },
- .out = &.{ "012345678/" ** 9 ++ "012345678", "foo" },
- },
- };
-
- for (cases) |case| {
- var header = Header.init(.regular);
- try header.setPath(case.in[0], case.in[1]);
- try testing.expectEqualStrings(case.out[0], str(&header.prefix));
- try testing.expectEqualStrings(case.out[1], str(&header.name));
- }
-
- const error_cases = [_]struct {
- in: []const []const u8,
- }{
- // basename can't fit into name (106 characters)
- .{ .in = &.{ "zig", "test/cases/compile_errors/regression_test_2980_base_type_u32_is_not_type_checked_properly_when_assigning_a_value_within_a_struct.zig" } },
- // cant fit into 255 + sep
- .{ .in = &.{ "prefix", "0123456789/" ** 22 ++ "basename" } },
- // can fit but sub_path can't be split (there is no separator)
- .{ .in = &.{ "prefix", "0123456789" ** 10 ++ "a" } },
- .{ .in = &.{ "prefix", "0123456789" ** 14 ++ "basename" } },
- };
-
- for (error_cases) |case| {
- var header = Header.init(.regular);
- try testing.expectError(
- error.NameTooLong,
- header.setPath(case.in[0], case.in[1]),
- );
- }
- }
-
- // Breaks string on first null character.
- fn str(s: []const u8) []const u8 {
- for (s, 0..) |c, i| {
- if (c == 0) return s[0..i];
- }
- return s;
- }
-};
-
-test {
- _ = Header;
-}
-
-test "write files" {
- const files = [_]struct {
- path: []const u8,
- content: []const u8,
- }{
- .{ .path = "foo", .content = "bar" },
- .{ .path = "a12345678/" ** 10 ++ "foo", .content = "a" ** 511 },
- .{ .path = "b12345678/" ** 24 ++ "foo", .content = "b" ** 512 },
- .{ .path = "c12345678/" ** 25 ++ "foo", .content = "c" ** 513 },
- .{ .path = "d12345678/" ** 51 ++ "foo", .content = "d" ** 1025 },
- .{ .path = "e123456789" ** 11, .content = "e" },
- };
-
- var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
- var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
-
- // with root
- {
- const root = "root";
-
- var output = std.ArrayList(u8).init(testing.allocator);
- defer output.deinit();
- var wrt = writer(output.writer());
- try wrt.setRoot(root);
- for (files) |file|
- try wrt.writeFileBytes(file.path, file.content, .{});
-
- var input = std.io.fixedBufferStream(output.items);
- var iter = std.tar.iterator(
- input.reader(),
- .{ .file_name_buffer = &file_name_buffer, .link_name_buffer = &link_name_buffer },
- );
-
- // first entry is directory with prefix
- {
- const actual = (try iter.next()).?;
- try testing.expectEqualStrings(root, actual.name);
- try testing.expectEqual(std.tar.FileKind.directory, actual.kind);
- }
-
- var i: usize = 0;
- while (try iter.next()) |actual| {
- defer i += 1;
- const expected = files[i];
- try testing.expectEqualStrings(root, actual.name[0..root.len]);
- try testing.expectEqual('/', actual.name[root.len..][0]);
- try testing.expectEqualStrings(expected.path, actual.name[root.len + 1 ..]);
-
- var content = std.ArrayList(u8).init(testing.allocator);
- defer content.deinit();
- try actual.writeAll(content.writer());
- try testing.expectEqualSlices(u8, expected.content, content.items);
- }
- }
- // without root
- {
- var output = std.ArrayList(u8).init(testing.allocator);
- defer output.deinit();
- var wrt = writer(output.writer());
- for (files) |file| {
- var content = std.io.fixedBufferStream(file.content);
- try wrt.writeFileStream(file.path, file.content.len, content.reader(), .{});
- }
-
- var input = std.io.fixedBufferStream(output.items);
- var iter = std.tar.iterator(
- input.reader(),
- .{ .file_name_buffer = &file_name_buffer, .link_name_buffer = &link_name_buffer },
- );
-
- var i: usize = 0;
- while (try iter.next()) |actual| {
- defer i += 1;
- const expected = files[i];
- try testing.expectEqualStrings(expected.path, actual.name);
-
- var content = std.ArrayList(u8).init(testing.allocator);
- defer content.deinit();
- try actual.writeAll(content.writer());
- try testing.expectEqualSlices(u8, expected.content, content.items);
- }
- try wrt.finish();
- }
-}