authorgravatar for igor.anic@gmail.comIgor Anić <igor.anic@gmail.com> 2023-11-29 21:37:13+01:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-01-13 19:37:33-07:00
logc761dfc1761b38be8d1dc72dd4c0cbf07d2c0eed
treea8aabf72c25ff848494460d7c7874fb2b3420022
parent48b160c1bf75f602acabc3b43eca56b8aa4abf4f

tar: add gnu path and link extensions handling


1 files changed, 92 insertions(+), 50 deletions(-)

lib/std/tar.zig+92-50
...@@ -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 };
8791
...@@ -119,7 +123,8 @@ pub const Header = struct {...@@ -119,7 +123,8 @@ pub const Header = struct {
119 }123 }
120124
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 }
124129
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 }
134139
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 }
143143
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};
192192
193// break string on first null char
194fn 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
193fn BufferedReader(comptime ReaderType: type) type {201fn 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,
276284
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;
279287
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 }
286294
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 }
291299
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 }
295303
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 }
301309
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" {
624640
625const TestCase = struct {641const 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 files647 truncated: bool = false, // when there is no file body, just header, usefull for huge files
634 };648 };
635649
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};
641655
642test "Go test cases" {656test "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 \n733 // 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{