master
  1const Writer = @This();
  2
  3const std = @import("std");
  4const Io = std.Io;
  5const assert = std.debug.assert;
  6const testing = std.testing;
  7
  8const block_size = @sizeOf(Header);
  9
 10/// Options for writing file/dir/link. If left empty 0o664 is used for
 11/// file mode and current time for mtime.
 12pub const Options = struct {
 13    /// File system permission mode.
 14    mode: u32 = 0,
 15    /// File system modification time.
 16    mtime: u64 = 0,
 17};
 18
 19underlying_writer: *Io.Writer,
 20prefix: []const u8 = "",
 21
 22const Error = error{
 23    WriteFailed,
 24    OctalOverflow,
 25    NameTooLong,
 26};
 27
 28/// Sets prefix for all other write* method paths.
 29pub fn setRoot(w: *Writer, root: []const u8) Error!void {
 30    if (root.len > 0)
 31        try w.writeDir(root, .{});
 32
 33    w.prefix = root;
 34}
 35
 36pub fn writeDir(w: *Writer, sub_path: []const u8, options: Options) Error!void {
 37    try w.writeHeader(.directory, sub_path, "", 0, options);
 38}
 39
 40pub const WriteFileError = Io.Writer.FileError || Error || Io.File.Reader.SizeError;
 41
 42pub fn writeFileTimestamp(
 43    w: *Writer,
 44    sub_path: []const u8,
 45    file_reader: *Io.File.Reader,
 46    mtime: Io.Timestamp,
 47) WriteFileError!void {
 48    return writeFile(w, sub_path, file_reader, @intCast(mtime.toSeconds()));
 49}
 50
 51pub fn writeFile(
 52    w: *Writer,
 53    sub_path: []const u8,
 54    file_reader: *Io.File.Reader,
 55    /// If you want to match the file format's expectations, it wants number of
 56    /// seconds since POSIX epoch. Zero is also a great option here to make
 57    /// generated tarballs more reproducible.
 58    mtime: u64,
 59) WriteFileError!void {
 60    const size = try file_reader.getSize();
 61
 62    var header: Header = .{};
 63    try w.setPath(&header, sub_path);
 64    try header.setSize(size);
 65    try header.setMtime(mtime);
 66    try header.updateChecksum();
 67
 68    try w.underlying_writer.writeAll(@ptrCast((&header)[0..1]));
 69    _ = try w.underlying_writer.sendFileAll(file_reader, .unlimited);
 70    try w.writePadding64(size);
 71}
 72
 73pub const WriteFileStreamError = Error || Io.Reader.StreamError;
 74
 75/// Writes file reading file content from `reader`. Reads exactly `size` bytes
 76/// from `reader`, or returns `error.EndOfStream`.
 77pub fn writeFileStream(
 78    w: *Writer,
 79    sub_path: []const u8,
 80    size: u64,
 81    reader: *Io.Reader,
 82    options: Options,
 83) WriteFileStreamError!void {
 84    try w.writeHeader(.regular, sub_path, "", size, options);
 85    try reader.streamExact64(w.underlying_writer, size);
 86    try w.writePadding64(size);
 87}
 88
 89/// Writes file using bytes buffer `content` for size and file content.
 90pub fn writeFileBytes(w: *Writer, sub_path: []const u8, content: []const u8, options: Options) Error!void {
 91    try w.writeHeader(.regular, sub_path, "", content.len, options);
 92    try w.underlying_writer.writeAll(content);
 93    try w.writePadding(content.len);
 94}
 95
 96pub fn writeLink(w: *Writer, sub_path: []const u8, link_name: []const u8, options: Options) Error!void {
 97    try w.writeHeader(.symbolic_link, sub_path, link_name, 0, options);
 98}
 99
