| ... | @@ -82,6 +82,10 @@ pub const Header = struct { | ... | @@ -82,6 +82,10 @@ pub const Header = struct { |
| 82 | contiguous = '7', | 82 | contiguous = '7', |
| 83 | global_extended_header = 'g', | 83 | global_extended_header = 'g', |
| 84 | extended_header = 'x', | 84 | extended_header = 'x', |
| | 85 | // Types 'L' and 'K' are used by the GNU format for a meta file |
| | 86 | // used to store the path or link name for the next file. |
| | 87 | gnu_long_name = 'L', |
| | 88 | gnu_long_link = 'K', |
| 85 | _, | 89 | _, |
| 86 | }; | 90 | }; |
| 87 | | 91 | |
| ... | @@ -119,7 +123,8 @@ pub const Header = struct { | ... | @@ -119,7 +123,8 @@ pub const Header = struct { |
| 119 | } | 123 | } |
| 120 | | 124 | |
| 121 | pub fn is_ustar(header: Header) bool { | 125 | pub fn is_ustar(header: Header) bool { |
| 122 | return std.mem.eql(u8, header.bytes[257..][0..6], "ustar\x00"); | 126 | const magic = header.bytes[257..][0..6]; |
| | 127 | return std.mem.eql(u8, magic[0..5], "ustar") and (magic[5] == 0 or magic[5] == ' '); |
| 123 | } | 128 | } |
| 124 | | 129 | |
| 125 | pub fn prefix(header: Header) []const u8 { | 130 | pub fn prefix(header: Header) []const u8 { |
| ... | @@ -133,12 +138,7 @@ pub const Header = struct { | ... | @@ -133,12 +138,7 @@ pub const Header = struct { |
| 133 | } | 138 | } |
| 134 | | 139 | |
| 135 | fn str(header: Header, start: usize, len: usize) []const u8 { | 140 | fn str(header: Header, start: usize, len: usize) []const u8 { |
| 136 | const end = start + len; | 141 | return nullStr(header.bytes[start .. start + len]); |
| 137 | var i: usize = start; | | |
| 138 | while (i < end) : (i += 1) { | | |
| 139 | if (header.bytes[i] == 0) break; | | |
| 140 | } | | |
| 141 | return header.bytes[start..i]; | | |
| 142 | } | 142 | } |
| 143 | | 143 | |
| 144 | fn numeric(header: Header, start: usize, len: usize) !u64 { | 144 | fn numeric(header: Header, start: usize, len: usize) !u64 { |
| ... | @@ -190,6 +190,14 @@ pub const Header = struct { | ... | @@ -190,6 +190,14 @@ pub const Header = struct { |
| 190 | } | 190 | } |
| 191 | }; | 191 | }; |
| 192 | | 192 | |
| | 193 | // break string on first null char |
| | 194 | fn nullStr(str: []const u8) []const u8 { |
| | 195 | for (str, 0..) |c, i| { |
| | 196 | if (c == 0) return str[0..i]; |
| | 197 | } |
| | 198 | return str; |
| | 199 | } |
| | 200 | |
| 193 | fn BufferedReader(comptime ReaderType: type) type { | 201 | fn BufferedReader(comptime ReaderType: type) type { |
| 194 | return struct { | 202 | return struct { |
| 195 | unbuffered_reader: ReaderType, | 203 | unbuffered_reader: ReaderType, |
| ... | @@ -274,7 +282,7 @@ fn BufferedReader(comptime ReaderType: type) type { | ... | @@ -274,7 +282,7 @@ fn BufferedReader(comptime ReaderType: type) type { |
| 274 | reader: *Self, | 282 | reader: *Self, |
| 275 | auto_advance: bool, | 283 | auto_advance: bool, |
| 276 | | 284 | |
| 277 | fn next(self: *@This()) !?[]const u8 { | 285 | pub fn next(self: *@This()) !?[]const u8 { |
| 278 | if (self.offset >= self.size) return null; | 286 | if (self.offset >= self.size) return null; |
| 279 | | 287 | |
| 280 | const temp = try self.reader.readChunk(self.chunk_size - self.offset); | 288 | const temp = try self.reader.readChunk(self.chunk_size - self.offset); |
| ... | @@ -284,22 +292,22 @@ fn BufferedReader(comptime ReaderType: type) type { | ... | @@ -284,22 +292,22 @@ fn BufferedReader(comptime ReaderType: type) type { |
| 284 | return slice; | 292 | return slice; |
| 285 | } | 293 | } |
| 286 | | 294 | |
| 287 | fn advance(self: *@This(), len: usize) !void { | 295 | pub fn advance(self: *@This(), len: usize) !void { |
| 288 | self.offset += len; | 296 | self.offset += len; |
| 289 | try self.reader.skip(len); | 297 | try self.reader.skip(len); |
| 290 | } | 298 | } |
| 291 | | 299 | |
| 292 | fn byte(self: *@This()) u8 { | 300 | pub fn byte(self: *@This()) u8 { |
| 293 | return self.reader.buffer[self.reader.start]; | 301 | return self.reader.buffer[self.reader.start]; |
| 294 | } | 302 | } |
| 295 | | 303 | |
| 296 | fn copy(self: *@This(), dst: []u8) ![]const u8 { | 304 | pub fn copy(self: *@This(), dst: []u8) ![]const u8 { |
| 297 | _ = try self.reader.copy(dst); | 305 | _ = try self.reader.copy(dst); |
| 298 | self.offset += dst.len; | 306 | self.offset += dst.len; |
| 299 | return dst; | 307 | return dst; |
| 300 | } | 308 | } |
| 301 | | 309 | |
| 302 | fn remainingSize(self: *@This()) usize { | 310 | pub fn remainingSize(self: *@This()) usize { |
| 303 | return self.size - self.offset; | 311 | return self.size - self.offset; |
| 304 | } | 312 | } |
| 305 | }; | 313 | }; |
| ... | @@ -443,6 +451,14 @@ fn Iterator(comptime ReaderType: type) type { | ... | @@ -443,6 +451,14 @@ fn Iterator(comptime ReaderType: type) type { |
| 443 | } | 451 | } |
| 444 | try self.reader.skipPadding(file_size); | 452 | try self.reader.skipPadding(file_size); |
| 445 | }, | 453 | }, |
| | 454 | .gnu_long_name => { |
| | 455 | file.name = nullStr(try self.reader.copy(try self.attrs.alloc(file_size))); |
| | 456 | try self.reader.skipPadding(file_size); |
| | 457 | }, |
| | 458 | .gnu_long_link => { |
| | 459 | file.link_name = nullStr(try self.reader.copy(try self.attrs.alloc(file_size))); |
| | 460 | try self.reader.skipPadding(file_size); |
| | 461 | }, |
| 446 | .hard_link => return error.TarUnsupportedFileType, | 462 | .hard_link => return error.TarUnsupportedFileType, |
| 447 | else => { | 463 | else => { |
| 448 | const d = self.diagnostics orelse return error.TarUnsupportedFileType; | 464 | const d = self.diagnostics orelse return error.TarUnsupportedFileType; |
| ... | @@ -624,22 +640,20 @@ test "parsePaxAttribute" { | ... | @@ -624,22 +640,20 @@ test "parsePaxAttribute" { |
| 624 | | 640 | |
| 625 | const TestCase = struct { | 641 | const TestCase = struct { |
| 626 | const File = struct { | 642 | const File = struct { |
| 627 | const empty_string = &[0]u8{}; | | |
| 628 | | | |
| 629 | name: []const u8, | 643 | name: []const u8, |
| 630 | size: usize = 0, | 644 | size: usize = 0, |
| 631 | link_name: []const u8 = empty_string, | 645 | link_name: []const u8 = &[0]u8{}, |
| 632 | file_type: Header.FileType = .normal, | 646 | file_type: Header.FileType = .normal, |
| 633 | truncated: bool = false, // when there is no file body, just header, usefull for huge files | 647 | truncated: bool = false, // when there is no file body, just header, usefull for huge files |
| 634 | }; | 648 | }; |
| 635 | | 649 | |
| 636 | path: []const u8, | 650 | path: []const u8, // path to the tar archive file on dis |
| 637 | files: []const File = &[_]TestCase.File{}, | 651 | files: []const File = &[_]TestCase.File{}, // expected files to found in archive |
| 638 | chksums: []const []const u8 = &[_][]const u8{}, | 652 | chksums: []const []const u8 = &[_][]const u8{}, // chksums of files content |
| 639 | err: ?anyerror = null, | 653 | err: ?anyerror = null, // parsing should fail with this error |
| 640 | }; | 654 | }; |
| 641 | | 655 | |
| 642 | test "Go test cases" { | 656 | test "tar: Go test cases" { |
| 643 | const test_dir = try std.fs.openDirAbsolute("/usr/local/go/src/archive/tar/testdata", .{}); | 657 | const test_dir = try std.fs.openDirAbsolute("/usr/local/go/src/archive/tar/testdata", .{}); |
| 644 | const cases = [_]TestCase{ | 658 | const cases = [_]TestCase{ |
| 645 | .{ | 659 | .{ |
| ... | @@ -718,12 +732,6 @@ test "Go test cases" { | ... | @@ -718,12 +732,6 @@ test "Go test cases" { |
| 718 | .{ | 732 | .{ |
| 719 | // pax attribute don't end with \n | 733 | // pax attribute don't end with \n |
| 720 | .path = "pax-bad-hdr-file.tar", | 734 | .path = "pax-bad-hdr-file.tar", |
| 721 | // .files = &[_]TestCase.File{ | | |
| 722 | // .{ | | |
| 723 | // .name = "PAX1/PAX1/long-path-name", | | |
| 724 | // .size = 684, | | |
| 725 | // }, | | |
| 726 | // }, | | |
| 727 | .err = error.InvalidPaxAttribute, | 735 | .err = error.InvalidPaxAttribute, |
| 728 | }, | 736 | }, |
| 729 | // | 737 | // |
| ... | @@ -808,9 +816,16 @@ test "Go test cases" { | ... | @@ -808,9 +816,16 @@ test "Go test cases" { |
| 808 | }, | 816 | }, |
| 809 | .{ | 817 | .{ |
| 810 | .path = "gnu-multi-hdrs.tar", | 818 | .path = "gnu-multi-hdrs.tar", |
| 811 | .err = error.TarUnsupportedFileType, | 819 | .files = &[_]TestCase.File{ |
| | 820 | .{ |
| | 821 | .name = "GNU2/GNU2/long-path-name", |
| | 822 | .link_name = "GNU4/GNU4/long-linkpath-name", |
| | 823 | .file_type = .symbolic_link, |
| | 824 | }, |
| | 825 | }, |
| 812 | }, | 826 | }, |
| 813 | .{ | 827 | .{ |
| | 828 | // has gnu type D (directory) and S (sparse) blocks |
| 814 | .path = "gnu-incremental.tar", | 829 | .path = "gnu-incremental.tar", |
| 815 | .err = error.TarUnsupportedFileType, | 830 | .err = error.TarUnsupportedFileType, |
| 816 | }, | 831 | }, |
| ... | @@ -825,23 +840,22 @@ test "Go test cases" { | ... | @@ -825,23 +840,22 @@ test "Go test cases" { |
| 825 | }, | 840 | }, |
| 826 | }, | 841 | }, |
| 827 | }, | 842 | }, |
| 828 | // .{ | 843 | .{ |
| 829 | // .path = "gnu-long-nul.tar", | 844 | .path = "gnu-long-nul.tar", |
| 830 | // .files = &[_]TestCase.File{ | 845 | .files = &[_]TestCase.File{ |
| 831 | // .{ | 846 | .{ |
| 832 | // .name = "012233456789", | 847 | .name = "0123456789", |
| 833 | // }, | 848 | }, |
| 834 | // }, | 849 | }, |
| 835 | // }, | 850 | }, |
| 836 | // .{ | 851 | .{ |
| 837 | // .path = "gnu-utf8.tar", | 852 | .path = "gnu-utf8.tar", |
| 838 | // .files = &[_]TestCase.File{ | 853 | .files = &[_]TestCase.File{ |
| 839 | // .{ | 854 | .{ |
| 840 | // .name = "012233456789", | 855 | .name = "☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹", |
| 841 | // }, | 856 | }, |
| 842 | // }, | 857 | }, |
| 843 | // }, | 858 | }, |
| 844 | // | | |
| 845 | .{ | 859 | .{ |
| 846 | .path = "gnu-not-utf8.tar", | 860 | .path = "gnu-not-utf8.tar", |
| 847 | .files = &[_]TestCase.File{ | 861 | .files = &[_]TestCase.File{ |
| ... | @@ -851,19 +865,47 @@ test "Go test cases" { | ... | @@ -851,19 +865,47 @@ test "Go test cases" { |
| 851 | }, | 865 | }, |
| 852 | }, | 866 | }, |
| 853 | .{ | 867 | .{ |
| 854 | .path = "neg-size.tar", | 868 | // null in pax key |
| 855 | .err = error.TarHeader, | 869 | .path = "pax-nul-xattrs.tar", |
| | 870 | .err = error.InvalidPaxAttribute, |
| 856 | }, | 871 | }, |
| 857 | .{ | 872 | .{ |
| 858 | .path = "pax-nul-path.tar", | 873 | .path = "pax-nul-path.tar", |
| 859 | .err = error.InvalidPaxAttribute, | 874 | .err = error.InvalidPaxAttribute, |
| 860 | }, | 875 | }, |
| 861 | .{ | 876 | .{ |
| 862 | .path = "pax-nul-xattrs.tar", | 877 | .path = "neg-size.tar", |
| 863 | .err = error.InvalidPaxAttribute, | 878 | .err = error.TarHeader, |
| | 879 | }, |
| | 880 | .{ |
| | 881 | .path = "issue10968.tar", |
| | 882 | .err = error.TarHeader, |
| | 883 | }, |
| | 884 | .{ |
| | 885 | .path = "issue11169.tar", |
| | 886 | .err = error.TarHeader, |
| | 887 | }, |
| | 888 | .{ |
| | 889 | .path = "issue12435.tar", |
| | 890 | .err = error.TarHeaderChksum, |
| | 891 | }, |
| | 892 | .{ |
| | 893 | // has magic with space at end instead of null |
| | 894 | .path = "invalid-go17.tar", |
| | 895 | .files = &[_]TestCase.File{ |
| | 896 | .{ |
| | 897 | .name = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/foo", |
| | 898 | }, |
| | 899 | }, |
| | 900 | }, |
| | 901 | .{ |
| | 902 | .path = "ustar-file-devs.tar", |
| | 903 | .files = &[_]TestCase.File{ |
| | 904 | .{ |
| | 905 | .name = "file", |
| | 906 | }, |
| | 907 | }, |
| 864 | }, | 908 | }, |
| 865 | // TODO some files with errors: | | |
| 866 | // issue10968.tar, issue11169.tar, issue12435.tar | | |
| 867 | .{ | 909 | .{ |
| 868 | .path = "trailing-slash.tar", | 910 | .path = "trailing-slash.tar", |
| 869 | .files = &[_]TestCase.File{ | 911 | .files = &[_]TestCase.File{ |