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");
19const builtin = @import("builtin");
20const Io = std.Io;
21const assert = std.debug.assert;
22const testing = std.testing;
23
24pub const Writer = @import("tar/Writer.zig");
25
26/// Provide this to receive detailed error messages.
27/// When this is provided, some errors which would otherwise be returned
28/// immediately will instead be added to this structure. The API user must check
29/// the errors in diagnostics to know whether the operation succeeded or failed.
30pub const Diagnostics = struct {
31 allocator: std.mem.Allocator,
32 errors: std.ArrayList(Error) = .empty,
33
34 entries: usize = 0,
35 root_dir: []const u8 = "",
36
37 pub const Error = union(enum) {
38 unable_to_create_sym_link: struct {
39 code: anyerror,
40 file_name: []const u8,
41 link_name: []const u8,
42 },
43 unable_to_create_file: struct {
44 code: anyerror,
45 file_name: []const u8,
46 },
47 unsupported_file_type: struct {
48 file_name: []const u8,
49 file_type: Header.Kind,
50 },
51 components_outside_stripped_prefix: struct {
52 file_name: []const u8,
53 },
54 };
55
56 fn findRoot(d: *Diagnostics, kind: FileKind, path: []const u8) !void {
57 if (path.len == 0) return;
58
59 d.entries += 1;
60 const root_dir = rootDir(path, kind);
61 if (d.entries == 1) {
62 d.root_dir = try d.allocator.dupe(u8, root_dir);
63 return;
64 }
65 if (d.root_dir.len == 0 or std.mem.eql(u8, root_dir, d.root_dir))
66 return;
67 d.allocator.free(d.root_dir);
68 d.root_dir = "";
69 }
70
71 // Returns root dir of the path, assumes non empty path.
72 fn rootDir(path: []const u8, kind: FileKind) []const u8 {
73 const start_index: usize = if (path[0] == '/') 1 else 0;
74 const end_index: usize = if (path[path.len - 1] == '/') path.len - 1 else path.len;
75 const buf = path[start_index..end_index];
76 if (std.mem.findScalarPos(u8, buf, 0, '/')) |idx| {
77 return buf[0..idx];
78 }
79
80 return switch (kind) {
81 .file => "",
82 .sym_link => "",
83 .directory => buf,
84 };
85 }
86
87 test rootDir {
88 const expectEqualStrings = testing.expectEqualStrings;
89 try expectEqualStrings("", rootDir("a", .file));
90 try expectEqualStrings("a", rootDir("a", .directory));
91 try expectEqualStrings("b", rootDir("b", .directory));
92 try expectEqualStrings("c", rootDir("/c", .directory));
93 try expectEqualStrings("d", rootDir("/d/", .directory));
94 try expectEqualStrings("a", rootDir("a/b", .directory));
95 try expectEqualStrings("a", rootDir("a/b", .file));
96 try expectEqualStrings("a", rootDir("a/b/c", .directory));
97 }
98
99 pub fn deinit(d: *Diagnostics) void {
100 for (d.errors.items) |item| {
101 switch (item) {
102 .unable_to_create_sym_link => |info| {
103 d.allocator.free(info.file_name);
104 d.allocator.free(info.link_name);
105 },
106 .unable_to_create_file => |info| {
107 d.allocator.free(info.file_name);
108 },
109 .unsupported_file_type => |info| {
110 d.allocator.free(info.file_name);
111 },
112 .components_outside_stripped_prefix => |info| {
113 d.allocator.free(info.file_name);
114 },
115 }
116 }
117 d.errors.deinit(d.allocator);
118 d.allocator.free(d.root_dir);
119 d.* = undefined;
120 }
121};
122
123/// Deprecated, renamed to `ExtractOptions`.
124pub const PipeOptions = ExtractOptions;
125
126pub const ExtractOptions = struct {
127 /// Number of directory levels to skip when extracting files.
128 strip_components: u32 = 0,
129 /// How to handle the "mode" property of files from within the tar file.
130 mode_mode: ModeMode = .executable_bit_only,
131 /// Prevents creation of empty directories.
132 exclude_empty_directories: bool = false,
133 /// Collects error messages during unpacking
134 diagnostics: ?*Diagnostics = null,
135
136 pub const ModeMode = enum {
137 /// The mode from the tar file is completely ignored. Files are created
138 /// with the default mode when creating files.
139 ignore,
140 /// The mode from the tar file is inspected for the owner executable bit
141 /// only. This bit is copied to the group and other executable bits.
142 /// Other bits of the mode are left as the default when creating files.
143 executable_bit_only,
144 };
145};
146
147const Header = struct {
148 const SIZE = 512;
149 const MAX_NAME_SIZE = 100 + 1 + 155; // name(100) + separator(1) + prefix(155)
150 const LINK_NAME_SIZE = 100;
151
152 bytes: *const [SIZE]u8,
153
154 const Kind = enum(u8) {
155 normal_alias = 0,
156 normal = '0',
157 hard_link = '1',
158 symbolic_link = '2',
159 character_special = '3',
160 block_special = '4',
161 directory = '5',
162 fifo = '6',
163 contiguous = '7',
164 global_extended_header = 'g',
165 extended_header = 'x',
166 // Types 'L' and 'K' are used by the GNU format for a meta file
167 // used to store the path or link name for the next file.
168 gnu_long_name = 'L',
169 gnu_long_link = 'K',
170 gnu_sparse = 'S',
171 solaris_extended_header = 'X',
172 _,
173 };
174
175 /// Includes prefix concatenated, if any.
176 /// TODO: check against "../" and other nefarious things
177 pub fn fullName(header: Header, buffer: []u8) ![]const u8 {
178 const n = name(header);
179 const p = prefix(header);
180 if (buffer.len < n.len + p.len + 1) return error.TarInsufficientBuffer;
181 if (!is_ustar(header) or p.len == 0) {
182 @memcpy(buffer[0..n.len], n);
183 return buffer[0..n.len];
184 }
185 @memcpy(buffer[0..p.len], p);
186 buffer[p.len] = '/';
187 @memcpy(buffer[p.len + 1 ..][0..n.len], n);
188 return buffer[0 .. p.len + 1 + n.len];
189 }
190
191 /// When kind is symbolic_link linked-to name (target_path) is specified in
192 /// the linkname field.
193 pub fn linkName(header: Header, buffer: []u8) ![]const u8 {
194 const link_name = header.str(157, 100);
195 if (link_name.len == 0) {
196 return buffer[0..0];
197 }
198 if (buffer.len < link_name.len) return error.TarInsufficientBuffer;
199 const buf = buffer[0..link_name.len];
200 @memcpy(buf, link_name);
201 return buf;
202 }
203
204 pub fn name(header: Header) []const u8 {
205 return header.str(0, 100);
206 }
207
208 pub fn mode(header: Header) !u32 {
209 return @intCast(try header.octal(100, 8));
210 }
211
212 pub fn size(header: Header) !u64 {
213 const start = 124;
214 const len = 12;
215 const raw = header.bytes[start..][0..len];
216 // If the leading byte is 0xff (255), all the bytes of the field
217 // (including the leading byte) are concatenated in big-endian order,
218 // with the result being a negative number expressed in two’s
219 // complement form.
220 if (raw[0] == 0xff) return error.TarNumericValueNegative;
221 // If the leading byte is 0x80 (128), the non-leading bytes of the
222 // field are concatenated in big-endian order.
223 if (raw[0] == 0x80) {
224 if (raw[1] != 0 or raw[2] != 0 or raw[3] != 0) return error.TarNumericValueTooBig;
225 return std.mem.readInt(u64, raw[4..12], .big);
226 }
227 return try header.octal(start, len);
228 }
229
230 pub fn chksum(header: Header) !u64 {
231 return header.octal(148, 8);
232 }
233
234 pub fn is_ustar(header: Header) bool {
235 const magic = header.bytes[257..][0..6];
236 return std.mem.eql(u8, magic[0..5], "ustar") and (magic[5] == 0 or magic[5] == ' ');
237 }
238
239 pub fn prefix(header: Header) []const u8 {
240 return header.str(345, 155);
241 }
242
243 pub fn kind(header: Header) Kind {
244 const result: Kind = @fromBackingInt(@intCast(header.bytes[156]));
245 if (result == .normal_alias) return .normal;
246 return result;
247 }
248
249 fn str(header: Header, start: usize, len: usize) []const u8 {
250 return nullStr(header.bytes[start .. start + len]);
251 }
252
253 fn octal(header: Header, start: usize, len: usize) !u64 {
254 const raw = header.bytes[start..][0..len];
255 // Zero-filled octal number in ASCII. Each numeric field of width w
256 // contains w minus 1 digits, and a null
257 const ltrimmed = std.mem.trimStart(u8, raw, "0 ");
258 const rtrimmed = std.mem.trimEnd(u8, ltrimmed, " \x00");
259 if (rtrimmed.len == 0) return 0;
260 return std.fmt.parseInt(u64, rtrimmed, 8) catch return error.TarHeader;
261 }
262
263 const Chksums = struct {
264 unsigned: u64,
265 signed: i64,
266 };
267
268 // Sum of all bytes in the header block. The chksum field is treated as if
269 // it were filled with spaces (ASCII 32).
270 fn computeChksum(header: Header) Chksums {
271 var cs: Chksums = .{ .signed = 0, .unsigned = 0 };
272 for (header.bytes, 0..) |v, i| {
273 const b = if (148 <= i and i < 156) 32 else v; // Treating chksum bytes as spaces.
274 cs.unsigned += b;
275 cs.signed += @as(i8, @bitCast(b));
276 }
277 return cs;
278 }
279
280 // Checks calculated chksum with value of chksum field.
281 // Returns error or valid chksum value.
282 // Zero value indicates empty block.
283 pub fn checkChksum(header: Header) !u64 {
284 const field = try header.chksum();
285 const cs = header.computeChksum();
286 if (field == 0 and cs.unsigned == 256) return 0;
287 if (field != cs.unsigned and field != cs.signed) return error.TarHeaderChksum;
288 return field;
289 }
290};
291
292// Breaks string on first null character.
293fn nullStr(str: []const u8) []const u8 {
294 for (str, 0..) |c, i| {
295 if (c == 0) return str[0..i];
296 }
297 return str;
298}
299
300/// Type of the file returned by iterator `next` method.
301pub const FileKind = enum {
302 directory,
303 sym_link,
304 file,
305};
306
307/// Iterator over entries in the tar file represented by reader.
308pub const Iterator = struct {
309 reader: *Io.Reader,
310 diagnostics: ?*Diagnostics = null,
311
312 // buffers for heeader and file attributes
313 header_buffer: [Header.SIZE]u8 = undefined,
314 file_name_buffer: []u8,
315 link_name_buffer: []u8,
316
317 // bytes of padding to the end of the block
318 padding: usize = 0,
319 // not consumed bytes of file from last next iteration
320 unread_file_bytes: u64 = 0,
321
322 /// Options for iterator.
323 /// Buffers should be provided by the caller.
324 pub const Options = struct {
325 /// Use a buffer with length `std.fs.max_path_bytes` to match file system capabilities.
326 file_name_buffer: []u8,
327 /// Use a buffer with length `std.fs.max_path_bytes` to match file system capabilities.
328 link_name_buffer: []u8,
329 /// Collects error messages during unpacking
330 diagnostics: ?*Diagnostics = null,
331 };
332
333 /// Iterates over files in tar archive.
334 /// `next` returns each file in tar archive.
335 pub fn init(reader: *Io.Reader, options: Options) Iterator {
336 return .{
337 .reader = reader,
338 .diagnostics = options.diagnostics,
339 .file_name_buffer = options.file_name_buffer,
340 .link_name_buffer = options.link_name_buffer,
341 };
342 }
343
344 pub const File = struct {
345 name: []const u8, // name of file, symlink or directory
346 link_name: []const u8, // target name of symlink
347 size: u64 = 0, // size of the file in bytes
348 mode: u32 = 0,
349 kind: FileKind = .file,
350 };
351
352 fn readHeader(self: *Iterator) !?Header {
353 if (self.padding > 0) {
354 try self.reader.discardAll(self.padding);
355 }
356 const n = try self.reader.readSliceShort(&self.header_buffer);
357 if (n == 0) return null;
358 if (n < Header.SIZE) return error.UnexpectedEndOfStream;
359 const header = Header{ .bytes = self.header_buffer[0..Header.SIZE] };
360 if (try header.checkChksum() == 0) return null;
361 return header;
362 }
363
364 fn readString(self: *Iterator, size: usize, buffer: []u8) ![]const u8 {
365 if (size > buffer.len) return error.TarInsufficientBuffer;
366 const buf = buffer[0..size];
367 try self.reader.readSliceAll(buf);
368 return nullStr(buf);
369 }
370
371 fn newFile(self: *Iterator) File {
372 return .{
373 .name = self.file_name_buffer[0..0],
374 .link_name = self.link_name_buffer[0..0],
375 };
376 }
377
378 // Number of padding bytes in the last file block.
379 fn blockPadding(size: u64) usize {
380 const block_rounded = std.mem.alignForward(u64, size, Header.SIZE); // size rounded to te block boundary
381 return @intCast(block_rounded - size);
382 }
383
384 /// Iterates through the tar archive as if it is a series of files.
385 /// Internally, the tar format often uses entries (header with optional
386 /// content) to add meta data that describes the next file. These
387 /// entries should not normally be visible to the outside. As such, this
388 /// loop iterates through one or more entries until it collects a all
389 /// file attributes.
390 pub fn next(self: *Iterator) !?File {
391 if (self.unread_file_bytes > 0) {
392 // If file content was not consumed by caller
393 try self.reader.discardAll64(self.unread_file_bytes);
394 self.unread_file_bytes = 0;
395 }
396 var file: File = self.newFile();
397
398 while (try self.readHeader()) |header| {
399 const kind = header.kind();
400 const size: u64 = try header.size();
401 self.padding = blockPadding(size);
402
403 switch (kind) {
404 // File types to return upstream
405 .directory, .normal, .symbolic_link => {
406 file.kind = switch (kind) {
407 .directory => .directory,
408 .normal => .file,
409 .symbolic_link => .sym_link,
410 else => unreachable,
411 };
412 file.mode = try header.mode();
413
414 // set file attributes if not already set by prefix/extended headers
415 if (file.size == 0) {
416 file.size = size;
417 }
418 if (file.link_name.len == 0) {
419 file.link_name = try header.linkName(self.link_name_buffer);
420 }
421 if (file.name.len == 0) {
422 file.name = try header.fullName(self.file_name_buffer);
423 }
424
425 self.padding = blockPadding(file.size);
426 self.unread_file_bytes = file.size;
427 return file;
428 },
429 // Prefix header types
430 .gnu_long_name => {
431 file.name = try self.readString(@intCast(size), self.file_name_buffer);
432 },
433 .gnu_long_link => {
434 file.link_name = try self.readString(@intCast(size), self.link_name_buffer);
435 },
436 .extended_header => {
437 // Use just attributes from last extended header.
438 file = self.newFile();
439
440 var rdr: PaxIterator = .{
441 .reader = self.reader,
442 .size = @intCast(size),
443 };
444 while (try rdr.next()) |attr| {
445 switch (attr.kind) {
446 .path => {
447 file.name = try attr.value(self.file_name_buffer);
448 },
449 .linkpath => {
450 file.link_name = try attr.value(self.link_name_buffer);
451 },
452 .size => {
453 var buf: [pax_max_size_attr_len]u8 = undefined;
454 file.size = try std.fmt.parseInt(u64, try attr.value(&buf), 10);
455 },
456 }
457 }
458 },
459 // Ignored header type
460 .global_extended_header => {
461 self.reader.discardAll64(size) catch return error.TarHeadersTooBig;
462 },
463 // All other are unsupported header types
464 else => {
465 const d = self.diagnostics orelse return error.TarUnsupportedHeader;
466 try d.errors.append(d.allocator, .{ .unsupported_file_type = .{
467 .file_name = try d.allocator.dupe(u8, header.name()),
468 .file_type = kind,
469 } });
470 if (kind == .gnu_sparse) {
471 try self.skipGnuSparseExtendedHeaders(header);
472 }
473 self.reader.discardAll64(size) catch return error.TarHeadersTooBig;
474 },
475 }
476 }
477 return null;
478 }
479
480 pub fn streamRemaining(it: *Iterator, file: File, w: *Io.Writer) Io.Reader.StreamError!void {
481 try it.reader.streamExact64(w, file.size);
482 it.unread_file_bytes = 0;
483 }
484
485 fn skipGnuSparseExtendedHeaders(self: *Iterator, header: Header) !void {
486 var is_extended = header.bytes[482] > 0;
487 while (is_extended) {
488 var buf: [Header.SIZE]u8 = undefined;
489 try self.reader.readSliceAll(&buf);
490 is_extended = buf[504] > 0;
491 }
492 }
493};
494
495const PaxAttributeKind = enum {
496 path,
497 linkpath,
498 size,
499};
500
501// maxInt(u64) has 20 chars, base 10 in practice we got 24 chars
502const pax_max_size_attr_len = 64;
503
504pub const PaxIterator = struct {
505 size: usize, // cumulative size of all pax attributes
506 reader: *Io.Reader,
507
508 const Self = @This();
509
510 const Attribute = struct {
511 kind: PaxAttributeKind,
512 len: usize, // length of the attribute value
513 reader: *Io.Reader, // reader positioned at value start
514
515 // Copies pax attribute value into destination buffer.
516 // Must be called with destination buffer of size at least Attribute.len.
517 pub fn value(self: Attribute, dst: []u8) ![]const u8 {
518 if (self.len > dst.len) return error.TarInsufficientBuffer;
519 // assert(self.len <= dst.len);
520 const buf = dst[0..self.len];
521 const n = try self.reader.readSliceShort(buf);
522 if (n < self.len) return error.UnexpectedEndOfStream;
523 try validateAttributeEnding(self.reader);
524 if (hasNull(buf)) return error.PaxNullInValue;
525 return buf;
526 }
527 };
528
529 // Iterates over pax attributes. Returns known only known attributes.
530 // Caller has to call value in Attribute, to advance reader across value.
531 pub fn next(self: *Self) !?Attribute {
532 // Pax extended header consists of one or more attributes, each constructed as follows:
533 // "%d %s=%s\n", <length>, <keyword>, <value>
534 while (self.size > 0) {
535 const length_buf = try self.reader.takeSentinel(' ');
536 const length = try std.fmt.parseInt(usize, length_buf, 10); // record length in bytes
537
538 const keyword = try self.reader.takeSentinel('=');
539 if (hasNull(keyword)) return error.PaxNullInKeyword;
540
541 // calculate value_len
542 const value_start = length_buf.len + keyword.len + 2; // 2 separators
543 if (length < value_start + 1 or self.size < length) return error.UnexpectedEndOfStream;
544 const value_len = length - value_start - 1; // \n separator at end
545 self.size -= length;
546
547 const kind: PaxAttributeKind = if (eql(keyword, "path"))
548 .path
549 else if (eql(keyword, "linkpath"))
550 .linkpath
551 else if (eql(keyword, "size"))
552 .size
553 else {
554 try self.reader.discardAll(value_len);
555 try validateAttributeEnding(self.reader);
556 continue;
557 };
558 if (kind == .size and value_len > pax_max_size_attr_len) {
559 return error.PaxSizeAttrOverflow;
560 }
561 return .{
562 .kind = kind,
563 .len = value_len,
564 .reader = self.reader,
565 };
566 }
567
568 return null;
569 }
570
571 fn eql(a: []const u8, b: []const u8) bool {
572 return std.mem.eql(u8, a, b);
573 }
574
575 fn hasNull(str: []const u8) bool {
576 return (std.mem.findScalar(u8, str, 0)) != null;
577 }
578
579 // Checks that each record ends with new line.
580 fn validateAttributeEnding(reader: *Io.Reader) !void {
581 if (try reader.takeByte() != '\n') return error.PaxInvalidAttributeEnd;
582 }
583};
584
585/// Deprecated, renamed to `extract`.
586pub const pipeToFileSystem = extract;
587
588/// Ingests tar file from `reader`, populating file contents within `dir`. If
589/// any file would be extracted outside of `dir`, an error is return instead.
590pub fn extract(io: Io, dir: Io.Dir, reader: *Io.Reader, options: ExtractOptions) !void {
591 var file_name_buffer: [Io.Dir.max_path_bytes]u8 = undefined;
592 var link_name_buffer: [Io.Dir.max_path_bytes]u8 = undefined;
593 var sanitize_buffer: [Io.Dir.max_path_bytes]u8 = undefined;
594 var file_contents_buffer: [1024]u8 = undefined;
595 var it: Iterator = .init(reader, .{
596 .file_name_buffer = &file_name_buffer,
597 .link_name_buffer = &link_name_buffer,
598 .diagnostics = options.diagnostics,
599 });
600
601 while (try it.next()) |file| {
602 const n = sanitizePath(&sanitize_buffer, file.name, options.strip_components) catch 0;
603 if (n == 0 and file.kind != .directory) {
604 const d = options.diagnostics orelse return error.TarComponentsOutsideStrippedPrefix;
605 try d.errors.append(d.allocator, .{ .components_outside_stripped_prefix = .{
606 .file_name = try d.allocator.dupe(u8, file.name),
607 } });
608 continue;
609 }
610 const file_name = sanitize_buffer[0..n];
611 if (options.diagnostics) |d| {
612 try d.findRoot(file.kind, file_name);
613 }
614
615 switch (file.kind) {
616 .directory => {
617 if (file_name.len > 0 and !options.exclude_empty_directories) {
618 try dir.createDirPath(io, file_name);
619 }
620 },
621 .file => {
622 if (createDirAndFile(io, dir, file_name, filePermissions(file.mode, options))) |fs_file| {
623 defer fs_file.close(io);
624 var file_writer = fs_file.writer(io, &file_contents_buffer);
625 try it.streamRemaining(file, &file_writer.interface);
626 try file_writer.interface.flush();
627 } else |err| {
628 const d = options.diagnostics orelse return err;
629 try d.errors.append(d.allocator, .{ .unable_to_create_file = .{
630 .code = err,
631 .file_name = try d.allocator.dupe(u8, file_name),
632 } });
633 }
634 },
635 .sym_link => {
636 const link_name = file.link_name;
637 createDirAndSymlink(io, dir, link_name, file_name) catch |err| {
638 const d = options.diagnostics orelse return error.UnableToCreateSymLink;
639 try d.errors.append(d.allocator, .{ .unable_to_create_sym_link = .{
640 .code = err,
641 .file_name = try d.allocator.dupe(u8, file_name),
642 .link_name = try d.allocator.dupe(u8, link_name),
643 } });
644 };
645 },
646 }
647 }
648}
649
650fn createDirAndFile(io: Io, dir: Io.Dir, file_name: []const u8, permissions: Io.File.Permissions) !Io.File {
651 const fs_file = dir.createFile(io, file_name, .{ .exclusive = true, .permissions = permissions }) catch |err| {
652 if (err == error.FileNotFound) {
653 if (std.fs.path.dirname(file_name)) |dir_name| {
654 try dir.createDirPath(io, dir_name);
655 return try dir.createFile(io, file_name, .{ .exclusive = true, .permissions = permissions });
656 }
657 }
658 return err;
659 };
660 return fs_file;
661}
662
663// Creates a symbolic link at path `file_name` which points to `link_name`.
664fn createDirAndSymlink(io: Io, dir: Io.Dir, link_name: []const u8, file_name: []const u8) !void {
665 dir.symLink(io, link_name, file_name, .{}) catch |err| {
666 if (err == error.FileNotFound) {
667 if (std.fs.path.dirname(file_name)) |dir_name| {
668 try dir.createDirPath(io, dir_name);
669 return try dir.symLink(io, link_name, file_name, .{});
670 }
671 }
672 return err;
673 };
674}
675
676fn sanitizePath(buffer: []u8, path: []const u8, strip_components: u32) error{Invalid}!usize {
677 if (path.len == 0 or path[0] == '/') return error.Invalid;
678 var i: usize = 0;
679 var c = strip_components;
680 var it = std.mem.tokenizeScalar(u8, path, '/');
681 while (it.next()) |component| {
682 if (std.mem.eql(u8, component, ".")) continue;
683 if (std.mem.eql(u8, component, "..")) {
684 if (i == 0) return error.Invalid;
685 while (true) {
686 const ends_with_slash = buffer[i - 1] == '/';
687 i -= 1;
688 if (ends_with_slash or i == 0) break;
689 }
690 continue;
691 }
692 if (c > 0) {
693 c -= 1;
694 continue;
695 }
696 if (i > 0) {
697 buffer[i] = '/';
698 i += 1;
699 }
700 @memcpy(buffer[i..][0..component.len], component);
701 i += component.len;
702 }
703 if (c > 0) return error.Invalid;
704 return i;
705}
706
707fn testSanitizePath(expected: []const u8, input: []const u8, strip: u32) !void {
708 var buffer: [Io.Dir.max_path_bytes]u8 = undefined;
709 const result = buffer[0..try sanitizePath(&buffer, input, strip)];
710 try testing.expectEqualStrings(expected, result);
711}
712
713fn testSanitizePathError(expected: anyerror, input: []const u8, strip: u32) !void {
714 var buffer: [Io.Dir.max_path_bytes]u8 = undefined;
715 try testing.expectError(expected, sanitizePath(&buffer, input, strip));
716}
717
718test sanitizePath {
719 try testSanitizePath("a/b/c", "a/b/c", 0);
720 try testSanitizePath("a/b/c", "a/x/y/../../b/c", 0);
721 try testSanitizePath("b/c", "a/b/c", 1);
722 try testSanitizePath("c", "a/b/c", 2);
723 try testSanitizePath("", "a/b/c", 3);
724 try testSanitizePath("", "a/b/c/../../..", 0);
725 try testSanitizePathError(error.Invalid, "a/b/c", 4);
726 try testSanitizePathError(error.Invalid, "..", 0);
727 try testSanitizePathError(error.Invalid, "a/b/../../..", 0);
728 try testSanitizePathError(error.Invalid, "a/b/../..", 1);
729}
730
731test PaxIterator {
732 const Attr = struct {
733 kind: PaxAttributeKind,
734 value: []const u8 = undefined,
735 err: ?anyerror = null,
736 };
737 const long_path: *const [1000]u8 = comptime path: {
738 const buf: [100][10]u8 = @splat("0123456789".*);
739 break :path @ptrCast(&buf);
740 };
741 const cases = [_]struct {
742 data: []const u8,
743 attrs: []const Attr,
744 err: ?anyerror = null,
745 }{
746 .{ // valid but unknown keys
747 .data =
748 \\30 mtime=1350244992.023960108
749 \\6 k=1
750 \\13 key1=val1
751 \\10 a=name
752 \\9 a=name
753 \\
754 ,
755 .attrs = &[_]Attr{},
756 },
757 .{ // mix of known and unknown keys
758 .data =
759 \\6 k=1
760 \\13 path=name
761 \\17 linkpath=link
762 \\13 key1=val1
763 \\12 size=123
764 \\13 key2=val2
765 \\
766 ,
767 .attrs = &[_]Attr{
768 .{ .kind = .path, .value = "name" },
769 .{ .kind = .linkpath, .value = "link" },
770 .{ .kind = .size, .value = "123" },
771 },
772 },
773 .{ // too short size of the second key-value pair
774 .data =
775 \\13 path=name
776 \\10 linkpath=value
777 \\
778 ,
779 .attrs = &[_]Attr{
780 .{ .kind = .path, .value = "name" },
781 },
782 .err = error.UnexpectedEndOfStream,
783 },
784 .{ // too long size of the second key-value pair
785 .data =
786 \\13 path=name
787 \\6 k=1
788 \\19 linkpath=value
789 \\
790 ,
791 .attrs = &[_]Attr{
792 .{ .kind = .path, .value = "name" },
793 },
794 .err = error.UnexpectedEndOfStream,
795 },
796
797 .{ // too long size of the second key-value pair
798 .data =
799 \\13 path=name
800 \\19 linkpath=value
801 \\6 k=1
802 \\
803 ,
804 .attrs = &[_]Attr{
805 .{ .kind = .path, .value = "name" },
806 .{ .kind = .linkpath, .err = error.PaxInvalidAttributeEnd },
807 },
808 },
809 .{ // null in keyword is not valid
810 .data = "13 path=name\n" ++ "7 k\x00b=1\n",
811 .attrs = &[_]Attr{
812 .{ .kind = .path, .value = "name" },
813 },
814 .err = error.PaxNullInKeyword,
815 },
816 .{ // null in value is not valid
817 .data = "23 path=name\x00with null\n",
818 .attrs = &[_]Attr{
819 .{ .kind = .path, .err = error.PaxNullInValue },
820 },
821 },
822 .{ // 1000 characters path
823 .data = "1011 path=" ++ long_path ++ "\n",
824 .attrs = &[_]Attr{
825 .{ .kind = .path, .value = long_path },
826 },
827 },
828 };
829 var buffer: [1024]u8 = undefined;
830
831 outer: for (cases) |case| {
832 var reader: Io.Reader = .fixed(case.data);
833 var it: PaxIterator = .{
834 .size = case.data.len,
835 .reader = &reader,
836 };
837
838 var i: usize = 0;
839 while (it.next() catch |err| {
840 if (case.err) |e| {
841 try testing.expectEqual(e, err);
842 continue;
843 }
844 return err;
845 }) |attr| : (i += 1) {
846 const exp = case.attrs[i];
847 try testing.expectEqual(exp.kind, attr.kind);
848 const value = attr.value(&buffer) catch |err| {
849 if (exp.err) |e| {
850 try testing.expectEqual(e, err);
851 break :outer;
852 }
853 return err;
854 };
855 try testing.expectEqualStrings(exp.value, value);
856 }
857 try testing.expectEqual(case.attrs.len, i);
858 try testing.expect(case.err == null);
859 }
860}
861
862test "header parse size" {
863 const cases = [_]struct {
864 in: []const u8,
865 want: u64 = 0,
866 err: ?anyerror = null,
867 }{
868 // Test base-256 (binary) encoded values.
869 .{ .in = "", .want = 0 },
870 .{ .in = "\x80", .want = 0 },
871 .{ .in = "\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", .want = 1 },
872 .{ .in = "\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02", .want = 0x0102 },
873 .{ .in = "\x80\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08", .want = 0x0102030405060708 },
874 .{ .in = "\x80\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09", .err = error.TarNumericValueTooBig },
875 .{ .in = "\x80\x00\x00\x00\x07\x76\xa2\x22\xeb\x8a\x72\x61", .want = 537795476381659745 },
876 .{ .in = "\x80\x80\x80\x00\x01\x02\x03\x04\x05\x06\x07\x08", .err = error.TarNumericValueTooBig },
877
878 // // Test base-8 (octal) encoded values.
879 .{ .in = "00000000227\x00", .want = 0o227 },
880 .{ .in = " 000000227\x00", .want = 0o227 },
881 .{ .in = "00000000228\x00", .err = error.TarHeader },
882 .{ .in = "11111111111\x00", .want = 0o11111111111 },
883 };
884
885 for (cases) |case| {
886 var bytes: [Header.SIZE]u8 = @splat(0);
887 @memcpy(bytes[124 .. 124 + case.in.len], case.in);
888 var header = Header{ .bytes = &bytes };
889 if (case.err) |err| {
890 try testing.expectError(err, header.size());
891 } else {
892 try testing.expectEqual(case.want, try header.size());
893 }
894 }
895}
896
897test "header parse mode" {
898 const cases = [_]struct {
899 in: []const u8,
900 want: u64 = 0,
901 err: ?anyerror = null,
902 }{
903 .{ .in = "0000644\x00", .want = 0o644 },
904 .{ .in = "0000777\x00", .want = 0o777 },
905 .{ .in = "7777777\x00", .want = 0o7777777 },
906 .{ .in = "7777778\x00", .err = error.TarHeader },
907 .{ .in = "77777777", .want = 0o77777777 },
908 .{ .in = "777777777777", .want = 0o77777777 },
909 };
910 for (cases) |case| {
911 var bytes: [Header.SIZE]u8 = @splat(0);
912 @memcpy(bytes[100 .. 100 + case.in.len], case.in);
913 var header = Header{ .bytes = &bytes };
914 if (case.err) |err| {
915 try testing.expectError(err, header.mode());
916 } else {
917 try testing.expectEqual(case.want, try header.mode());
918 }
919 }
920}
921
922test "create file and symlink" {
923 const io = testing.io;
924
925 var root = testing.tmpDir(.{});
926 defer root.cleanup();
927
928 var file = try createDirAndFile(io, root.dir, "file1", .default_file);
929 file.close(io);
930 file = try createDirAndFile(io, root.dir, "a/b/c/file2", .default_file);
931 file.close(io);
932
933 createDirAndSymlink(io, root.dir, "a/b/c/file2", "symlink1") catch |err| switch (err) {
934 // On Windows, symlinks require developer mode/admin privileges and the underlying filesystem must support symlinks
935 error.AccessDenied, error.PermissionDenied, error.FileSystem => if (builtin.os.tag == .windows) return error.SkipZigTest else return err,
936 else => return err,
937 };
938 try createDirAndSymlink(io, root.dir, "../../../file1", "d/e/f/symlink2");
939
940 // Danglink symlnik, file created later
941 try createDirAndSymlink(io, root.dir, "../../../g/h/i/file4", "j/k/l/symlink3");
942 file = try createDirAndFile(io, root.dir, "g/h/i/file4", .default_file);
943 file.close(io);
944}
945
946test Iterator {
947 // Example tar file is created from this tree structure:
948 // $ tree example
949 // example
950 // ├── a
951 // │   └── file
952 // ├── b
953 // │   └── symlink -> ../a/file
954 // └── empty
955 // $ cat example/a/file
956 // content
957 // $ tar -cf example.tar example
958 // $ tar -tvf example.tar
959 // example/
960 // example/b/
961 // example/b/symlink -> ../a/file
962 // example/a/
963 // example/a/file
964 // example/empty/
965
966 const data = @embedFile("tar/testdata/example.tar");
967 var reader: Io.Reader = .fixed(data);
968
969 // User provided buffers to the iterator
970 var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
971 var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
972 // Create iterator
973 var it: Iterator = .init(&reader, .{
974 .file_name_buffer = &file_name_buffer,
975 .link_name_buffer = &link_name_buffer,
976 });
977 // Iterate over files in example.tar
978 var file_no: usize = 0;
979 while (try it.next()) |file| : (file_no += 1) {
980 switch (file.kind) {
981 .directory => {
982 switch (file_no) {
983 0 => try testing.expectEqualStrings("example/", file.name),
984 1 => try testing.expectEqualStrings("example/b/", file.name),
985 3 => try testing.expectEqualStrings("example/a/", file.name),
986 5 => try testing.expectEqualStrings("example/empty/", file.name),
987 else => unreachable,
988 }
989 },
990 .file => {
991 try testing.expectEqualStrings("example/a/file", file.name);
992 var buf: [16]u8 = undefined;
993 var w: Io.Writer = .fixed(&buf);
994 try it.streamRemaining(file, &w);
995 try testing.expectEqualStrings("content\n", w.buffered());
996 },
997 .sym_link => {
998 try testing.expectEqualStrings("example/b/symlink", file.name);
999 try testing.expectEqualStrings("../a/file", file.link_name);
1000 },
1001 }
1002 }
1003}
1004
1005test extract {
1006 const io = testing.io;
1007 // Example tar file is created from this tree structure:
1008 // $ tree example
1009 // example
1010 // ├── a
1011 // │   └── file
1012 // ├── b
1013 // │   └── symlink -> ../a/file
1014 // └── empty
1015 // $ cat example/a/file
1016 // content
1017 // $ tar -cf example.tar example
1018 // $ tar -tvf example.tar
1019 // example/
1020 // example/b/
1021 // example/b/symlink -> ../a/file
1022 // example/a/
1023 // example/a/file
1024 // example/empty/
1025
1026 const data = @embedFile("tar/testdata/example.tar");
1027 var reader: Io.Reader = .fixed(data);
1028
1029 var tmp = testing.tmpDir(.{ .follow_symlinks = false });
1030 defer tmp.cleanup();
1031 const dir = tmp.dir;
1032
1033 // Save tar from reader to the file system `dir`
1034 extract(io, dir, &reader, .{
1035 .mode_mode = .ignore,
1036 .strip_components = 1,
1037 .exclude_empty_directories = true,
1038 }) catch |err| {
1039 // Skip on platform which don't support symlinks
1040 if (err == error.UnableToCreateSymLink) return error.SkipZigTest;
1041 return err;
1042 };
1043
1044 try testing.expectError(error.FileNotFound, dir.statFile(io, "empty", .{}));
1045 try testing.expect((try dir.statFile(io, "a/file", .{})).kind == .file);
1046 try testing.expect((try dir.statFile(io, "b/symlink", .{})).kind == .file); // statFile follows symlink
1047
1048 var buf: [32]u8 = undefined;
1049 try testing.expectEqualSlices(
1050 u8,
1051 "../a/file",
1052 normalizePath(buf[0..try dir.readLink(io, "b/symlink", &buf)]),
1053 );
1054}
1055
1056test "extract root_dir" {
1057 const io = testing.io;
1058 const data = @embedFile("tar/testdata/example.tar");
1059 var reader: Io.Reader = .fixed(data);
1060
1061 // with strip_components = 1
1062 {
1063 var tmp = testing.tmpDir(.{ .follow_symlinks = false });
1064 defer tmp.cleanup();
1065 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
1066 defer diagnostics.deinit();
1067
1068 extract(io, tmp.dir, &reader, .{
1069 .strip_components = 1,
1070 .diagnostics = &diagnostics,
1071 }) catch |err| {
1072 // Skip on platform which don't support symlinks
1073 if (err == error.UnableToCreateSymLink) return error.SkipZigTest;
1074 return err;
1075 };
1076
1077 // there is no root_dir
1078 try testing.expectEqual(0, diagnostics.root_dir.len);
1079 try testing.expectEqual(5, diagnostics.entries);
1080 }
1081
1082 // with strip_components = 0
1083 {
1084 reader = .fixed(data);
1085 var tmp = testing.tmpDir(.{ .follow_symlinks = false });
1086 defer tmp.cleanup();
1087 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
1088 defer diagnostics.deinit();
1089
1090 extract(io, tmp.dir, &reader, .{
1091 .strip_components = 0,
1092 .diagnostics = &diagnostics,
1093 }) catch |err| {
1094 // Skip on platform which don't support symlinks
1095 if (err == error.UnableToCreateSymLink) return error.SkipZigTest;
1096 return err;
1097 };
1098
1099 // root_dir found
1100 try testing.expectEqualStrings("example", diagnostics.root_dir);
1101 try testing.expectEqual(6, diagnostics.entries);
1102 }
1103}
1104
1105test "findRoot with single file archive" {
1106 const io = testing.io;
1107 const data = @embedFile("tar/testdata/22752.tar");
1108 var reader: Io.Reader = .fixed(data);
1109
1110 var tmp = testing.tmpDir(.{});
1111 defer tmp.cleanup();
1112
1113 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
1114 defer diagnostics.deinit();
1115 try extract(io, tmp.dir, &reader, .{ .diagnostics = &diagnostics });
1116
1117 try testing.expectEqualStrings("", diagnostics.root_dir);
1118}
1119
1120test "findRoot without explicit root dir" {
1121 const io = testing.io;
1122 const data = @embedFile("tar/testdata/19820.tar");
1123 var reader: Io.Reader = .fixed(data);
1124
1125 var tmp = testing.tmpDir(.{});
1126 defer tmp.cleanup();
1127
1128 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
1129 defer diagnostics.deinit();
1130 try extract(io, tmp.dir, &reader, .{ .diagnostics = &diagnostics });
1131
1132 try testing.expectEqualStrings("root", diagnostics.root_dir);
1133}
1134
1135test "extract strip_components" {
1136 const io = testing.io;
1137 const data = @embedFile("tar/testdata/example.tar");
1138 var reader: Io.Reader = .fixed(data);
1139
1140 var tmp = testing.tmpDir(.{ .follow_symlinks = false });
1141 defer tmp.cleanup();
1142 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
1143 defer diagnostics.deinit();
1144
1145 extract(io, tmp.dir, &reader, .{
1146 .strip_components = 3,
1147 .diagnostics = &diagnostics,
1148 }) catch |err| {
1149 // Skip on platform which don't support symlinks
1150 if (err == error.UnableToCreateSymLink) return error.SkipZigTest;
1151 return err;
1152 };
1153
1154 try testing.expectEqual(2, diagnostics.errors.items.len);
1155 try testing.expectEqualStrings("example/b/symlink", diagnostics.errors.items[0].components_outside_stripped_prefix.file_name);
1156 try testing.expectEqualStrings("example/a/file", diagnostics.errors.items[1].components_outside_stripped_prefix.file_name);
1157}
1158
1159fn normalizePath(bytes: []u8) []u8 {
1160 const canonical_sep = std.fs.path.sep_posix;
1161 if (std.fs.path.sep == canonical_sep) return bytes;
1162 std.mem.replaceScalar(u8, bytes, std.fs.path.sep, canonical_sep);
1163 return bytes;
1164}
1165
1166// File system mode based on tar header mode and mode_mode options.
1167fn filePermissions(mode: u32, options: ExtractOptions) Io.File.Permissions {
1168 return if (!Io.File.Permissions.has_executable_bit or options.mode_mode == .ignore or (mode & 0o100) == 0)
1169 .default_file
1170 else
1171 .executable_file;
1172}
1173
1174test filePermissions {
1175 if (!Io.File.Permissions.has_executable_bit) return error.SkipZigTest;
1176 try testing.expectEqual(Io.File.Permissions.default_file, filePermissions(0o744, .{ .mode_mode = .ignore }));
1177 try testing.expectEqual(Io.File.Permissions.executable_file, filePermissions(0o744, .{}));
1178 try testing.expectEqual(Io.File.Permissions.default_file, filePermissions(0o644, .{}));
1179 try testing.expectEqual(Io.File.Permissions.default_file, filePermissions(0o655, .{}));
1180}
1181
1182test "executable bit" {
1183 if (!Io.File.Permissions.has_executable_bit) return error.SkipZigTest;
1184
1185 const io = testing.io;
1186 const S = std.posix.S;
1187 const data = @embedFile("tar/testdata/example.tar");
1188
1189 for ([_]ExtractOptions.ModeMode{ .ignore, .executable_bit_only }) |opt| {
1190 var reader: Io.Reader = .fixed(data);
1191
1192 var tmp = testing.tmpDir(.{ .follow_symlinks = false });
1193 //defer tmp.cleanup();
1194
1195 extract(io, tmp.dir, &reader, .{
1196 .strip_components = 1,
1197 .exclude_empty_directories = true,
1198 .mode_mode = opt,
1199 }) catch |err| {
1200 // Skip on platform which don't support symlinks
1201 if (err == error.UnableToCreateSymLink) return error.SkipZigTest;
1202 return err;
1203 };
1204
1205 const fs = try tmp.dir.statFile(io, "a/file", .{});
1206 try testing.expect(fs.kind == .file);
1207
1208 const mode = fs.permissions.toMode();
1209
1210 if (opt == .executable_bit_only) {
1211 // Executable bit is set for user, group and others
1212 try testing.expect(mode & S.IXUSR > 0);
1213 try testing.expect(mode & S.IXGRP > 0);
1214 try testing.expect(mode & S.IXOTH > 0);
1215 }
1216 if (opt == .ignore) {
1217 try testing.expect(mode & S.IXUSR == 0);
1218 try testing.expect(mode & S.IXGRP == 0);
1219 try testing.expect(mode & S.IXOTH == 0);
1220 }
1221 }
1222}
1223
1224test {
1225 _ = @import("tar/test.zig");
1226 _ = Writer;
1227 _ = Diagnostics;
1228}