authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-01-13 18:44:14-08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2024-01-13 18:44:14-08:00
logd55d1e32b65dd829cec17b5c5d65db10608d097c
tree2fa33f274b5460dfd024b091dd6e4152a1942190
parent7916cf6f83650517f39a9e6aec23ba53d176a2ba
parent3f809cbe7ded23a236f98eb1809fc7cda65021e1
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #18261 from ianic/tar_tests

std:tar Copy Go tar test suite and make them pass

31 files changed, 913 insertions(+), 188 deletions(-)

build.zig+2
...@@ -165,6 +165,8 @@ pub fn build(b: *std.Build) !void {...@@ -165,6 +165,8 @@ pub fn build(b: *std.Build) !void {
165 ".xz",165 ".xz",
166 // exclude files from lib/std/tz/166 // exclude files from lib/std/tz/
167 ".tzif",167 ".tzif",
168 // exclude files from lib/std/tar/testdata
169 ".tar",
168 // others170 // others
169 "README.md",171 "README.md",
170 },172 },
lib/std/tar.zig+544-188
...@@ -1,3 +1,23 @@...@@ -1,3 +1,23 @@
1/// Tar archive is single ordinary file which can contain many files (or
2/// directories, symlinks, ...). It's build by series of blocks each size of 512
3/// bytes. First block of each entry is header which defines type, name, size
4/// permissions and other attributes. Header is followed by series of blocks of
5/// file content, if any that entry has content. Content is padded to the block
6/// size, so next header always starts at block boundary.
7///
8/// This simple format is extended by GNU and POSIX pax extensions to support
9/// file names longer than 256 bytes and additional attributes.
10///
11/// This is not comprehensive tar parser. Here we are only file types needed to
12/// support Zig package manager; normal file, directory, symbolic link. And
13/// subset of attributes: name, size, permissions.
14///
15/// GNU tar reference: https://www.gnu.org/software/tar/manual/html_node/Standard.html
16/// pax reference: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13
17///
18const std = @import("std.zig");
19const assert = std.debug.assert;
20
1pub const Options = struct {21pub const Options = struct {
2 /// Number of directory levels to skip when extracting files.22 /// Number of directory levels to skip when extracting files.
3 strip_components: u32 = 0,23 strip_components: u32 = 0,
...@@ -37,7 +57,7 @@ pub const Options = struct {...@@ -37,7 +57,7 @@ pub const Options = struct {
37 },57 },
38 unsupported_file_type: struct {58 unsupported_file_type: struct {
39 file_name: []const u8,59 file_name: []const u8,
40 file_type: Header.FileType,60 file_type: Header.Kind,
41 },61 },
42 };62 };
4363
...@@ -63,9 +83,13 @@ pub const Options = struct {...@@ -63,9 +83,13 @@ pub const Options = struct {
63};83};
6484
65pub const Header = struct {85pub const Header = struct {
66 bytes: *const [512]u8,86 const SIZE = 512;
87 const MAX_NAME_SIZE = 100 + 1 + 155; // name(100) + separator(1) + prefix(155)
88 const LINK_NAME_SIZE = 100;
6789
68 pub const FileType = enum(u8) {90 bytes: *const [SIZE]u8,
91
92 pub const Kind = enum(u8) {
69 normal_alias = 0,93 normal_alias = 0,
70 normal = '0',94 normal = '0',
71 hard_link = '1',95 hard_link = '1',
...@@ -77,103 +101,424 @@ pub const Header = struct {...@@ -77,103 +101,424 @@ pub const Header = struct {
77 contiguous = '7',101 contiguous = '7',
78 global_extended_header = 'g',102 global_extended_header = 'g',
79 extended_header = 'x',103 extended_header = 'x',
104 // Types 'L' and 'K' are used by the GNU format for a meta file
105 // used to store the path or link name for the next file.
106 gnu_long_name = 'L',
107 gnu_long_link = 'K',
108 gnu_sparse = 'S',
109 solaris_extended_header = 'X',
80 _,110 _,
81 };111 };
82112
83 pub fn fileSize(header: Header) !u64 {
84 const raw = header.bytes[124..][0..12];
85 const ltrimmed = std.mem.trimLeft(u8, raw, "0 ");
86 const rtrimmed = std.mem.trimRight(u8, ltrimmed, " \x00");
87 if (rtrimmed.len == 0) return 0;
88 return std.fmt.parseInt(u64, rtrimmed, 8);
89 }
90
91 pub fn is_ustar(header: Header) bool {
92 return std.mem.eql(u8, header.bytes[257..][0..6], "ustar\x00");
93 }
94
95 /// Includes prefix concatenated, if any.113 /// Includes prefix concatenated, if any.
96 /// Return value may point into Header buffer, or might point into the
97 /// argument buffer.
98 /// TODO: check against "../" and other nefarious things114 /// TODO: check against "../" and other nefarious things
99 pub fn fullFileName(header: Header, buffer: *[std.fs.MAX_PATH_BYTES]u8) ![]const u8 {115 pub fn fullName(header: Header, buffer: *[MAX_NAME_SIZE]u8) ![]const u8 {
100 const n = name(header);116 const n = name(header);
101 if (!is_ustar(header))
102 return n;
103 const p = prefix(header);117 const p = prefix(header);
104 if (p.len == 0)118 if (!is_ustar(header) or p.len == 0) {
105 return n;119 @memcpy(buffer[0..n.len], n);
120 return buffer[0..n.len];
121 }
106 @memcpy(buffer[0..p.len], p);122 @memcpy(buffer[0..p.len], p);
107 buffer[p.len] = '/';123 buffer[p.len] = '/';
108 @memcpy(buffer[p.len + 1 ..][0..n.len], n);124 @memcpy(buffer[p.len + 1 ..][0..n.len], n);
109 return buffer[0 .. p.len + 1 + n.len];125 return buffer[0 .. p.len + 1 + n.len];
110 }126 }
111127
128 pub fn linkName(header: Header, buffer: *[LINK_NAME_SIZE]u8) []const u8 {
129 const link_name = header.str(157, 100);
130 if (link_name.len == 0) {
131 return buffer[0..0];
132 }
133 const buf = buffer[0..link_name.len];
134 @memcpy(buf, link_name);
135 return buf;
136 }
137
112 pub fn name(header: Header) []const u8 {138 pub fn name(header: Header) []const u8 {
113 return str(header, 0, 0 + 100);139 return header.str(0, 100);
140 }
141
142 pub fn mode(header: Header) !u32 {
143 return @intCast(try header.numeric(100, 8));
114 }144 }
115145
116 pub fn linkName(header: Header) []const u8 {146 pub fn size(header: Header) !u64 {
117 return str(header, 157, 157 + 100);147 return header.numeric(124, 12);
148 }
149
150 pub fn chksum(header: Header) !u64 {
151 return header.octal(148, 8);
152 }
153
154 pub fn is_ustar(header: Header) bool {
155 const magic = header.bytes[257..][0..6];
156 return std.mem.eql(u8, magic[0..5], "ustar") and (magic[5] == 0 or magic[5] == ' ');
118 }157 }
119158
120 pub fn prefix(header: Header) []const u8 {159 pub fn prefix(header: Header) []const u8 {
121 return str(header, 345, 345 + 155);160 return header.str(345, 155);
122 }161 }
123162
124 pub fn fileType(header: Header) FileType {163 pub fn kind(header: Header) Kind {
125 const result: FileType = @enumFromInt(header.bytes[156]);164 const result: Kind = @enumFromInt(header.bytes[156]);
126 if (result == .normal_alias) return .normal;165 if (result == .normal_alias) return .normal;
127 return result;166 return result;
128 }167 }
129168
130 fn str(header: Header, start: usize, end: usize) []const u8 {169 fn str(header: Header, start: usize, len: usize) []const u8 {
131 var i: usize = start;170 return nullStr(header.bytes[start .. start + len]);
132 while (i < end) : (i += 1) {171 }
133 if (header.bytes[i] == 0) break;172
173 fn numeric(header: Header, start: usize, len: usize) !u64 {
174 const raw = header.bytes[start..][0..len];
175 // If the leading byte is 0xff (255), all the bytes of the field
176 // (including the leading byte) are concatenated in big-endian order,
177 // with the result being a negative number expressed in two’s
178 // complement form.
179 if (raw[0] == 0xff) return error.TarNumericValueNegative;
180 // If the leading byte is 0x80 (128), the non-leading bytes of the
181 // field are concatenated in big-endian order.
182 if (raw[0] == 0x80) {
183 if (raw[1] + raw[2] + raw[3] != 0) return error.TarNumericValueTooBig;
184 return std.mem.readInt(u64, raw[4..12], .big);
134 }185 }
135 return header.bytes[start..i];186 return try header.octal(start, len);
136 }187 }
137};
138188
139const Buffer = struct {189 fn octal(header: Header, start: usize, len: usize) !u64 {
140 buffer: [512 * 8]u8 = undefined,190 const raw = header.bytes[start..][0..len];
141 start: usize = 0,191 // Zero-filled octal number in ASCII. Each numeric field of width w
142 end: usize = 0,192 // contains w minus 1 digits, and a null
193 const ltrimmed = std.mem.trimLeft(u8, raw, "0 ");
194 const rtrimmed = std.mem.trimRight(u8, ltrimmed, " \x00");
195 if (rtrimmed.len == 0) return 0;
196 return std.fmt.parseInt(u64, rtrimmed, 8) catch return error.TarHeader;
197 }
143198
144 pub fn readChunk(b: *Buffer, reader: anytype, count: usize) ![]const u8 {199 const Chksums = struct {
145 b.ensureCapacity(1024);200 unsigned: u64,
201 signed: i64,
202 };
146203
147 const ask = @min(b.buffer.len - b.end, count -| (b.end - b.start));204 // Sum of all bytes in the header block. The chksum field is treated as if
148 b.end += try reader.readAtLeast(b.buffer[b.end..], ask);205 // it were filled with spaces (ASCII 32).
206 fn computeChksum(header: Header) Chksums {
207 var cs: Chksums = .{ .signed = 0, .unsigned = 0 };
208 for (header.bytes, 0..) |v, i| {
209 const b = if (148 <= i and i < 156) 32 else v; // Treating chksum bytes as spaces.
210 cs.unsigned += b;
211 cs.signed += @as(i8, @bitCast(b));
212 }
213 return cs;
214 }
149215
150 return b.buffer[b.start..b.end];216 // Checks calculated chksum with value of chksum field.
217 // Returns error or valid chksum value.
218 // Zero value indicates empty block.
219 pub fn checkChksum(header: Header) !u64 {
220 const field = try header.chksum();
221 const cs = header.computeChksum();
222 if (field == 0 and cs.unsigned == 256) return 0;
223 if (field != cs.unsigned and field != cs.signed) return error.TarHeaderChksum;
224 return field;
151 }225 }
226};
152227
153 pub fn advance(b: *Buffer, count: usize) void {228// Breaks string on first null character.
154 b.start += count;229fn nullStr(str: []const u8) []const u8 {
155 assert(b.start <= b.end);230 for (str, 0..) |c, i| {
231 if (c == 0) return str[0..i];
156 }232 }
233 return str;
234}
157235
158 pub fn skip(b: *Buffer, reader: anytype, count: usize) !void {236/// Iterates over files in tar archive.
159 if (b.start + count > b.end) {237/// `next` returns each file in `reader` tar archive.
160 try reader.skipBytes(b.start + count - b.end, .{});238pub fn iterator(reader: anytype, diagnostics: ?*Options.Diagnostics) Iterator(@TypeOf(reader)) {
161 b.start = b.end;239 return .{
162 } else {240 .reader = reader,
163 b.advance(count);241 .diagnostics = diagnostics,
242 };
243}
244
245fn Iterator(comptime ReaderType: type) type {
246 return struct {
247 reader: ReaderType,
248 diagnostics: ?*Options.Diagnostics,
249
250 // buffers for heeader and file attributes
251 header_buffer: [Header.SIZE]u8 = undefined,
252 file_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined,
253 link_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined,
254
255 // bytes of padding to the end of the block
256 padding: usize = 0,
257 // current tar file
258 file: File = undefined,
259
260 pub const File = struct {
261 name: []const u8, // name of file, symlink or directory
262 link_name: []const u8, // target name of symlink
263 size: u64, // size of the file in bytes
264 mode: u32,
265 kind: Header.Kind,
266
267 reader: ReaderType,
268
269 // Writes file content to writer.
270 pub fn write(self: File, writer: anytype) !void {
271 var buffer: [4096]u8 = undefined;
272
273 var n: u64 = 0;
274 while (n < self.size) {
275 const buf = buffer[0..@min(buffer.len, self.size - n)];
276 try self.reader.readNoEof(buf);
277 try writer.writeAll(buf);
278 n += buf.len;
279 }
280 }
281
282 // Skips file content. Advances reader.
283 pub fn skip(self: File) !void {
284 try self.reader.skipBytes(self.size, .{});
285 }
286 };
287
288 const Self = @This();
289
290 fn readHeader(self: *Self) !?Header {
291 if (self.padding > 0) {
292 try self.reader.skipBytes(self.padding, .{});
293 }
294 const n = try self.reader.readAll(&self.header_buffer);
295 if (n == 0) return null;
296 if (n < Header.SIZE) return error.UnexpectedEndOfStream;
297 const header = Header{ .bytes = self.header_buffer[0..Header.SIZE] };
298 if (try header.checkChksum() == 0) return null;
299 return header;
164 }300 }
165 }
166301
167 inline fn ensureCapacity(b: *Buffer, count: usize) void {302 inline fn readString(self: *Self, size: usize, buffer: []u8) ![]const u8 {
168 if (b.buffer.len - b.start < count) {303 assert(buffer.len >= size);
169 const dest_end = b.end - b.start;304 const buf = buffer[0..size];
170 @memcpy(b.buffer[0..dest_end], b.buffer[b.start..b.end]);305 try self.reader.readNoEof(buf);
171 b.end = dest_end;306 return nullStr(buf);
172 b.start = 0;
173 }307 }
174 }308
309 inline fn initFile(self: *Self) void {
310 self.file = File{
311 .name = self.file_name_buffer[0..0],
312 .link_name = self.link_name_buffer[0..0],
313 .size = 0,
314 .kind = .normal,
315 .mode = 0,
316 .reader = self.reader,
317 };
318 }
319
320 // Number of padding bytes in the last file block.
321 inline fn blockPadding(size: u64) usize {
322 const block_rounded = std.mem.alignForward(u64, size, Header.SIZE); // size rounded to te block boundary
323 return @intCast(block_rounded - size);
324 }
325
326 /// Iterates through the tar archive as if it is a series of files.
327 /// Internally, the tar format often uses entries (header with optional
328 /// content) to add meta data that describes the next file. These
329 /// entries should not normally be visible to the outside. As such, this
330 /// loop iterates through one or more entries until it collects a all
331 /// file attributes.
332 pub fn next(self: *Self) !?File {
333 self.initFile();
334
335 while (try self.readHeader()) |header| {
336 const kind = header.kind();
337 const size: u64 = try header.size();
338 self.padding = blockPadding(size);
339
340 switch (kind) {
341 // File types to retrun upstream
342 .directory, .normal, .symbolic_link => {
343 self.file.kind = kind;
344 self.file.mode = try header.mode();
345
346 // set file attributes if not already set by prefix/extended headers
347 if (self.file.size == 0) {
348 self.file.size = size;
349 }
350 if (self.file.link_name.len == 0) {
351 self.file.link_name = header.linkName(self.link_name_buffer[0..Header.LINK_NAME_SIZE]);
352 }
353 if (self.file.name.len == 0) {
354 self.file.name = try header.fullName(self.file_name_buffer[0..Header.MAX_NAME_SIZE]);
355 }
356
357 self.padding = blockPadding(self.file.size);
358 return self.file;
359 },
360 // Prefix header types
361 .gnu_long_name => {
362 self.file.name = try self.readString(@intCast(size), &self.file_name_buffer);
363 },
364 .gnu_long_link => {
365 self.file.link_name = try self.readString(@intCast(size), &self.link_name_buffer);
366 },
367 .extended_header => {
368 // Use just attributes from last extended header.
369 self.initFile();
370
371 var rdr = paxIterator(self.reader, @intCast(size));
372 while (try rdr.next()) |attr| {
373 switch (attr.kind) {
374 .path => {
375 self.file.name = try attr.value(&self.file_name_buffer);
376 },
377 .linkpath => {
378 self.file.link_name = try attr.value(&self.link_name_buffer);
379 },
380 .size => {
381 var buf: [64]u8 = undefined;
382 self.file.size = try std.fmt.parseInt(u64, try attr.value(&buf), 10);
383 },
384 }
385 }
386 },
387 // Ignored header type
388 .global_extended_header => {
389 self.reader.skipBytes(size, .{}) catch return error.TarHeadersTooBig;
390 },
391 // All other are unsupported header types
392 else => {
393 const d = self.diagnostics orelse return error.TarUnsupportedHeader;
394 try d.errors.append(d.allocator, .{ .unsupported_file_type = .{
395 .file_name = try d.allocator.dupe(u8, header.name()),
396 .file_type = kind,
397 } });
398 if (kind == .gnu_sparse) {
399 try self.skipGnuSparseExtendedHeaders(header);
400 }
401 self.reader.skipBytes(size, .{}) catch return error.TarHeadersTooBig;
402 },
403 }
404 }
405 return null;
406 }
407
408 fn skipGnuSparseExtendedHeaders(self: *Self, header: Header) !void {
409 var is_extended = header.bytes[482] > 0;
410 while (is_extended) {
411 var buf: [Header.SIZE]u8 = undefined;
412 const n = try self.reader.readAll(&buf);
413 if (n < Header.SIZE) return error.UnexpectedEndOfStream;
414 is_extended = buf[504] > 0;
415 }
416 }
417 };
418}
419
420/// Pax attributes iterator.
421/// Size is length of pax extended header in reader.
422fn paxIterator(reader: anytype, size: usize) PaxIterator(@TypeOf(reader)) {
423 return PaxIterator(@TypeOf(reader)){
424 .reader = reader,
425 .size = size,
426 };
427}
428
429const PaxAttributeKind = enum {
430 path,
431 linkpath,
432 size,
175};433};
176434
435fn PaxIterator(comptime ReaderType: type) type {
436 return struct {
437 size: usize, // cumulative size of all pax attributes
438 reader: ReaderType,
439 // scratch buffer used for reading attribute length and keyword
440 scratch: [128]u8 = undefined,
441
442 const Self = @This();
443
444 const Attribute = struct {
445 kind: PaxAttributeKind,
446 len: usize, // length of the attribute value
447 reader: ReaderType, // reader positioned at value start
448
449 // Copies pax attribute value into destination buffer.
450 // Must be called with destination buffer of size at least Attribute.len.
451 pub fn value(self: Attribute, dst: []u8) ![]const u8 {
452 assert(self.len <= dst.len);
453 const buf = dst[0..self.len];
454 const n = try self.reader.readAll(buf);
455 if (n < self.len) return error.UnexpectedEndOfStream;
456 try validateAttributeEnding(self.reader);
457 if (hasNull(buf)) return error.PaxNullInValue;
458 return buf;
459 }
460 };
461
462 // Iterates over pax attributes. Returns known only known attributes.
463 // Caller has to call value in Attribute, to advance reader across value.
464 pub fn next(self: *Self) !?Attribute {
465 // Pax extended header consists of one or more attributes, each constructed as follows:
466 // "%d %s=%s\n", <length>, <keyword>, <value>
467 while (self.size > 0) {
468 const length_buf = try self.readUntil(' ');
469 const length = try std.fmt.parseInt(usize, length_buf, 10); // record length in bytes
470
471 const keyword = try self.readUntil('=');
472 if (hasNull(keyword)) return error.PaxNullInKeyword;
473
474 // calculate value_len
475 const value_start = length_buf.len + keyword.len + 2; // 2 separators
476 if (length < value_start + 1 or self.size < length) return error.UnexpectedEndOfStream;
477 const value_len = length - value_start - 1; // \n separator at end
478 self.size -= length;
479
480 const kind: PaxAttributeKind = if (eql(keyword, "path"))
481 .path
482 else if (eql(keyword, "linkpath"))
483 .linkpath
484 else if (eql(keyword, "size"))
485 .size
486 else {
487 try self.reader.skipBytes(value_len, .{});
488 try validateAttributeEnding(self.reader);
489 continue;
490 };
491 return Attribute{
492 .kind = kind,
493 .len = value_len,
494 .reader = self.reader,
495 };
496 }
497
498 return null;
499 }
500
501 inline fn readUntil(self: *Self, delimiter: u8) ![]const u8 {
502 var fbs = std.io.fixedBufferStream(&self.scratch);
503 try self.reader.streamUntilDelimiter(fbs.writer(), delimiter, null);
504 return fbs.getWritten();
505 }
506
507 inline fn eql(a: []const u8, b: []const u8) bool {
508 return std.mem.eql(u8, a, b);
509 }
510
511 inline fn hasNull(str: []const u8) bool {
512 return (std.mem.indexOfScalar(u8, str, 0)) != null;
513 }
514
515 // Checks that each record ends with new line.
516 inline fn validateAttributeEnding(reader: ReaderType) !void {
517 if (try reader.readByte() != '\n') return error.PaxInvalidAttributeEnd;
518 }
519 };
520}
521
177pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !void {522pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !void {
178 switch (options.mode_mode) {523 switch (options.mode_mode) {
179 .ignore => {},524 .ignore => {},
...@@ -186,39 +531,21 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi...@@ -186,39 +531,21 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi
186 @panic("TODO: unimplemented: tar ModeMode.executable_bit_only");531 @panic("TODO: unimplemented: tar ModeMode.executable_bit_only");
187 },532 },
188 }533 }
189 var file_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;534
190 var file_name_override_len: usize = 0;535 var iter = iterator(reader, options.diagnostics);
191 var buffer: Buffer = .{};536 while (try iter.next()) |file| {
192 header: while (true) {537 switch (file.kind) {
193 const chunk = try buffer.readChunk(reader, 1024);
194 switch (chunk.len) {
195 0 => return,
196 1...511 => return error.UnexpectedEndOfStream,
197 else => {},
198 }
199 buffer.advance(512);
200
201 const header: Header = .{ .bytes = chunk[0..512] };
202 const file_size = try header.fileSize();
203 const rounded_file_size = std.mem.alignForward(u64, file_size, 512);
204 const pad_len: usize = @intCast(rounded_file_size - file_size);
205 const unstripped_file_name = if (file_name_override_len > 0)
206 file_name_buffer[0..file_name_override_len]
207 else
208 try header.fullFileName(&file_name_buffer);
209 file_name_override_len = 0;
210 switch (header.fileType()) {
211 .directory => {538 .directory => {
212 const file_name = try stripComponents(unstripped_file_name, options.strip_components);539 const file_name = try stripComponents(file.name, options.strip_components);
213 if (file_name.len != 0 and !options.exclude_empty_directories) {540 if (file_name.len != 0 and !options.exclude_empty_directories) {
214 try dir.makePath(file_name);541 try dir.makePath(file_name);
215 }542 }
216 },543 },
217 .normal => {544 .normal => {
218 if (file_size == 0 and unstripped_file_name.len == 0) return;545 if (file.size == 0 and file.name.len == 0) return;
219 const file_name = try stripComponents(unstripped_file_name, options.strip_components);546 const file_name = try stripComponents(file.name, options.strip_components);
220547
221 const file = dir.createFile(file_name, .{}) catch |err| switch (err) {548 const fs_file = dir.createFile(file_name, .{}) catch |err| switch (err) {
222 error.FileNotFound => again: {549 error.FileNotFound => again: {
223 const code = code: {550 const code = code: {
224 if (std.fs.path.dirname(file_name)) |dir_name| {551 if (std.fs.path.dirname(file_name)) |dir_name| {
...@@ -238,70 +565,19 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi...@@ -238,70 +565,19 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi
238 },565 },
239 else => |e| return e,566 else => |e| return e,
240 };567 };
241 defer if (file) |f| f.close();568 defer if (fs_file) |f| f.close();
242
243 var file_off: usize = 0;
244 while (true) {
245 const temp = try buffer.readChunk(reader, @intCast(rounded_file_size + 512 - file_off));
246 if (temp.len == 0) return error.UnexpectedEndOfStream;
247 const slice = temp[0..@intCast(@min(file_size - file_off, temp.len))];
248 if (file) |f| try f.writeAll(slice);
249
250 file_off += slice.len;
251 buffer.advance(slice.len);
252 if (file_off >= file_size) {
253 buffer.advance(pad_len);
254 continue :header;
255 }
256 }
257 },
258 .extended_header => {
259 if (file_size == 0) {
260 buffer.advance(@intCast(rounded_file_size));
261 continue;
262 }
263569
264 const chunk_size: usize = @intCast(rounded_file_size + 512);570 if (fs_file) |f| {
265 var data_off: usize = 0;571 try file.write(f);
266 file_name_override_len = while (data_off < file_size) {572 } else {
267 const slice = try buffer.readChunk(reader, chunk_size - data_off);573 try file.skip();
268 if (slice.len == 0) return error.UnexpectedEndOfStream;
269 const remaining_size: usize = @intCast(file_size - data_off);
270 const attr_info = try parsePaxAttribute(slice[0..@min(remaining_size, slice.len)], remaining_size);
271
272 if (std.mem.eql(u8, attr_info.key, "path")) {
273 if (attr_info.value_len > file_name_buffer.len) return error.NameTooLong;
274 buffer.advance(attr_info.value_off);
275 data_off += attr_info.value_off;
276 break attr_info.value_len;
277 }
278
279 try buffer.skip(reader, attr_info.size);
280 data_off += attr_info.size;
281 } else 0;
282
283 var i: usize = 0;
284 while (i < file_name_override_len) {
285 const slice = try buffer.readChunk(reader, chunk_size - data_off - i);
286 if (slice.len == 0) return error.UnexpectedEndOfStream;
287 const copy_size: usize = @intCast(@min(file_name_override_len - i, slice.len));
288 @memcpy(file_name_buffer[i .. i + copy_size], slice[0..copy_size]);
289 buffer.advance(copy_size);
290 i += copy_size;
291 }574 }
292
293 try buffer.skip(reader, @intCast(rounded_file_size - data_off - file_name_override_len));
294 continue :header;
295 },575 },
296 .global_extended_header => {
297 buffer.skip(reader, @intCast(rounded_file_size)) catch return error.TarHeadersTooBig;
298 },
299 .hard_link => return error.TarUnsupportedFileType,
300 .symbolic_link => {576 .symbolic_link => {
301 // The file system path of the symbolic link.577 // The file system path of the symbolic link.
302 const file_name = try stripComponents(unstripped_file_name, options.strip_components);578 const file_name = try stripComponents(file.name, options.strip_components);
303 // The data inside the symbolic link.579 // The data inside the symbolic link.
304 const link_name = header.linkName();580 const link_name = file.link_name;
305581
306 dir.symLink(link_name, file_name, .{}) catch |err| again: {582 dir.symLink(link_name, file_name, .{}) catch |err| again: {
307 const code = code: {583 const code = code: {
...@@ -323,13 +599,7 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi...@@ -323,13 +599,7 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi
323 } });599 } });
324 };600 };
325 },601 },
326 else => |file_type| {602 else => unreachable,
327 const d = options.diagnostics orelse return error.TarUnsupportedFileType;
328 try d.errors.append(d.allocator, .{ .unsupported_file_type = .{
329 .file_name = try d.allocator.dupe(u8, unstripped_file_name),
330 .file_type = file_type,
331 } });
332 },
333 }603 }
334 }604 }
335}605}
...@@ -347,51 +617,137 @@ fn stripComponents(path: []const u8, count: u32) ![]const u8 {...@@ -347,51 +617,137 @@ fn stripComponents(path: []const u8, count: u32) ![]const u8 {
347 return path[i..];617 return path[i..];
348}618}
349619
350test stripComponents {620test "tar stripComponents" {
351 const expectEqualStrings = std.testing.expectEqualStrings;621 const expectEqualStrings = std.testing.expectEqualStrings;
352 try expectEqualStrings("a/b/c", try stripComponents("a/b/c", 0));622 try expectEqualStrings("a/b/c", try stripComponents("a/b/c", 0));
353 try expectEqualStrings("b/c", try stripComponents("a/b/c", 1));623 try expectEqualStrings("b/c", try stripComponents("a/b/c", 1));
354 try expectEqualStrings("c", try stripComponents("a/b/c", 2));624 try expectEqualStrings("c", try stripComponents("a/b/c", 2));
355}625}
356626
357const PaxAttributeInfo = struct {627test "tar PaxIterator" {
358 size: usize,628 const Attr = struct {
359 key: []const u8,629 kind: PaxAttributeKind,
360 value_off: usize,630 value: []const u8 = undefined,
361 value_len: usize,631 err: ?anyerror = null,
362};632 };
633 const cases = [_]struct {
634 data: []const u8,
635 attrs: []const Attr,
636 err: ?anyerror = null,
637 }{
638 .{ // valid but unknown keys
639 .data =
640 \\30 mtime=1350244992.023960108
641 \\6 k=1
642 \\13 key1=val1
643 \\10 a=name
644 \\9 a=name
645 \\
646 ,
647 .attrs = &[_]Attr{},
648 },
649 .{ // mix of known and unknown keys
650 .data =
651 \\6 k=1
652 \\13 path=name
653 \\17 linkpath=link
654 \\13 key1=val1
655 \\12 size=123
656 \\13 key2=val2
657 \\
658 ,
659 .attrs = &[_]Attr{
660 .{ .kind = .path, .value = "name" },
661 .{ .kind = .linkpath, .value = "link" },
662 .{ .kind = .size, .value = "123" },
663 },
664 },
665 .{ // too short size of the second key-value pair
666 .data =
667 \\13 path=name
668 \\10 linkpath=value
669 \\
670 ,
671 .attrs = &[_]Attr{
672 .{ .kind = .path, .value = "name" },
673 },
674 .err = error.UnexpectedEndOfStream,
675 },
676 .{ // too long size of the second key-value pair
677 .data =
678 \\13 path=name
679 \\6 k=1
680 \\19 linkpath=value
681 \\
682 ,
683 .attrs = &[_]Attr{
684 .{ .kind = .path, .value = "name" },
685 },
686 .err = error.UnexpectedEndOfStream,
687 },
363688
364fn parsePaxAttribute(data: []const u8, max_size: usize) !PaxAttributeInfo {689 .{ // too long size of the second key-value pair
365 const pos_space = std.mem.indexOfScalar(u8, data, ' ') orelse return error.InvalidPaxAttribute;690 .data =
366 const pos_equals = std.mem.indexOfScalarPos(u8, data, pos_space, '=') orelse return error.InvalidPaxAttribute;691 \\13 path=name
367 const kv_size = try std.fmt.parseInt(usize, data[0..pos_space], 10);692 \\19 linkpath=value
368 if (kv_size > max_size) {693 \\6 k=1
369 return error.InvalidPaxAttribute;694 \\
370 }695 ,
371 return .{696 .attrs = &[_]Attr{
372 .size = kv_size,697 .{ .kind = .path, .value = "name" },
373 .key = data[pos_space + 1 .. pos_equals],698 .{ .kind = .linkpath, .err = error.PaxInvalidAttributeEnd },
374 .value_off = pos_equals + 1,699 },
375 .value_len = kv_size - pos_equals - 2,700 },
701 .{ // null in keyword is not valid
702 .data = "13 path=name\n" ++ "7 k\x00b=1\n",
703 .attrs = &[_]Attr{
704 .{ .kind = .path, .value = "name" },
705 },
706 .err = error.PaxNullInKeyword,
707 },
708 .{ // null in value is not valid
709 .data = "23 path=name\x00with null\n",
710 .attrs = &[_]Attr{
711 .{ .kind = .path, .err = error.PaxNullInValue },
712 },
713 },
714 .{ // 1000 characters path
715 .data = "1011 path=" ++ "0123456789" ** 100 ++ "\n",
716 .attrs = &[_]Attr{
717 .{ .kind = .path, .value = "0123456789" ** 100 },
718 },
719 },
376 };720 };
377}721 var buffer: [1024]u8 = undefined;
378722
379test parsePaxAttribute {723 outer: for (cases) |case| {
380 const expectEqual = std.testing.expectEqual;724 var stream = std.io.fixedBufferStream(case.data);
381 const expectEqualStrings = std.testing.expectEqualStrings;725 var iter = paxIterator(stream.reader(), case.data.len);
382 const expectError = std.testing.expectError;726
383 const prefix = "1011 path=";727 var i: usize = 0;
384 const file_name = "0123456789" ** 100;728 while (iter.next() catch |err| {
385 const header = prefix ++ file_name ++ "\n";729 if (case.err) |e| {
386 const attr_info = try parsePaxAttribute(header, 1011);730 try std.testing.expectEqual(e, err);
387 try expectEqual(@as(usize, 1011), attr_info.size);731 continue;
388 try expectEqualStrings("path", attr_info.key);732 }
389 try expectEqual(prefix.len, attr_info.value_off);733 return err;
390 try expectEqual(file_name.len, attr_info.value_len);734 }) |attr| : (i += 1) {
391 try expectEqual(attr_info, try parsePaxAttribute(header, 1012));735 const exp = case.attrs[i];
392 try expectError(error.InvalidPaxAttribute, parsePaxAttribute(header, 1010));736 try std.testing.expectEqual(exp.kind, attr.kind);
393 try expectError(error.InvalidPaxAttribute, parsePaxAttribute("", 0));737 const value = attr.value(&buffer) catch |err| {
738 if (exp.err) |e| {
739 try std.testing.expectEqual(e, err);
740 break :outer;
741 }
742 return err;
743 };
744 try std.testing.expectEqualStrings(exp.value, value);
745 }
746 try std.testing.expectEqual(case.attrs.len, i);
747 try std.testing.expect(case.err == null);
748 }
394}749}
395750
396const std = @import("std.zig");751test {
397const assert = std.debug.assert;752 _ = @import("tar/test.zig");
753}
lib/std/tar/test.zig created+367
...@@ -0,0 +1,367 @@
1const std = @import("../std.zig");
2const tar = std.tar;
3const testing = std.testing;
4
5test "tar run Go test cases" {
6 const Case = struct {
7 const File = struct {
8 name: []const u8,
9 size: u64 = 0,
10 mode: u32 = 0,
11 link_name: []const u8 = &[0]u8{},
12 kind: tar.Header.Kind = .normal,
13 truncated: bool = false, // when there is no file body, just header, usefull for huge files
14 };
15
16 data: []const u8, // testdata file content
17 files: []const File = &[_]@This().File{}, // expected files to found in archive
18 chksums: []const []const u8 = &[_][]const u8{}, // chksums of each file content
19 err: ?anyerror = null, // parsing should fail with this error
20 };
21
22 const cases = [_]Case{
23 .{
24 .data = @embedFile("testdata/gnu.tar"),
25 .files = &[_]Case.File{
26 .{
27 .name = "small.txt",
28 .size = 5,
29 .mode = 0o640,
30 },
31 .{
32 .name = "small2.txt",
33 .size = 11,
34 .mode = 0o640,
35 },
36 },
37 .chksums = &[_][]const u8{
38 "e38b27eaccb4391bdec553a7f3ae6b2f",
39 "c65bd2e50a56a2138bf1716f2fd56fe9",
40 },
41 },
42 .{
43 .data = @embedFile("testdata/sparse-formats.tar"),
44 .err = error.TarUnsupportedHeader,
45 },
46 .{
47 .data = @embedFile("testdata/star.tar"),
48 .files = &[_]Case.File{
49 .{
50 .name = "small.txt",
51 .size = 5,
52 .mode = 0o640,
53 },
54 .{
55 .name = "small2.txt",
56 .size = 11,
57 .mode = 0o640,
58 },
59 },
60 .chksums = &[_][]const u8{
61 "e38b27eaccb4391bdec553a7f3ae6b2f",
62 "c65bd2e50a56a2138bf1716f2fd56fe9",
63 },
64 },
65 .{
66 .data = @embedFile("testdata/v7.tar"),
67 .files = &[_]Case.File{
68 .{
69 .name = "small.txt",
70 .size = 5,
71 .mode = 0o444,
72 },
73 .{
74 .name = "small2.txt",
75 .size = 11,
76 .mode = 0o444,
77 },
78 },
79 .chksums = &[_][]const u8{
80 "e38b27eaccb4391bdec553a7f3ae6b2f",
81 "c65bd2e50a56a2138bf1716f2fd56fe9",
82 },
83 },
84 .{
85 .data = @embedFile("testdata/pax.tar"),
86 .files = &[_]Case.File{
87 .{
88 .name = "a/123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100",
89 .size = 7,
90 .mode = 0o664,
91 },
92 .{
93 .name = "a/b",
94 .size = 0,
95 .kind = .symbolic_link,
96 .mode = 0o777,
97 .link_name = "123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100",
98 },
99 },
100 .chksums = &[_][]const u8{
101 "3c382e8f5b6631aa2db52643912ffd4a",
102 },
103 },
104 .{
105 // pax attribute don't end with \n
106 .data = @embedFile("testdata/pax-bad-hdr-file.tar"),
107 .err = error.PaxInvalidAttributeEnd,
108 },
109 .{
110 // size is in pax attribute
111 .data = @embedFile("testdata/pax-pos-size-file.tar"),
112 .files = &[_]Case.File{
113 .{
114 .name = "foo",
115 .size = 999,
116 .kind = .normal,
117 .mode = 0o640,
118 },
119 },
120 .chksums = &[_][]const u8{
121 "0afb597b283fe61b5d4879669a350556",
122 },
123 },
124 .{
125 // has pax records which we are not interested in
126 .data = @embedFile("testdata/pax-records.tar"),
127 .files = &[_]Case.File{
128 .{
129 .name = "file",
130 },
131 },
132 },
133 .{
134 // has global records which we are ignoring
135 .data = @embedFile("testdata/pax-global-records.tar"),
136 .files = &[_]Case.File{
137 .{
138 .name = "file1",
139 },
140 .{
141 .name = "file2",
142 },
143 .{
144 .name = "file3",
145 },
146 .{
147 .name = "file4",
148 },
149 },
150 },
151 .{
152 .data = @embedFile("testdata/nil-uid.tar"),
153 .files = &[_]Case.File{
154 .{
155 .name = "P1050238.JPG.log",
156 .size = 14,
157 .kind = .normal,
158 .mode = 0o664,
159 },
160 },
161 .chksums = &[_][]const u8{
162 "08d504674115e77a67244beac19668f5",
163 },
164 },
165 .{
166 // has xattrs and pax records which we are ignoring
167 .data = @embedFile("testdata/xattrs.tar"),
168 .files = &[_]Case.File{
169 .{
170 .name = "small.txt",
171 .size = 5,
172 .kind = .normal,
173 .mode = 0o644,
174 },
175 .{
176 .name = "small2.txt",
177 .size = 11,
178 .kind = .normal,
179 .mode = 0o644,
180 },
181 },
182 .chksums = &[_][]const u8{
183 "e38b27eaccb4391bdec553a7f3ae6b2f",
184 "c65bd2e50a56a2138bf1716f2fd56fe9",
185 },
186 },
187 .{
188 .data = @embedFile("testdata/gnu-multi-hdrs.tar"),
189 .files = &[_]Case.File{
190 .{
191 .name = "GNU2/GNU2/long-path-name",
192 .link_name = "GNU4/GNU4/long-linkpath-name",
193 .kind = .symbolic_link,
194 },
195 },
196 },
197 .{
198 // has gnu type D (directory) and S (sparse) blocks
199 .data = @embedFile("testdata/gnu-incremental.tar"),
200 .err = error.TarUnsupportedHeader,
201 },
202 .{
203 // should use values only from last pax header
204 .data = @embedFile("testdata/pax-multi-hdrs.tar"),
205 .files = &[_]Case.File{
206 .{
207 .name = "bar",
208 .link_name = "PAX4/PAX4/long-linkpath-name",
209 .kind = .symbolic_link,
210 },
211 },
212 },
213 .{
214 .data = @embedFile("testdata/gnu-long-nul.tar"),
215 .files = &[_]Case.File{
216 .{
217 .name = "0123456789",
218 .mode = 0o644,
219 },
220 },
221 },
222 .{
223 .data = @embedFile("testdata/gnu-utf8.tar"),
224 .files = &[_]Case.File{
225 .{
226 .name = "☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹",
227 .mode = 0o644,
228 },
229 },
230 },
231 .{
232 .data = @embedFile("testdata/gnu-not-utf8.tar"),
233 .files = &[_]Case.File{
234 .{
235 .name = "hi\x80\x81\x82\x83bye",
236 .mode = 0o644,
237 },
238 },
239 },
240 .{
241 // null in pax key
242 .data = @embedFile("testdata/pax-nul-xattrs.tar"),
243 .err = error.PaxNullInKeyword,
244 },
245 .{
246 .data = @embedFile("testdata/pax-nul-path.tar"),
247 .err = error.PaxNullInValue,
248 },
249 .{
250 .data = @embedFile("testdata/neg-size.tar"),
251 .err = error.TarHeader,
252 },
253 .{
254 .data = @embedFile("testdata/issue10968.tar"),
255 .err = error.TarHeader,
256 },
257 .{
258 .data = @embedFile("testdata/issue11169.tar"),
259 .err = error.TarHeader,
260 },
261 .{
262 .data = @embedFile("testdata/issue12435.tar"),
263 .err = error.TarHeaderChksum,
264 },
265 .{
266 // has magic with space at end instead of null
267 .data = @embedFile("testdata/invalid-go17.tar"),
268 .files = &[_]Case.File{
269 .{
270 .name = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/foo",
271 },
272 },
273 },
274 .{
275 .data = @embedFile("testdata/ustar-file-devs.tar"),
276 .files = &[_]Case.File{
277 .{
278 .name = "file",
279 .mode = 0o644,
280 },
281 },
282 },
283 .{
284 .data = @embedFile("testdata/trailing-slash.tar"),
285 .files = &[_]Case.File{
286 .{
287 .name = "123456789/" ** 30,
288 .kind = .directory,
289 },
290 },
291 },
292 .{
293 // Has size in gnu extended format. To represent size bigger than 8 GB.
294 .data = @embedFile("testdata/writer-big.tar"),
295 .files = &[_]Case.File{
296 .{
297 .name = "tmp/16gig.txt",
298 .size = 16 * 1024 * 1024 * 1024,
299 .truncated = true,
300 .mode = 0o640,
301 },
302 },
303 },
304 .{
305 // Size in gnu extended format, and name in pax attribute.
306 .data = @embedFile("testdata/writer-big-long.tar"),
307 .files = &[_]Case.File{
308 .{
309 .name = "longname/" ** 15 ++ "16gig.txt",
310 .size = 16 * 1024 * 1024 * 1024,
311 .mode = 0o644,
312 .truncated = true,
313 },
314 },
315 },
316 };
317
318 for (cases) |case| {
319 var fsb = std.io.fixedBufferStream(case.data);
320 var iter = tar.iterator(fsb.reader(), null);
321 var i: usize = 0;
322 while (iter.next() catch |err| {
323 if (case.err) |e| {
324 try testing.expectEqual(e, err);
325 continue;
326 } else {
327 return err;
328 }
329 }) |actual| : (i += 1) {
330 const expected = case.files[i];
331 try testing.expectEqualStrings(expected.name, actual.name);
332 try testing.expectEqual(expected.size, actual.size);
333 try testing.expectEqual(expected.kind, actual.kind);
334 try testing.expectEqual(expected.mode, actual.mode);
335 try testing.expectEqualStrings(expected.link_name, actual.link_name);
336
337 if (case.chksums.len > i) {
338 var md5writer = Md5Writer{};
339 try actual.write(&md5writer);
340 const chksum = md5writer.chksum();
341 try testing.expectEqualStrings(case.chksums[i], &chksum);
342 } else {
343 if (!expected.truncated) try actual.skip(); // skip file content
344 }
345 }
346 try testing.expectEqual(case.files.len, i);
347 }
348}
349
350// used in test to calculate file chksum
351const Md5Writer = struct {
352 h: std.crypto.hash.Md5 = std.crypto.hash.Md5.init(.{}),
353
354 pub fn writeAll(self: *Md5Writer, buf: []const u8) !void {
355 self.h.update(buf);
356 }
357
358 pub fn writeByte(self: *Md5Writer, byte: u8) !void {
359 self.h.update(&[_]u8{byte});
360 }
361
362 pub fn chksum(self: *Md5Writer) [32]u8 {
363 var s = [_]u8{0} ** 16;
364 self.h.final(&s);
365 return std.fmt.bytesToHex(s, .lower);
366 }
367};
lib/std/tar/testdata/gnu-incremental.tar created
Binary files /dev/null and b/lib/std/tar/testdata/gnu-incremental.tar differ
lib/std/tar/testdata/gnu-long-nul.tar created
Binary files /dev/null and b/lib/std/tar/testdata/gnu-long-nul.tar differ
lib/std/tar/testdata/gnu-multi-hdrs.tar created
Binary files /dev/null and b/lib/std/tar/testdata/gnu-multi-hdrs.tar differ
lib/std/tar/testdata/gnu-not-utf8.tar created
Binary files /dev/null and b/lib/std/tar/testdata/gnu-not-utf8.tar differ
lib/std/tar/testdata/gnu-utf8.tar created
Binary files /dev/null and b/lib/std/tar/testdata/gnu-utf8.tar differ
lib/std/tar/testdata/gnu.tar created
Binary files /dev/null and b/lib/std/tar/testdata/gnu.tar differ
lib/std/tar/testdata/invalid-go17.tar created
Binary files /dev/null and b/lib/std/tar/testdata/invalid-go17.tar differ
lib/std/tar/testdata/issue10968.tar created
Binary files /dev/null and b/lib/std/tar/testdata/issue10968.tar differ
lib/std/tar/testdata/issue11169.tar created
Binary files /dev/null and b/lib/std/tar/testdata/issue11169.tar differ
lib/std/tar/testdata/issue12435.tar created
Binary files /dev/null and b/lib/std/tar/testdata/issue12435.tar differ
lib/std/tar/testdata/neg-size.tar created
Binary files /dev/null and b/lib/std/tar/testdata/neg-size.tar differ
lib/std/tar/testdata/nil-uid.tar created
Binary files /dev/null and b/lib/std/tar/testdata/nil-uid.tar differ
lib/std/tar/testdata/pax-bad-hdr-file.tar created
Binary files /dev/null and b/lib/std/tar/testdata/pax-bad-hdr-file.tar differ
lib/std/tar/testdata/pax-global-records.tar created
Binary files /dev/null and b/lib/std/tar/testdata/pax-global-records.tar differ
lib/std/tar/testdata/pax-multi-hdrs.tar created
Binary files /dev/null and b/lib/std/tar/testdata/pax-multi-hdrs.tar differ
lib/std/tar/testdata/pax-nul-path.tar created
Binary files /dev/null and b/lib/std/tar/testdata/pax-nul-path.tar differ
lib/std/tar/testdata/pax-nul-xattrs.tar created
Binary files /dev/null and b/lib/std/tar/testdata/pax-nul-xattrs.tar differ
lib/std/tar/testdata/pax-pos-size-file.tar created
Binary files /dev/null and b/lib/std/tar/testdata/pax-pos-size-file.tar differ
lib/std/tar/testdata/pax-records.tar created
Binary files /dev/null and b/lib/std/tar/testdata/pax-records.tar differ
lib/std/tar/testdata/pax.tar created
Binary files /dev/null and b/lib/std/tar/testdata/pax.tar differ
lib/std/tar/testdata/sparse-formats.tar created
Binary files /dev/null and b/lib/std/tar/testdata/sparse-formats.tar differ
lib/std/tar/testdata/star.tar created
Binary files /dev/null and b/lib/std/tar/testdata/star.tar differ
lib/std/tar/testdata/trailing-slash.tar created
Binary files /dev/null and b/lib/std/tar/testdata/trailing-slash.tar differ
lib/std/tar/testdata/ustar-file-devs.tar created
Binary files /dev/null and b/lib/std/tar/testdata/ustar-file-devs.tar differ
lib/std/tar/testdata/v7.tar created
Binary files /dev/null and b/lib/std/tar/testdata/v7.tar differ
lib/std/tar/testdata/writer-big-long.tar created
Binary files /dev/null and b/lib/std/tar/testdata/writer-big-long.tar differ
lib/std/tar/testdata/writer-big.tar created
Binary files /dev/null and b/lib/std/tar/testdata/writer-big.tar differ
lib/std/tar/testdata/xattrs.tar created
Binary files /dev/null and b/lib/std/tar/testdata/xattrs.tar differ