| ... | @@ -331,11 +331,6 @@ test "tar stripComponents" { | ... | @@ -331,11 +331,6 @@ test "tar stripComponents" { |
| 331 | try expectEqualStrings("c", try stripComponents("a/b/c", 2)); | 331 | try expectEqualStrings("c", try stripComponents("a/b/c", 2)); |
| 332 | } | 332 | } |
| 333 | | 333 | |
| 334 | fn noNull(str: []const u8) ![]const u8 { | | |
| 335 | if (std.mem.indexOfScalar(u8, str, 0)) |_| return error.InvalidPaxAttribute; | | |
| 336 | return str; | | |
| 337 | } | | |
| 338 | | | |
| 339 | test "tar run Go test cases" { | 334 | test "tar run Go test cases" { |
| 340 | const Case = struct { | 335 | const Case = struct { |
| 341 | const File = struct { | 336 | const File = struct { |
| ... | @@ -443,7 +438,7 @@ test "tar run Go test cases" { | ... | @@ -443,7 +438,7 @@ test "tar run Go test cases" { |
| 443 | .{ | 438 | .{ |
| 444 | // pax attribute don't end with \n | 439 | // pax attribute don't end with \n |
| 445 | .path = "pax-bad-hdr-file.tar", | 440 | .path = "pax-bad-hdr-file.tar", |
| 446 | .err = error.InvalidPaxAttribute, | 441 | .err = error.PaxInvalidAttributeEnd, |
| 447 | }, | 442 | }, |
| 448 | .{ | 443 | .{ |
| 449 | // size is in pax attribute | 444 | // size is in pax attribute |
| ... | @@ -579,11 +574,11 @@ test "tar run Go test cases" { | ... | @@ -579,11 +574,11 @@ test "tar run Go test cases" { |
| 579 | .{ | 574 | .{ |
| 580 | // null in pax key | 575 | // null in pax key |
| 581 | .path = "pax-nul-xattrs.tar", | 576 | .path = "pax-nul-xattrs.tar", |
| 582 | .err = error.InvalidPaxAttribute, | 577 | .err = error.PaxNullInKeyword, |
| 583 | }, | 578 | }, |
| 584 | .{ | 579 | .{ |
| 585 | .path = "pax-nul-path.tar", | 580 | .path = "pax-nul-path.tar", |
| 586 | .err = error.InvalidPaxAttribute, | 581 | .err = error.PaxNullInValue, |
| 587 | }, | 582 | }, |
| 588 | .{ | 583 | .{ |
| 589 | .path = "neg-size.tar", | 584 | .path = "neg-size.tar", |
| ... | @@ -715,7 +710,7 @@ fn paxReader(reader: anytype, size: usize) PaxReader(@TypeOf(reader)) { | ... | @@ -715,7 +710,7 @@ fn paxReader(reader: anytype, size: usize) PaxReader(@TypeOf(reader)) { |
| 715 | }; | 710 | }; |
| 716 | } | 711 | } |
| 717 | | 712 | |
| 718 | const PaxAttrKind = enum { | 713 | const PaxAttributeKind = enum { |
| 719 | path, | 714 | path, |
| 720 | linkpath, | 715 | linkpath, |
| 721 | size, | 716 | size, |
| ... | @@ -723,54 +718,50 @@ const PaxAttrKind = enum { | ... | @@ -723,54 +718,50 @@ const PaxAttrKind = enum { |
| 723 | | 718 | |
| 724 | fn PaxReader(comptime ReaderType: type) type { | 719 | fn PaxReader(comptime ReaderType: type) type { |
| 725 | return struct { | 720 | return struct { |
| 726 | size: usize, | 721 | size: usize, // cumulative size of all pax attributes |
| 727 | reader: ReaderType, | 722 | reader: ReaderType, |
| | 723 | // scratch buffer used for reading attribute length and keyword |
| | 724 | scratch: [128]u8 = undefined, |
| 728 | | 725 | |
| 729 | const Self = @This(); | 726 | const Self = @This(); |
| 730 | | 727 | |
| 731 | const Attr = struct { | 728 | const Attribute = struct { |
| 732 | kind: PaxAttrKind, | 729 | kind: PaxAttributeKind, |
| 733 | len: usize, | 730 | len: usize, // length of the attribute value |
| 734 | reader: ReaderType, | 731 | reader: ReaderType, // reader positioned at value start |
| 735 | | 732 | |
| 736 | // Copies pax attribute value into destination buffer. | 733 | // Copies pax attribute value into destination buffer. |
| 737 | // Must be called with destination buffer of size at least value_len. | 734 | // Must be called with destination buffer of size at least Attribute.len. |
| 738 | pub fn value(self: Attr, dst: []u8) ![]const u8 { | 735 | pub fn value(self: Attribute, dst: []u8) ![]const u8 { |
| 739 | assert(self.len <= dst.len); | 736 | assert(self.len <= dst.len); |
| 740 | const buf = dst[0..self.len]; | 737 | const buf = dst[0..self.len]; |
| 741 | const n = try self.reader.readAll(buf); | 738 | const n = try self.reader.readAll(buf); |
| 742 | if (n < self.len) return error.UnexpectedEndOfStream; | 739 | if (n < self.len) return error.UnexpectedEndOfStream; |
| 743 | try checkRecordEnd(self.reader); | 740 | try validateAttributeEnding(self.reader); |
| 744 | return noNull(buf); | 741 | if (hasNull(buf)) return error.PaxNullInValue; |
| | 742 | return buf; |
| 745 | } | 743 | } |
| 746 | }; | 744 | }; |
| 747 | | 745 | |
| 748 | // Iterates over pax records. Returns known records. Caller has to call | 746 | // Iterates over pax attributes. Returns known only known attributes. |
| 749 | // value in Record, to advance reader across value. | 747 | // Caller has to call value in Attribute, to advance reader across value. |
| 750 | pub fn next(self: *Self) !?Attr { | 748 | pub fn next(self: *Self) !?Attribute { |
| 751 | var buf: [128]u8 = undefined; | 749 | // Pax extended header consists of one or more attributes, each constructed as follows: |
| 752 | var fbs = std.io.fixedBufferStream(&buf); | | |
| 753 | | | |
| 754 | // An extended header consists of one or more records, each constructed as follows: | | |
| 755 | // "%d %s=%s\n", <length>, <keyword>, <value> | 750 | // "%d %s=%s\n", <length>, <keyword>, <value> |
| 756 | while (self.size > 0) { | 751 | while (self.size > 0) { |
| 757 | fbs.reset(); | 752 | const length_buf = try self.readUntil(' '); |
| 758 | // read length | 753 | const length = try std.fmt.parseInt(usize, length_buf, 10); // record length in bytes |
| 759 | try self.reader.streamUntilDelimiter(fbs.writer(), ' ', null); | 754 | |
| 760 | const rec_len = try std.fmt.parseInt(usize, fbs.getWritten(), 10); // record len in bytes | 755 | const keyword = try self.readUntil('='); |
| 761 | var pos = try fbs.getPos() + 1; // bytes used for record len + separator | 756 | if (hasNull(keyword)) return error.PaxNullInKeyword; |
| 762 | fbs.reset(); | 757 | |
| 763 | // read keyword | 758 | // calculate value_len |
| 764 | try self.reader.streamUntilDelimiter(fbs.writer(), '=', null); | 759 | const value_start = length_buf.len + keyword.len + 2; // 2 separators |
| 765 | const keyword = fbs.getWritten(); | 760 | if (length < value_start + 1 or self.size < length) return error.UnexpectedEndOfStream; |
| 766 | pos += try fbs.getPos() + 1; // keyword bytes + separator | 761 | const value_len = length - value_start - 1; // \n separator at end |
| 767 | try checkKeyword(keyword); | 762 | self.size -= length; |
| 768 | // get value_len | 763 | |
| 769 | if (rec_len < pos + 1) return error.InvalidPaxAttribute; | 764 | const kind: PaxAttributeKind = if (eql(keyword, "path")) |
| 770 | const value_len = rec_len - pos - 1; // pos = start of value, -1 => without \n record terminator | | |
| 771 | | | |
| 772 | self.size -= rec_len; | | |
| 773 | const kind: PaxAttrKind = if (eql(keyword, "path")) | | |
| 774 | .path | 765 | .path |
| 775 | else if (eql(keyword, "linkpath")) | 766 | else if (eql(keyword, "linkpath")) |
| 776 | .linkpath | 767 | .linkpath |
| ... | @@ -778,10 +769,10 @@ fn PaxReader(comptime ReaderType: type) type { | ... | @@ -778,10 +769,10 @@ fn PaxReader(comptime ReaderType: type) type { |
| 778 | .size | 769 | .size |
| 779 | else { | 770 | else { |
| 780 | try self.reader.skipBytes(value_len, .{}); | 771 | try self.reader.skipBytes(value_len, .{}); |
| 781 | try checkRecordEnd(self.reader); | 772 | try validateAttributeEnding(self.reader); |
| 782 | continue; | 773 | continue; |
| 783 | }; | 774 | }; |
| 784 | return Attr{ | 775 | return Attribute{ |
| 785 | .kind = kind, | 776 | .kind = kind, |
| 786 | .len = value_len, | 777 | .len = value_len, |
| 787 | .reader = self.reader, | 778 | .reader = self.reader, |
| ... | @@ -791,24 +782,30 @@ fn PaxReader(comptime ReaderType: type) type { | ... | @@ -791,24 +782,30 @@ fn PaxReader(comptime ReaderType: type) type { |
| 791 | return null; | 782 | return null; |
| 792 | } | 783 | } |
| 793 | | 784 | |
| | 785 | inline fn readUntil(self: *Self, delimiter: u8) ![]const u8 { |
| | 786 | var fbs = std.io.fixedBufferStream(&self.scratch); |
| | 787 | try self.reader.streamUntilDelimiter(fbs.writer(), delimiter, null); |
| | 788 | return fbs.getWritten(); |
| | 789 | } |
| | 790 | |
| 794 | inline fn eql(a: []const u8, b: []const u8) bool { | 791 | inline fn eql(a: []const u8, b: []const u8) bool { |
| 795 | return std.mem.eql(u8, a, b); | 792 | return std.mem.eql(u8, a, b); |
| 796 | } | 793 | } |
| 797 | | 794 | |
| 798 | fn checkKeyword(keyword: []const u8) !void { | 795 | inline fn hasNull(str: []const u8) bool { |
| 799 | if (std.mem.indexOfScalar(u8, keyword, 0)) |_| return error.InvalidPaxAttribute; | 796 | return (std.mem.indexOfScalar(u8, str, 0)) != null; |
| 800 | } | 797 | } |
| 801 | | 798 | |
| 802 | // Checks that each record ends with new line. | 799 | // Checks that each record ends with new line. |
| 803 | fn checkRecordEnd(reader: ReaderType) !void { | 800 | inline fn validateAttributeEnding(reader: ReaderType) !void { |
| 804 | if (try reader.readByte() != '\n') return error.InvalidPaxAttribute; | 801 | if (try reader.readByte() != '\n') return error.PaxInvalidAttributeEnd; |
| 805 | } | 802 | } |
| 806 | }; | 803 | }; |
| 807 | } | 804 | } |
| 808 | | 805 | |
| 809 | test "tar PaxReader" { | 806 | test "tar PaxReader" { |
| 810 | const Attr = struct { | 807 | const Attr = struct { |
| 811 | kind: PaxAttrKind, | 808 | kind: PaxAttributeKind, |
| 812 | value: []const u8 = undefined, | 809 | value: []const u8 = undefined, |
| 813 | err: ?anyerror = null, | 810 | err: ?anyerror = null, |
| 814 | }; | 811 | }; |
| ... | @@ -853,8 +850,21 @@ test "tar PaxReader" { | ... | @@ -853,8 +850,21 @@ test "tar PaxReader" { |
| 853 | .attrs = &[_]Attr{ | 850 | .attrs = &[_]Attr{ |
| 854 | .{ .kind = .path, .value = "name" }, | 851 | .{ .kind = .path, .value = "name" }, |
| 855 | }, | 852 | }, |
| 856 | .err = error.InvalidPaxAttribute, | 853 | .err = error.UnexpectedEndOfStream, |
| | 854 | }, |
| | 855 | .{ // too long size of the second key-value pair |
| | 856 | .data = |
| | 857 | \\13 path=name |
| | 858 | \\6 k=1 |
| | 859 | \\19 linkpath=value |
| | 860 | \\ |
| | 861 | , |
| | 862 | .attrs = &[_]Attr{ |
| | 863 | .{ .kind = .path, .value = "name" }, |
| | 864 | }, |
| | 865 | .err = error.UnexpectedEndOfStream, |
| 857 | }, | 866 | }, |
| | 867 | |
| 858 | .{ // too long size of the second key-value pair | 868 | .{ // too long size of the second key-value pair |
| 859 | .data = | 869 | .data = |
| 860 | \\13 path=name | 870 | \\13 path=name |
| ... | @@ -864,7 +874,7 @@ test "tar PaxReader" { | ... | @@ -864,7 +874,7 @@ test "tar PaxReader" { |
| 864 | , | 874 | , |
| 865 | .attrs = &[_]Attr{ | 875 | .attrs = &[_]Attr{ |
| 866 | .{ .kind = .path, .value = "name" }, | 876 | .{ .kind = .path, .value = "name" }, |
| 867 | .{ .kind = .linkpath, .err = error.InvalidPaxAttribute }, | 877 | .{ .kind = .linkpath, .err = error.PaxInvalidAttributeEnd }, |
| 868 | }, | 878 | }, |
| 869 | }, | 879 | }, |
| 870 | .{ // null in keyword is not valid | 880 | .{ // null in keyword is not valid |
| ... | @@ -872,12 +882,12 @@ test "tar PaxReader" { | ... | @@ -872,12 +882,12 @@ test "tar PaxReader" { |
| 872 | .attrs = &[_]Attr{ | 882 | .attrs = &[_]Attr{ |
| 873 | .{ .kind = .path, .value = "name" }, | 883 | .{ .kind = .path, .value = "name" }, |
| 874 | }, | 884 | }, |
| 875 | .err = error.InvalidPaxAttribute, | 885 | .err = error.PaxNullInKeyword, |
| 876 | }, | 886 | }, |
| 877 | .{ // null in value is not valid | 887 | .{ // null in value is not valid |
| 878 | .data = "23 path=name\x00with null\n", | 888 | .data = "23 path=name\x00with null\n", |
| 879 | .attrs = &[_]Attr{ | 889 | .attrs = &[_]Attr{ |
| 880 | .{ .kind = .path, .err = error.InvalidPaxAttribute }, | 890 | .{ .kind = .path, .err = error.PaxNullInValue }, |
| 881 | }, | 891 | }, |
| 882 | }, | 892 | }, |
| 883 | .{ // 1000 characters path | 893 | .{ // 1000 characters path |
| ... | @@ -1019,6 +1029,16 @@ fn TarReader(comptime ReaderType: type) type { | ... | @@ -1019,6 +1029,16 @@ fn TarReader(comptime ReaderType: type) type { |
| 1019 | return nullStr(buf); | 1029 | return nullStr(buf); |
| 1020 | } | 1030 | } |
| 1021 | | 1031 | |
| | 1032 | fn reset(self: *Self) void { |
| | 1033 | self.file = File{ |
| | 1034 | .name = self.file_name_buffer[0..0], |
| | 1035 | .link_name = self.link_name_buffer[0..0], |
| | 1036 | .size = 0, |
| | 1037 | .file_type = 0xff, |
| | 1038 | .mode = 0, |
| | 1039 | }; |
| | 1040 | } |
| | 1041 | |
| 1022 | // Externally, `next` iterates through the tar archive as if it is a | 1042 | // Externally, `next` iterates through the tar archive as if it is a |
| 1023 | // series of files. Internally, the tar format often uses fake "files" | 1043 | // series of files. Internally, the tar format often uses fake "files" |
| 1024 | // to add meta data that describes the next file. These meta data | 1044 | // to add meta data that describes the next file. These meta data |