authorgravatar for igor.anic@gmail.comIgor Anić <igor.anic@gmail.com> 2024-08-16 07:05:53+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2024-08-15 22:05:53-07:00
log72bcc3b7a52a8ce7a582ed205325f761da72a6a5
treec28796fb8db1093c10f3b27ca8b0980c6103308b
parent708414aaf4d88157153cf413137d2cb3c106c3b4
signaturebadge-check Signed by PGP key B5690EEEBB952194

std.tar: add writer (#19603)

Simplifies code in docs creation where we used `std.tar.output.Header`. Writer uses that Header internally and provides higher level interface. Updates checksum on write, handles long file names, allows setting mtime and file permission mode. Provides handy interface for passing `Dir.WalkerEntry`.

6 files changed, 517 insertions(+), 174 deletions(-)

lib/compiler/std-docs.zig+8-32
......@@ -181,7 +181,6 @@ fn serveSourcesTar(request: *std.http.Server.Request, context: *Context) !void {
181181 },
182182 },
183183 });
184 const w = response.writer();
185184
186185 var std_dir = try context.lib_dir.openDir("std", .{ .iterate = true });
187186 defer std_dir.close();
......@@ -189,6 +188,9 @@ fn serveSourcesTar(request: *std.http.Server.Request, context: *Context) !void {
189188 var walker = try std_dir.walk(gpa);
190189 defer walker.deinit();
191190
191 var archiver = std.tar.writer(response.writer());
192 archiver.prefix = "std";
193
192194 while (try walker.next()) |entry| {
193195 switch (entry.kind) {
194196 .file => {
......@@ -199,47 +201,21 @@ fn serveSourcesTar(request: *std.http.Server.Request, context: *Context) !void {
199201 },
200202 else => continue,
201203 }
202
203 var file = try std_dir.openFile(entry.path, .{});
204 var file = try entry.dir.openFile(entry.basename, .{});
204205 defer file.close();
205
206 const stat = try file.stat();
207 const padding = p: {
208 const remainder = stat.size % 512;
209 break :p if (remainder > 0) 512 - remainder else 0;
210 };
211
212 var file_header = std.tar.output.Header.init();
213 file_header.typeflag = .regular;
214 try file_header.setPath("std", entry.path);
215 try file_header.setSize(stat.size);
216 try file_header.updateChecksum();
217 try w.writeAll(std.mem.asBytes(&file_header));
218 try w.writeFile(file);
219 try w.writeByteNTimes(0, padding);
206 try archiver.writeFile(entry.path, file);
220207 }
221208
222209 {
223210 // Since this command is JIT compiled, the builtin module available in
224211 // this source file corresponds to the user's host system.
225212 const builtin_zig = @embedFile("builtin");
226
227 var file_header = std.tar.output.Header.init();
228 file_header.typeflag = .regular;
229 try file_header.setPath("builtin", "builtin.zig");
230 try file_header.setSize(builtin_zig.len);
231 try file_header.updateChecksum();
232 try w.writeAll(std.mem.asBytes(&file_header));
233 try w.writeAll(builtin_zig);
234 const padding = p: {
235 const remainder = builtin_zig.len % 512;
236 break :p if (remainder > 0) 512 - remainder else 0;
237 };
238 try w.writeByteNTimes(0, padding);
213 archiver.prefix = "builtin";
214 try archiver.writeFileBytes("builtin.zig", builtin_zig, .{});
239215 }
240216
241217 // intentionally omitting the pointless trailer
242 //try w.writeByteNTimes(0, 512 * 2);
218 //try archiver.finish();
243219 try response.end();
244220}
245221
lib/std/Build/Fuzz/WebServer.zig+5-25
......@@ -456,7 +456,6 @@ fn serveSourcesTar(ws: *WebServer, request: *std.http.Server.Request) !void {
456456 },
457457 },
458458 });
459 const w = response.writer();
460459
461460 const DedupeTable = std.ArrayHashMapUnmanaged(Build.Cache.Path, void, Build.Cache.Path.TableAdapter, false);
462461 var dedupe_table: DedupeTable = .{};
......@@ -490,6 +489,8 @@ fn serveSourcesTar(ws: *WebServer, request: *std.http.Server.Request) !void {
490489
491490 var cwd_cache: ?[]const u8 = null;
492491
492 var archiver = std.tar.writer(response.writer());
493
493494 for (deduped_paths) |joined_path| {
494495 var file = joined_path.root_dir.handle.openFile(joined_path.sub_path, .{}) catch |err| {
495496 log.err("failed to open {}: {s}", .{ joined_path, @errorName(err) });
......@@ -497,33 +498,12 @@ fn serveSourcesTar(ws: *WebServer, request: *std.http.Server.Request) !void {
497498 };
498499 defer file.close();
499500
500 const stat = file.stat() catch |err| {
501 log.err("failed to stat {}: {s}", .{ joined_path, @errorName(err) });
502 continue;
503 };
504 if (stat.kind != .file)
505 continue;
506
507 const padding = p: {
508 const remainder = stat.size % 512;
509 break :p if (remainder > 0) 512 - remainder else 0;
510 };
511
512 var file_header = std.tar.output.Header.init();
513 file_header.typeflag = .regular;
514 try file_header.setPath(
515 joined_path.root_dir.path orelse try memoizedCwd(arena, &cwd_cache),
516 joined_path.sub_path,
517 );
518 try file_header.setSize(stat.size);
519 try file_header.updateChecksum();
520 try w.writeAll(std.mem.asBytes(&file_header));
521 try w.writeFile(file);
522 try w.writeByteNTimes(0, padding);
501 archiver.prefix = joined_path.root_dir.path orelse try memoizedCwd(arena, &cwd_cache);
502 try archiver.writeFile(joined_path.sub_path, file);
523503 }
524504
525505 // intentionally omitting the pointless trailer
526 //try w.writeByteNTimes(0, 512 * 2);
506 //try archiver.finish();
527507 try response.end();
528508}
529509
lib/std/tar.zig+3-3
......@@ -19,7 +19,7 @@ const std = @import("std");
1919const assert = std.debug.assert;
2020const testing = std.testing;
2121
22pub const output = @import("tar/output.zig");
22pub const writer = @import("tar/writer.zig").writer;
2323
2424/// Provide this to receive detailed error messages.
2525/// When this is provided, some errors which would otherwise be returned
......@@ -355,13 +355,13 @@ pub fn Iterator(comptime ReaderType: type) type {
355355 }
356356
357357 // Writes file content to writer.
358 pub fn writeAll(self: File, writer: anytype) !void {
358 pub fn writeAll(self: File, out_writer: anytype) !void {
359359 var buffer: [4096]u8 = undefined;
360360
361361 while (self.unread_bytes.* > 0) {
362362 const buf = buffer[0..@min(buffer.len, self.unread_bytes.*)];
363363 try self.parent_reader.readNoEof(buf);
364 try writer.writeAll(buf);
364 try out_writer.writeAll(buf);
365365 self.unread_bytes.* -= buf.len;
366366 }
367367 }
lib/std/tar/output.zig deleted-85
......@@ -1,85 +0,0 @@
1/// A struct that is exactly 512 bytes and matches tar file format. This is
2/// intended to be used for outputting tar files; for parsing there is
3/// `std.tar.Header`.
4pub const Header = extern struct {
5 // This struct was originally copied from
6 // https://github.com/mattnite/tar/blob/main/src/main.zig which is MIT
7 // licensed.
8
9 name: [100]u8,
10 mode: [7:0]u8,
11 uid: [7:0]u8,
12 gid: [7:0]u8,
13 size: [11:0]u8,
14 mtime: [11:0]u8,
15 checksum: [7:0]u8,
16 typeflag: FileType,
17 linkname: [100]u8,
18 magic: [5:0]u8,
19 version: [2]u8,
20 uname: [31:0]u8,
21 gname: [31:0]u8,
22 devmajor: [7:0]u8,
23 devminor: [7:0]u8,
24 prefix: [155]u8,
25 pad: [12]u8,
26
27 pub const FileType = enum(u8) {
28 regular = '0',
29 hard_link = '1',
30 symbolic_link = '2',
31 character = '3',
32 block = '4',
33 directory = '5',
34 fifo = '6',
35 reserved = '7',
36 pax_global = 'g',
37 extended = 'x',
38 _,
39 };
40
41 pub fn init() Header {
42 var ret = std.mem.zeroes(Header);
43 ret.magic = [_:0]u8{ 'u', 's', 't', 'a', 'r' };
44 ret.version = [_:0]u8{ '0', '0' };
45 return ret;
46 }
47
48 pub fn setPath(self: *Header, prefix: []const u8, path: []const u8) !void {
49 if (prefix.len + 1 + path.len > 100) {
50 var i: usize = 0;
51 while (i < path.len and path.len - i > 100) {
52 while (path[i] != '/') : (i += 1) {}
53 }
54
55 _ = try std.fmt.bufPrint(&self.prefix, "{s}/{s}", .{ prefix, path[0..i] });
56 _ = try std.fmt.bufPrint(&self.name, "{s}", .{path[i + 1 ..]});
57 } else {
58 _ = try std.fmt.bufPrint(&self.name, "{s}/{s}", .{ prefix, path });
59 }
60 }
61
62 pub fn setSize(self: *Header, size: u64) !void {
63 _ = try std.fmt.bufPrint(&self.size, "{o:0>11}", .{size});
64 }
65
66 pub fn updateChecksum(self: *Header) !void {
67 const offset = @offsetOf(Header, "checksum");
68 var checksum: usize = 0;
69 for (std.mem.asBytes(self), 0..) |val, i| {
70 checksum += if (i >= offset and i < offset + @sizeOf(@TypeOf(self.checksum)))
71 ' '
72 else
73 val;
74 }
75
76 _ = try std.fmt.bufPrint(&self.checksum, "{o:0>7}", .{checksum});
77 }
78
79 comptime {
80 assert(@sizeOf(Header) == 512);
81 }
82};
83
84const std = @import("../std.zig");
85const assert = std.debug.assert;
lib/std/tar/writer.zig created+497
......@@ -0,0 +1,497 @@
1const std = @import("std");
2const assert = std.debug.assert;
3const testing = std.testing;
4
5/// Creates tar Writer which will write tar content to the `underlying_writer`.
6/// Use setRoot to nest all following entries under single root. If file don't
7/// fit into posix header (name+prefix: 100+155 bytes) gnu extented header will
8/// be used for long names. Options enables setting file premission mode and
9/// mtime. Default is to use current time for mtime and 0o664 for file mode.
10pub fn writer(underlying_writer: anytype) Writer(@TypeOf(underlying_writer)) {
11 return .{ .underlying_writer = underlying_writer };
12}
13
14pub fn Writer(comptime WriterType: type) type {
15 return struct {
16 const block_size = @sizeOf(Header);
17 const empty_block: [block_size]u8 = [_]u8{0} ** block_size;
18
19 /// Options for writing file/dir/link. If left empty 0o664 is used for
20 /// file mode and current time for mtime.
21 pub const Options = struct {
22 /// File system permission mode.
23 mode: u32 = 0,
24 /// File system modification time.
25 mtime: u64 = 0,
26 };
27 const Self = @This();
28
29 underlying_writer: WriterType,
30 prefix: []const u8 = "",
31 mtime_now: u64 = 0,
32
33 /// Sets prefix for all other write* method paths.
34 pub fn setRoot(self: *Self, root: []const u8) !void {
35 if (root.len > 0)
36 try self.writeDir(root, .{});
37
38 self.prefix = root;
39 }
40
41 /// Writes directory.
42 pub fn writeDir(self: *Self, sub_path: []const u8, opt: Options) !void {
43 try self.writeHeader(.directory, sub_path, "", 0, opt);
44 }
45
46 /// Writes file system file.
47 pub fn writeFile(self: *Self, sub_path: []const u8, file: std.fs.File) !void {
48 const stat = try file.stat();
49 const mtime: u64 = @intCast(@divFloor(stat.mtime, std.time.ns_per_s));
50
51 var header = Header{};
52 try self.setPath(&header, sub_path);
53 try header.setSize(stat.size);
54 try header.setMtime(mtime);
55 try header.write(self.underlying_writer);
56
57 try self.underlying_writer.writeFile(file);
58 try self.writePadding(stat.size);
59 }
60
61 /// Writes file reading file content from `reader`. Number of bytes in
62 /// reader must be equal to `size`.
63 pub fn writeFileStream(self: *Self, sub_path: []const u8, size: usize, reader: anytype, opt: Options) !void {
64 try self.writeHeader(.regular, sub_path, "", @intCast(size), opt);
65
66 var counting_reader = std.io.countingReader(reader);
67 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
68 try fifo.pump(counting_reader.reader(), self.underlying_writer);
69 if (counting_reader.bytes_read != size) return error.WrongReaderSize;
70 try self.writePadding(size);
71 }
72
73 /// Writes file using bytes buffer `content` for size and file content.
74 pub fn writeFileBytes(self: *Self, sub_path: []const u8, content: []const u8, opt: Options) !void {
75 try self.writeHeader(.regular, sub_path, "", @intCast(content.len), opt);
76 try self.underlying_writer.writeAll(content);
77 try self.writePadding(content.len);
78 }
79
80 /// Writes symlink.
81 pub fn writeLink(self: *Self, sub_path: []const u8, link_name: []const u8, opt: Options) !void {
82 try self.writeHeader(.symbolic_link, sub_path, link_name, 0, opt);
83 }
84
85 /// Writes fs.Dir.WalkerEntry. Uses `mtime` from file system entry and
86 /// default for entry mode .
87 pub fn writeEntry(self: *Self, entry: std.fs.Dir.Walker.WalkerEntry) !void {
88 switch (entry.kind) {
89 .directory => {
90 try self.writeDir(entry.path, .{ .mtime = try entryMtime(entry) });
91 },
92 .file => {
93 var file = try entry.dir.openFile(entry.basename, .{});
94 defer file.close();
95 try self.writeFile(entry.path, file);
96 },
97 .sym_link => {
98 var link_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
99 const link_name = try entry.dir.readLink(entry.basename, &link_name_buffer);
100 try self.writeLink(entry.path, link_name, .{ .mtime = try entryMtime(entry) });
101 },
102 else => {
103 return error.UnsupportedWalkerEntryKind;
104 },
105 }
106 }
107
108 fn writeHeader(
109 self: *Self,
110 typeflag: Header.FileType,
111 sub_path: []const u8,
112 link_name: []const u8,
113 size: u64,
114 opt: Options,
115 ) !void {
116 var header = Header.init(typeflag);
117 try self.setPath(&header, sub_path);
118 try header.setSize(size);
119 try header.setMtime(if (opt.mtime != 0) opt.mtime else self.mtimeNow());
120 if (opt.mode != 0)
121 try header.setMode(opt.mode);
122 if (typeflag == .symbolic_link)
123 header.setLinkname(link_name) catch |err| switch (err) {
124 error.NameTooLong => try self.writeExtendedHeader(.gnu_long_link, &.{link_name}),
125 else => return err,
126 };
127 try header.write(self.underlying_writer);
128 }
129
130 fn mtimeNow(self: *Self) u64 {
131 if (self.mtime_now == 0)
132 self.mtime_now = @intCast(std.time.timestamp());
133 return self.mtime_now;
134 }
135
136 fn entryMtime(entry: std.fs.Dir.Walker.WalkerEntry) !u64 {
137 const stat = try entry.dir.statFile(entry.basename);
138 return @intCast(@divFloor(stat.mtime, std.time.ns_per_s));
139 }
140
141 /// Writes path in posix header, if don't fit (in name+prefix; 100+155
142 /// bytes) writes it in gnu extended header.
143 fn setPath(self: *Self, header: *Header, sub_path: []const u8) !void {
144 header.setPath(self.prefix, sub_path) catch |err| switch (err) {
145 error.NameTooLong => {
146 // write extended header
147 const buffers: []const []const u8 = if (self.prefix.len == 0)
148 &.{sub_path}
149 else
150 &.{ self.prefix, "/", sub_path };
151 try self.writeExtendedHeader(.gnu_long_name, buffers);
152 },
153 else => return err,
154 };
155 }
156
157 /// Writes gnu extended header: gnu_long_name or gnu_long_link.
158 fn writeExtendedHeader(self: *Self, typeflag: Header.FileType, buffers: []const []const u8) !void {
159 var len: usize = 0;
160 for (buffers) |buf|
161 len += buf.len;
162
163 var header = Header.init(typeflag);
164 try header.setSize(len);
165 try header.write(self.underlying_writer);
166 for (buffers) |buf|
167 try self.underlying_writer.writeAll(buf);
168 try self.writePadding(len);
169 }
170
171 fn writePadding(self: *Self, bytes: u64) !void {
172 const pos: usize = @intCast(bytes % block_size);
173 if (pos == 0) return;
174 try self.underlying_writer.writeAll(empty_block[pos..]);
175 }
176
177 /// Tar should finish with two zero blocks, but 'reasonable system must
178 /// not assume that such a block exists when reading an archive' (from
179 /// reference). In practice it is safe to skip this finish.
180 pub fn finish(self: *Self) !void {
181 try self.underlying_writer.writeAll(&empty_block);
182 try self.underlying_writer.writeAll(&empty_block);
183 }
184 };
185}
186
187/// A struct that is exactly 512 bytes and matches tar file format. This is
188/// intended to be used for outputting tar files; for parsing there is
189/// `std.tar.Header`.
190const Header = extern struct {
191 // This struct was originally copied from
192 // https://github.com/mattnite/tar/blob/main/src/main.zig which is MIT
193 // licensed.
194 //
195 // The name, linkname, magic, uname, and gname are null-terminated character
196 // strings. All other fields are zero-filled octal numbers in ASCII. Each
197 // numeric field of width w contains w minus 1 digits, and a null.
198 // Reference: https://www.gnu.org/software/tar/manual/html_node/Standard.html
199 // POSIX header: byte offset
200 name: [100]u8 = [_]u8{0} ** 100, // 0
201 mode: [7:0]u8 = default_mode.file, // 100
202 uid: [7:0]u8 = [_:0]u8{0} ** 7, // unused 108
203 gid: [7:0]u8 = [_:0]u8{0} ** 7, // unused 116
204 size: [11:0]u8 = [_:0]u8{'0'} ** 11, // 124
205 mtime: [11:0]u8 = [_:0]u8{'0'} ** 11, // 136
206 checksum: [7:0]u8 = [_:0]u8{' '} ** 7, // 148
207 typeflag: FileType = .regular, // 156
208 linkname: [100]u8 = [_]u8{0} ** 100, // 157
209 magic: [6]u8 = [_]u8{ 'u', 's', 't', 'a', 'r', 0 }, // 257
210 version: [2]u8 = [_]u8{ '0', '0' }, // 263
211 uname: [32]u8 = [_]u8{0} ** 32, // unused 265
212 gname: [32]u8 = [_]u8{0} ** 32, // unused 297
213 devmajor: [7:0]u8 = [_:0]u8{0} ** 7, // unused 329
214 devminor: [7:0]u8 = [_:0]u8{0} ** 7, // unused 337
215 prefix: [155]u8 = [_]u8{0} ** 155, // 345
216 pad: [12]u8 = [_]u8{0} ** 12, // unused 500
217
218 pub const FileType = enum(u8) {
219 regular = '0',
220 symbolic_link = '2',
221 directory = '5',
222 gnu_long_name = 'L',
223 gnu_long_link = 'K',
224 };
225
226 const default_mode = struct {
227 const file = [_:0]u8{ '0', '0', '0', '0', '6', '6', '4' }; // 0o664
228 const dir = [_:0]u8{ '0', '0', '0', '0', '7', '7', '5' }; // 0o775
229 const sym_link = [_:0]u8{ '0', '0', '0', '0', '7', '7', '7' }; // 0o777
230 const other = [_:0]u8{ '0', '0', '0', '0', '0', '0', '0' }; // 0o000
231 };
232
233 pub fn init(typeflag: FileType) Header {
234 return .{
235 .typeflag = typeflag,
236 .mode = switch (typeflag) {
237 .directory => default_mode.dir,
238 .symbolic_link => default_mode.sym_link,
239 .regular => default_mode.file,
240 else => default_mode.other,
241 },
242 };
243 }
244
245 pub fn setSize(self: *Header, size: u64) !void {
246 try octal(&self.size, size);
247 }
248
249 fn octal(buf: []u8, value: u64) !void {
250 var remainder: u64 = value;
251 var pos: usize = buf.len;
252 while (remainder > 0 and pos > 0) {
253 pos -= 1;
254 const c: u8 = @as(u8, @intCast(remainder % 8)) + '0';
255 buf[pos] = c;
256 remainder /= 8;
257 if (pos == 0 and remainder > 0) return error.OctalOverflow;
258 }
259 }
260
261 pub fn setMode(self: *Header, mode: u32) !void {
262 try octal(&self.mode, mode);
263 }
264
265 // Integer number of seconds since January 1, 1970, 00:00 Coordinated Universal Time.
266 // mtime == 0 will use current time
267 pub fn setMtime(self: *Header, mtime: u64) !void {
268 try octal(&self.mtime, mtime);
269 }
270
271 pub fn updateChecksum(self: *Header) !void {
272 var checksum: usize = ' '; // other 7 self.checksum bytes are initialized to ' '
273 for (std.mem.asBytes(self)) |val|
274 checksum += val;
275 try octal(&self.checksum, checksum);
276 }
277
278 pub fn write(self: *Header, output_writer: anytype) !void {
279 try self.updateChecksum();
280 try output_writer.writeAll(std.mem.asBytes(self));
281 }
282
283 pub fn setLinkname(self: *Header, link: []const u8) !void {
284 if (link.len > self.linkname.len) return error.NameTooLong;
285 @memcpy(self.linkname[0..link.len], link);
286 }
287
288 pub fn setPath(self: *Header, prefix: []const u8, sub_path: []const u8) !void {
289 const max_prefix = self.prefix.len;
290 const max_name = self.name.len;
291 const sep = std.fs.path.sep_posix;
292
293 if (prefix.len + sub_path.len > max_name + max_prefix or prefix.len > max_prefix)
294 return error.NameTooLong;
295
296 // both fit into name
297 if (prefix.len > 0 and prefix.len + sub_path.len < max_name) {
298 @memcpy(self.name[0..prefix.len], prefix);
299 self.name[prefix.len] = sep;
300 @memcpy(self.name[prefix.len + 1 ..][0..sub_path.len], sub_path);
301 return;
302 }
303
304 // sub_path fits into name
305 // there is no prefix or prefix fits into prefix
306 if (sub_path.len <= max_name) {
307 @memcpy(self.name[0..sub_path.len], sub_path);
308 @memcpy(self.prefix[0..prefix.len], prefix);
309 return;
310 }
311
312 if (prefix.len > 0) {
313 @memcpy(self.prefix[0..prefix.len], prefix);
314 self.prefix[prefix.len] = sep;
315 }
316 const prefix_pos = if (prefix.len > 0) prefix.len + 1 else 0;
317
318 // add as much to prefix as you can, must split at /
319 const prefix_remaining = max_prefix - prefix_pos;
320 if (std.mem.lastIndexOf(u8, sub_path[0..@min(prefix_remaining, sub_path.len)], &.{'/'})) |sep_pos| {
321 @memcpy(self.prefix[prefix_pos..][0..sep_pos], sub_path[0..sep_pos]);
322 if ((sub_path.len - sep_pos - 1) > max_name) return error.NameTooLong;
323 @memcpy(self.name[0..][0 .. sub_path.len - sep_pos - 1], sub_path[sep_pos + 1 ..]);
324 return;
325 }
326
327 return error.NameTooLong;
328 }
329
330 comptime {
331 assert(@sizeOf(Header) == 512);
332 }
333
334 test setPath {
335 const cases = [_]struct {
336 in: []const []const u8,
337 out: []const []const u8,
338 }{
339 .{
340 .in = &.{ "", "123456789" },
341 .out = &.{ "", "123456789" },
342 },
343 // can fit into name
344 .{
345 .in = &.{ "prefix", "sub_path" },
346 .out = &.{ "", "prefix/sub_path" },
347 },
348 // no more both fits into name
349 .{
350 .in = &.{ "prefix", "0123456789/" ** 8 ++ "basename" },
351 .out = &.{ "prefix", "0123456789/" ** 8 ++ "basename" },
352 },
353 // put as much as you can into prefix the rest goes into name
354 .{
355 .in = &.{ "prefix", "0123456789/" ** 10 ++ "basename" },
356 .out = &.{ "prefix/" ++ "0123456789/" ** 9 ++ "0123456789", "basename" },
357 },
358
359 .{
360 .in = &.{ "prefix", "0123456789/" ** 15 ++ "basename" },
361 .out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/0123456789/basename" },
362 },
363 .{
364 .in = &.{ "prefix", "0123456789/" ** 21 ++ "basename" },
365 .out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/" ** 8 ++ "basename" },
366 },
367 .{
368 .in = &.{ "", "012345678/" ** 10 ++ "foo" },
369 .out = &.{ "012345678/" ** 9 ++ "012345678", "foo" },
370 },
371 };
372
373 for (cases) |case| {
374 var header = Header.init(.regular);
375 try header.setPath(case.in[0], case.in[1]);
376 try testing.expectEqualStrings(case.out[0], str(&header.prefix));
377 try testing.expectEqualStrings(case.out[1], str(&header.name));
378 }
379
380 const error_cases = [_]struct {
381 in: []const []const u8,
382 }{
383 // basename can't fit into name (106 characters)
384 .{ .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" } },
385 // cant fit into 255 + sep
386 .{ .in = &.{ "prefix", "0123456789/" ** 22 ++ "basename" } },
387 // can fit but sub_path can't be split (there is no separator)
388 .{ .in = &.{ "prefix", "0123456789" ** 10 ++ "a" } },
389 .{ .in = &.{ "prefix", "0123456789" ** 14 ++ "basename" } },
390 };
391
392 for (error_cases) |case| {
393 var header = Header.init(.regular);
394 try testing.expectError(
395 error.NameTooLong,
396 header.setPath(case.in[0], case.in[1]),
397 );
398 }
399 }
400
401 // Breaks string on first null character.
402 fn str(s: []const u8) []const u8 {
403 for (s, 0..) |c, i| {
404 if (c == 0) return s[0..i];
405 }
406 return s;
407 }
408};
409
410test {
411 _ = Header;
412}
413
414test "write files" {
415 const files = [_]struct {
416 path: []const u8,
417 content: []const u8,
418 }{
419 .{ .path = "foo", .content = "bar" },
420 .{ .path = "a12345678/" ** 10 ++ "foo", .content = "a" ** 511 },
421 .{ .path = "b12345678/" ** 24 ++ "foo", .content = "b" ** 512 },
422 .{ .path = "c12345678/" ** 25 ++ "foo", .content = "c" ** 513 },
423 .{ .path = "d12345678/" ** 51 ++ "foo", .content = "d" ** 1025 },
424 .{ .path = "e123456789" ** 11, .content = "e" },
425 };
426
427 var file_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
428 var link_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
429
430 // with root
431 {
432 const root = "root";
433
434 var output = std.ArrayList(u8).init(testing.allocator);
435 defer output.deinit();
436 var wrt = writer(output.writer());
437 try wrt.setRoot(root);
438 for (files) |file|
439 try wrt.writeFileBytes(file.path, file.content, .{});
440
441 var input = std.io.fixedBufferStream(output.items);
442 var iter = std.tar.iterator(
443 input.reader(),
444 .{ .file_name_buffer = &file_name_buffer, .link_name_buffer = &link_name_buffer },
445 );
446
447 // first entry is directory with prefix
448 {
449 const actual = (try iter.next()).?;
450 try testing.expectEqualStrings(root, actual.name);
451 try testing.expectEqual(std.tar.FileKind.directory, actual.kind);
452 }
453
454 var i: usize = 0;
455 while (try iter.next()) |actual| {
456 defer i += 1;
457 const expected = files[i];
458 try testing.expectEqualStrings(root, actual.name[0..root.len]);
459 try testing.expectEqual('/', actual.name[root.len..][0]);
460 try testing.expectEqualStrings(expected.path, actual.name[root.len + 1 ..]);
461
462 var content = std.ArrayList(u8).init(testing.allocator);
463 defer content.deinit();
464 try actual.writeAll(content.writer());
465 try testing.expectEqualSlices(u8, expected.content, content.items);
466 }
467 }
468 // without root
469 {
470 var output = std.ArrayList(u8).init(testing.allocator);
471 defer output.deinit();
472 var wrt = writer(output.writer());
473 for (files) |file| {
474 var content = std.io.fixedBufferStream(file.content);
475 try wrt.writeFileStream(file.path, file.content.len, content.reader(), .{});
476 }
477
478 var input = std.io.fixedBufferStream(output.items);
479 var iter = std.tar.iterator(
480 input.reader(),
481 .{ .file_name_buffer = &file_name_buffer, .link_name_buffer = &link_name_buffer },
482 );
483
484 var i: usize = 0;
485 while (try iter.next()) |actual| {
486 defer i += 1;
487 const expected = files[i];
488 try testing.expectEqualStrings(expected.path, actual.name);
489
490 var content = std.ArrayList(u8).init(testing.allocator);
491 defer content.deinit();
492 try actual.writeAll(content.writer());
493 try testing.expectEqualSlices(u8, expected.content, content.items);
494 }
495 try wrt.finish();
496 }
497}
src/Compilation.zig+4-29
......@@ -4071,7 +4071,8 @@ fn docsCopyModule(comp: *Compilation, module: *Package.Module, name: []const u8,
40714071 var walker = try mod_dir.walk(comp.gpa);
40724072 defer walker.deinit();
40734073
4074 const padding_buffer = [1]u8{0} ** 512;
4074 var archiver = std.tar.writer(tar_file.writer().any());
4075 archiver.prefix = name;
40754076
40764077 while (try walker.next()) |entry| {
40774078 switch (entry.kind) {
......@@ -4082,43 +4083,17 @@ fn docsCopyModule(comp: *Compilation, module: *Package.Module, name: []const u8,
40824083 },
40834084 else => continue,
40844085 }
4085
40864086 var file = mod_dir.openFile(entry.path, .{}) catch |err| {
40874087 return comp.lockAndSetMiscFailure(.docs_copy, "unable to open '{}{s}': {s}", .{
40884088 root, entry.path, @errorName(err),
40894089 });
40904090 };
40914091 defer file.close();
4092
4093 const stat = file.stat() catch |err| {
4094 return comp.lockAndSetMiscFailure(.docs_copy, "unable to stat '{}{s}': {s}", .{
4092 archiver.writeFile(entry.path, file) catch |err| {
4093 return comp.lockAndSetMiscFailure(.docs_copy, "unable to archive '{}{s}': {s}", .{
40954094 root, entry.path, @errorName(err),
40964095 });
40974096 };
4098
4099 var file_header = std.tar.output.Header.init();
4100 file_header.typeflag = .regular;
4101 try file_header.setPath(name, entry.path);
4102 try file_header.setSize(stat.size);
4103 try file_header.updateChecksum();
4104
4105 const header_bytes = std.mem.asBytes(&file_header);
4106 const padding = p: {
4107 const remainder: u16 = @intCast(stat.size % 512);
4108 const n = if (remainder > 0) 512 - remainder else 0;
4109 break :p padding_buffer[0..n];
4110 };
4111
4112 var header_and_trailer: [2]std.posix.iovec_const = .{
4113 .{ .base = header_bytes.ptr, .len = header_bytes.len },
4114 .{ .base = padding.ptr, .len = padding.len },
4115 };
4116
4117 try tar_file.writeFileAll(file, .{
4118 .in_len = stat.size,
4119 .headers_and_trailers = &header_and_trailer,
4120 .header_count = 1,
4121 });
41224097 }
41234098}
41244099