100fn writeHeader(
101    w: *Writer,
102    typeflag: Header.FileType,
103    sub_path: []const u8,
104    link_name: []const u8,
105    size: u64,
106    options: Options,
107) Error!void {
108    var header = Header.init(typeflag);
109    try w.setPath(&header, sub_path);
110    try header.setSize(size);
111    try header.setMtime(options.mtime);
112    if (options.mode != 0)
113        try header.setMode(options.mode);
114    if (typeflag == .symbolic_link)
115        header.setLinkname(link_name) catch |err| switch (err) {
116            error.NameTooLong => try w.writeExtendedHeader(.gnu_long_link, &.{link_name}),
117            else => return err,
118        };
119    try header.write(w.underlying_writer);
120}
121
122/// Writes path in posix header, if don't fit (in name+prefix; 100+155
123/// bytes) writes it in gnu extended header.
124fn setPath(w: *Writer, header: *Header, sub_path: []const u8) Error!void {
125    header.setPath(w.prefix, sub_path) catch |err| switch (err) {
126        error.NameTooLong => {
127            // write extended header
128            const buffers: []const []const u8 = if (w.prefix.len == 0)
129                &.{sub_path}
130            else
131                &.{ w.prefix, "/", sub_path };
132            try w.writeExtendedHeader(.gnu_long_name, buffers);
133        },
134        else => return err,
135    };
136}
137
138/// Writes gnu extended header: gnu_long_name or gnu_long_link.
139fn writeExtendedHeader(w: *Writer, typeflag: Header.FileType, buffers: []const []const u8) Error!void {
140    var len: usize = 0;
141    for (buffers) |buf| len += buf.len;
142
143    var header: Header = .init(typeflag);
144    try header.setSize(len);
145    try header.write(w.underlying_writer);
146    for (buffers) |buf|
147        try w.underlying_writer.writeAll(buf);
148    try w.writePadding(len);
149}
150
151fn writePadding(w: *Writer, bytes: usize) Io.Writer.Error!void {
152    return writePaddingPos(w, bytes % block_size);
153}
154
155fn writePadding64(w: *Writer, bytes: u64) Io.Writer.Error!void {
156    return writePaddingPos(w, @intCast(bytes % block_size));
157}
158
159fn writePaddingPos(w: *Writer, pos: usize) Io.Writer.Error!void {
160    if (pos == 0) return;
161    try w.underlying_writer.splatByteAll(0, block_size - pos);
162}
163
164/// According to the specification, tar should finish with two zero blocks, but
165/// "reasonable system must not assume that such a block exists when reading an
166/// archive". Therefore, the Zig standard library recommends to not call this
167/// function.
168pub fn finishPedantically(w: *Writer) Io.Writer.Error!void {
169    try w.underlying_writer.splatByteAll(0, block_size * 2);
170}
171
172/// A struct that is exactly 512 bytes and matches tar file format. This is
173/// intended to be used for outputting tar files; for parsing there is
174/// `std.tar.Header`.
175pub const Header = extern struct {
176    // This struct was originally copied from
177    // https://github.com/mattnite/tar/blob/main/src/main.zig which is MIT
178    // licensed.
179    //
180    // The name, linkname, magic, uname, and gname are null-terminated character
181    // strings. All other fields are zero-filled octal numbers in ASCII. Each
182    // numeric field of width w contains w minus 1 digits, and a null.
183    // Reference: https://www.gnu.org/software/tar/manual/html_node/Standard.html
184    // POSIX header:                                  byte offset
185    name: [100]u8 = [_]u8{0} ** 100, //                         0
186    mode: [7:0]u8 = default_mode.file, //                     100
187    uid: [7:0]u8 = [_:0]u8{0} ** 7, // unused                 108
188    gid: [7:0]u8 = [_:0]u8{0} ** 7, // unused                 116
189    size: [11:0]u8 = [_:0]u8{'0'} ** 11, //                   124
190    mtime: [11:0]u8 = [_:0]u8{'0'} ** 11, //                  136
191    checksum: [7:0]u8 = [_:0]u8{' '} ** 7, //                 148
192    typeflag: FileType = .regular, //                         156
193    linkname: [100]u8 = [_]u8{0} ** 100, //                   157
194    magic: [6]u8 = [_]u8{ 'u', 's', 't', 'a', 'r', 0 }, //    257
195    version: [2]u8 = [_]u8{ '0', '0' }, //                    263
196    uname: [32]u8 = [_]u8{0} ** 32, // unused                 265
197    gname: [32]u8 = [_]u8{0} ** 32, // unused                 297
198    devmajor: [7:0]u8 = [_:0]u8{0} ** 7, // unused            329
199    devminor: [7:0]u8 = [_:0]u8{0} ** 7, // unused            337
200    prefix: [155]u8 = [_]u8{0} ** 155, //                     345
201    pad: [12]u8 = [_]u8{0} ** 12, // unused                   500
202
203    pub const FileType = enum(u8) {
204        regular = '0',
205        symbolic_link = '2',
206        directory = '5',
207        gnu_long_name = 'L',
208        gnu_long_link = 'K',
209    };
210
211    const default_mode = struct {
212        const file = [_:0]u8{ '0', '0', '0', '0', '6', '6', '4' }; // 0o664
213        const dir = [_:0]u8{ '0', '0', '0', '0', '7', '7', '5' }; // 0o775
214        const sym_link = [_:0]u8{ '0', '0', '0', '0', '7', '7', '7' }; // 0o777
215        const other = [_:0]u8{ '0', '0', '0', '0', '0', '0', '0' }; // 0o000
216    };
217
218    pub fn init(typeflag: FileType) Header {
219        return .{
220            .typeflag = typeflag,
221            .mode = switch (typeflag) {
222                .directory => default_mode.dir,
223                .symbolic_link => default_mode.sym_link,
224                .regular => default_mode.file,
225                else => default_mode.other,
226            },
227        };
228    }
229
230    pub fn setSize(w: *Header, size: u64) error{OctalOverflow}!void {
231        try octal(&w.size, size);
232    }
233
234    fn octal(buf: []u8, value: u64) error{OctalOverflow}!void {
235        var remainder: u64 = value;
236        var pos: usize = buf.len;
237        while (remainder > 0 and pos > 0) {
238            pos -= 1;
239            const c: u8 = @as(u8, @intCast(remainder % 8)) + '0';
240            buf[pos] = c;
241            remainder /= 8;
242            if (pos == 0 and remainder > 0) return error.OctalOverflow;
243        }
244    }
245
246    pub fn setMode(w: *Header, mode: u32) error{OctalOverflow}!void {
247        try octal(&w.mode, mode);
248    }
249
250    // Integer number of seconds since January 1, 1970, 00:00 Coordinated Universal Time.
251    pub fn setMtime(w: *Header, mtime: u64) error{OctalOverflow}!void {
252        try octal(&w.mtime, mtime);
253    }
254
255    pub fn updateChecksum(w: *Header) !void {
256        var checksum: usize = ' '; // other 7 w.checksum bytes are initialized to ' '
257        for (std.mem.asBytes(w)) |val|
258            checksum += val;
259        try octal(&w.checksum, checksum);
260    }
261
262    pub fn write(h: *Header, bw: *Io.Writer) error{ OctalOverflow, WriteFailed }!void {
263        try h.updateChecksum();
264        try bw.writeAll(std.mem.asBytes(h));
265    }
266
267    pub fn setLinkname(w: *Header, link: []const u8) !void {
268        if (link.len > w.linkname.len) return error.NameTooLong;
269        @memcpy(w.linkname[0..link.len], link);
270    }
271
272    pub fn setPath(w: *Header, prefix: []const u8, sub_path: []const u8) !void {
273        const max_prefix = w.prefix.len;
274        const max_name = w.name.len;
275        const sep = std.fs.path.sep_posix;
276
277        if (prefix.len + sub_path.len > max_name + max_prefix or prefix.len > max_prefix)
278            return error.NameTooLong;
279
280        // both fit into name
281        if (prefix.len > 0 and prefix.len + sub_path.len < max_name) {
282            @memcpy(w.name[0..prefix.len], prefix);
283            w.name[prefix.len] = sep;
284            @memcpy(w.name[prefix.len + 1 ..][0..sub_path.len], sub_path);
285            return;
286        }
287
288        // sub_path fits into name
289        // there is no prefix or prefix fits into prefix
290        if (sub_path.len <= max_name) {
291            @memcpy(w.name[0..sub_path.len], sub_path);
292            @memcpy(w.prefix[0..prefix.len], prefix);
293            return;
294        }
295
296        if (prefix.len > 0) {
297            @memcpy(w.prefix[0..prefix.len], prefix);
298            w.prefix[prefix.len] = sep;
299        }
300        const prefix_pos = if (prefix.len > 0) prefix.len + 1 else 0;
301
302        // add as much to prefix as you can, must split at /
303        const prefix_remaining = max_prefix - prefix_pos;
304        if (std.mem.lastIndexOf(u8, sub_path[0..@min(prefix_remaining, sub_path.len)], &.{'/'})) |sep_pos| {
305            @memcpy(w.prefix[prefix_pos..][0..sep_pos], sub_path[0..sep_pos]);
306            if ((sub_path.len - sep_pos - 1) > max_name) return error.NameTooLong;
307            @memcpy(w.name[0..][0 .. sub_path.len - sep_pos - 1], sub_path[sep_pos + 1 ..]);
308            return;
309        }
310
311        return error.NameTooLong;
312    }
313
314    comptime {
315        assert(@sizeOf(Header) == 512);
316    }
317
318    test "setPath" {
319        const cases = [_]struct {
320            in: []const []const u8,
321            out: []const []const u8,
322        }{
323            .{
324                .in = &.{ "", "123456789" },
325                .out = &.{ "", "123456789" },
326            },
327            // can fit into name
328            .{
329                .in = &.{ "prefix", "sub_path" },
330                .out = &.{ "", "prefix/sub_path" },
331            },
332            // no more both fits into name
333            .{
334                .in = &.{ "prefix", "0123456789/" ** 8 ++ "basename" },
335                .out = &.{ "prefix", "0123456789/" ** 8 ++ "basename" },
336            },
337            // put as much as you can into prefix the rest goes into name
338            .{
339                .in = &.{ "prefix", "0123456789/" ** 10 ++ "basename" },
340                .out = &.{ "prefix/" ++ "0123456789/" ** 9 ++ "0123456789", "basename" },
341            },
342
343            .{
344                .in = &.{ "prefix", "0123456789/" ** 15 ++ "basename" },
345                .out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/0123456789/basename" },
346            },
347            .{
348                .in = &.{ "prefix", "0123456789/" ** 21 ++ "basename" },
349                .out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/" ** 8 ++ "basename" },
350            },
351            .{
352                .in = &.{ "", "012345678/" ** 10 ++ "foo" },
353                .out = &.{ "012345678/" ** 9 ++ "012345678", "foo" },
354            },
355        };
356
357        for (cases) |case| {
358            var header = Header.init(.regular);
359            try header.setPath(case.in[0], case.in[1]);
360            try testing.expectEqualStrings(case.out[0], std.mem.sliceTo(&header.prefix, 0));
361            try testing.expectEqualStrings(case.out[1], std.mem.sliceTo(&header.name, 0));
362        }
363
364        const error_cases = [_]struct {
365            in: []const []const u8,
366        }{
367            // basename can't fit into name (106 characters)
368            .{ .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" } },
369            // cant fit into 255 + sep
370            .{ .in = &.{ "prefix", "0123456789/" ** 22 ++ "basename" } },
371            // can fit but sub_path can't be split (there is no separator)
372            .{ .in = &.{ "prefix", "0123456789" ** 10 ++ "a" } },
373            .{ .in = &.{ "prefix", "0123456789" ** 14 ++ "basename" } },
374        };
375
376        for (error_cases) |case| {
377            var header = Header.init(.regular);
378            try testing.expectError(
379                error.NameTooLong,
380                header.setPath(case.in[0], case.in[1]),
381            );
382        }
383    }
384};
385
386test {
387    _ = Header;
388}
389
390test "write files" {
391    const files = [_]struct {
392        path: []const u8,
393        content: []const u8,
394    }{
395        .{ .path = "foo", .content = "bar" },
396        .{ .path = "a12345678/" ** 10 ++ "foo", .content = "a" ** 511 },
397        .{ .path = "b12345678/" ** 24 ++ "foo", .content = "b" ** 512 },
398        .{ .path = "c12345678/" ** 25 ++ "foo", .content = "c" ** 513 },
399        .{ .path = "d12345678/" ** 51 ++ "foo", .content = "d" ** 1025 },
400        .{ .path = "e123456789" ** 11, .content = "e" },
401    };
402
403    var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
404    var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
405
406    // with root
407    {
408        const root = "root";
409
410        var output: Io.Writer.Allocating = .init(testing.allocator);
411        var w: Writer = .{ .underlying_writer = &output.writer };
412        defer output.deinit();
413        try w.setRoot(root);
414        for (files) |file|
415            try w.writeFileBytes(file.path, file.content, .{});
416
417        var input: Io.Reader = .fixed(output.written());
418        var it: std.tar.Iterator = .init(&input, .{
419            .file_name_buffer = &file_name_buffer,
420            .link_name_buffer = &link_name_buffer,
421        });
422
423        // first entry is directory with prefix
424        {
425            const actual = (try it.next()).?;
426            try testing.expectEqualStrings(root, actual.name);
427            try testing.expectEqual(std.tar.FileKind.directory, actual.kind);
428        }
429
430        var i: usize = 0;
431        while (try it.next()) |actual| {
432            defer i += 1;
433            const expected = files[i];
434            try testing.expectEqualStrings(root, actual.name[0..root.len]);
435            try testing.expectEqual('/', actual.name[root.len..][0]);
436            try testing.expectEqualStrings(expected.path, actual.name[root.len + 1 ..]);
437
438            var content: Io.Writer.Allocating = .init(testing.allocator);
439            defer content.deinit();
440            try it.streamRemaining(actual, &content.writer);
441            try testing.expectEqualSlices(u8, expected.content, content.written());
442        }
443    }
444    // without root
445    {
446        var output: Io.Writer.Allocating = .init(testing.allocator);
447        var w: Writer = .{ .underlying_writer = &output.writer };
448        defer output.deinit();
449        for (files) |file| {
450            var content: Io.Reader = .fixed(file.content);
451            try w.writeFileStream(file.path, file.content.len, &content, .{});
452        }
453
454        var input: Io.Reader = .fixed(output.written());
455        var it: std.tar.Iterator = .init(&input, .{
456            .file_name_buffer = &file_name_buffer,
457            .link_name_buffer = &link_name_buffer,
458        });
459
460        var i: usize = 0;
461        while (try it.next()) |actual| {
462            defer i += 1;
463            const expected = files[i];
464            try testing.expectEqualStrings(expected.path, actual.name);
465
466            var content: Io.Writer.Allocating = .init(testing.allocator);
467            defer content.deinit();
468            try it.streamRemaining(actual, &content.writer);
469            try testing.expectEqualSlices(u8, expected.content, content.written());
470        }
471        try w.finishPedantically();
472    }
473}