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