| ... | @@ -120,8 +120,10 @@ pub const Diagnostics = struct { | ... | @@ -120,8 +120,10 @@ pub const Diagnostics = struct { |
| 120 | } | 120 | } |
| 121 | }; | 121 | }; |
| 122 | | 122 | |
| 123 | /// pipeToFileSystem options | 123 | /// Deprecated, renamed to `ExtractOptions`. |
| 124 | pub const PipeOptions = struct { | 124 | pub const PipeOptions = ExtractOptions; |
| | 125 | |
| | 126 | pub const ExtractOptions = struct { |
| 125 | /// Number of directory levels to skip when extracting files. | 127 | /// Number of directory levels to skip when extracting files. |
| 126 | strip_components: u32 = 0, | 128 | strip_components: u32 = 0, |
| 127 | /// How to handle the "mode" property of files from within the tar file. | 129 | /// How to handle the "mode" property of files from within the tar file. |
| ... | @@ -580,10 +582,15 @@ pub const PaxIterator = struct { | ... | @@ -580,10 +582,15 @@ pub const PaxIterator = struct { |
| 580 | } | 582 | } |
| 581 | }; | 583 | }; |
| 582 | | 584 | |
| 583 | /// Saves tar file content to the file systems. | 585 | /// Deprecated, renamed to `extract`. |
| 584 | pub fn pipeToFileSystem(io: Io, dir: Io.Dir, reader: *Io.Reader, options: PipeOptions) !void { | 586 | pub const pipeToFileSystem = extract; |
| 585 | var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined; | 587 | |
| 586 | var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined; | 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. |
| | 590 | pub 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; |
| 587 | var file_contents_buffer: [1024]u8 = undefined; | 594 | var file_contents_buffer: [1024]u8 = undefined; |
| 588 | var it: Iterator = .init(reader, .{ | 595 | var it: Iterator = .init(reader, .{ |
| 589 | .file_name_buffer = &file_name_buffer, | 596 | .file_name_buffer = &file_name_buffer, |
| ... | @@ -592,14 +599,15 @@ pub fn pipeToFileSystem(io: Io, dir: Io.Dir, reader: *Io.Reader, options: PipeOp | ... | @@ -592,14 +599,15 @@ pub fn pipeToFileSystem(io: Io, dir: Io.Dir, reader: *Io.Reader, options: PipeOp |
| 592 | }); | 599 | }); |
| 593 | | 600 | |
| 594 | while (try it.next()) |file| { | 601 | while (try it.next()) |file| { |
| 595 | const file_name = stripComponents(file.name, options.strip_components); | 602 | const n = sanitizePath(&sanitize_buffer, file.name, options.strip_components) catch 0; |
| 596 | if (file_name.len == 0 and file.kind != .directory) { | 603 | if (n == 0 and file.kind != .directory) { |
| 597 | const d = options.diagnostics orelse return error.TarComponentsOutsideStrippedPrefix; | 604 | const d = options.diagnostics orelse return error.TarComponentsOutsideStrippedPrefix; |
| 598 | try d.errors.append(d.allocator, .{ .components_outside_stripped_prefix = .{ | 605 | try d.errors.append(d.allocator, .{ .components_outside_stripped_prefix = .{ |
| 599 | .file_name = try d.allocator.dupe(u8, file.name), | 606 | .file_name = try d.allocator.dupe(u8, file.name), |
| 600 | } }); | 607 | } }); |
| 601 | continue; | 608 | continue; |
| 602 | } | 609 | } |
| | 610 | const file_name = sanitize_buffer[0..n]; |
| 603 | if (options.diagnostics) |d| { | 611 | if (options.diagnostics) |d| { |
| 604 | try d.findRoot(file.kind, file_name); | 612 | try d.findRoot(file.kind, file_name); |
| 605 | } | 613 | } |
| ... | @@ -665,27 +673,59 @@ fn createDirAndSymlink(io: Io, dir: Io.Dir, link_name: []const u8, file_name: [] | ... | @@ -665,27 +673,59 @@ fn createDirAndSymlink(io: Io, dir: Io.Dir, link_name: []const u8, file_name: [] |
| 665 | }; | 673 | }; |
| 666 | } | 674 | } |
| 667 | | 675 | |
| 668 | fn stripComponents(path: []const u8, count: u32) []const u8 { | 676 | fn sanitizePath(buffer: []u8, path: []const u8, strip_components: u32) error{Invalid}!usize { |
| | 677 | if (path.len == 0 or path[0] == '/') return error.Invalid; |
| 669 | var i: usize = 0; | 678 | var i: usize = 0; |
| 670 | var c = count; | 679 | var c = strip_components; |
| 671 | while (c > 0) : (c -= 1) { | 680 | var it = std.mem.tokenizeScalar(u8, path, '/'); |
| 672 | if (std.mem.findScalarPos(u8, path, i, '/')) |pos| { | 681 | while (it.next()) |component| { |
| 673 | i = pos + 1; | 682 | if (std.mem.eql(u8, component, ".")) continue; |
| 674 | } else { | 683 | if (std.mem.eql(u8, component, "..")) { |
| 675 | i = path.len; | 684 | if (i == 0) return error.Invalid; |
| 676 | break; | 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; |
| 677 | } | 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; |
| 678 | } | 702 | } |
| 679 | return path[i..]; | 703 | if (c > 0) return error.Invalid; |
| | 704 | return i; |
| | 705 | } |
| | 706 | |
| | 707 | fn 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 | |
| | 713 | fn 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)); |
| 680 | } | 716 | } |
| 681 | | 717 | |
| 682 | test stripComponents { | 718 | test sanitizePath { |
| 683 | const expectEqualStrings = testing.expectEqualStrings; | 719 | try testSanitizePath("a/b/c", "a/b/c", 0); |
| 684 | try expectEqualStrings("a/b/c", stripComponents("a/b/c", 0)); | 720 | try testSanitizePath("a/b/c", "a/x/y/../../b/c", 0); |
| 685 | try expectEqualStrings("b/c", stripComponents("a/b/c", 1)); | 721 | try testSanitizePath("b/c", "a/b/c", 1); |
| 686 | try expectEqualStrings("c", stripComponents("a/b/c", 2)); | 722 | try testSanitizePath("c", "a/b/c", 2); |
| 687 | try expectEqualStrings("", stripComponents("a/b/c", 3)); | 723 | try testSanitizePath("", "a/b/c", 3); |
| 688 | try expectEqualStrings("", stripComponents("a/b/c", 4)); | 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); |
| 689 | } | 729 | } |
| 690 | | 730 | |
| 691 | test PaxIterator { | 731 | test PaxIterator { |
| ... | @@ -958,7 +998,7 @@ test Iterator { | ... | @@ -958,7 +998,7 @@ test Iterator { |
| 958 | } | 998 | } |
| 959 | } | 999 | } |
| 960 | | 1000 | |
| 961 | test pipeToFileSystem { | 1001 | test extract { |
| 962 | const io = testing.io; | 1002 | const io = testing.io; |
| 963 | // Example tar file is created from this tree structure: | 1003 | // Example tar file is created from this tree structure: |
| 964 | // $ tree example | 1004 | // $ tree example |
| ... | @@ -987,7 +1027,7 @@ test pipeToFileSystem { | ... | @@ -987,7 +1027,7 @@ test pipeToFileSystem { |
| 987 | const dir = tmp.dir; | 1027 | const dir = tmp.dir; |
| 988 | | 1028 | |
| 989 | // Save tar from reader to the file system `dir` | 1029 | // Save tar from reader to the file system `dir` |
| 990 | pipeToFileSystem(io, dir, &reader, .{ | 1030 | extract(io, dir, &reader, .{ |
| 991 | .mode_mode = .ignore, | 1031 | .mode_mode = .ignore, |
| 992 | .strip_components = 1, | 1032 | .strip_components = 1, |
| 993 | .exclude_empty_directories = true, | 1033 | .exclude_empty_directories = true, |
| ... | @@ -1009,7 +1049,7 @@ test pipeToFileSystem { | ... | @@ -1009,7 +1049,7 @@ test pipeToFileSystem { |
| 1009 | ); | 1049 | ); |
| 1010 | } | 1050 | } |
| 1011 | | 1051 | |
| 1012 | test "pipeToFileSystem root_dir" { | 1052 | test "extract root_dir" { |
| 1013 | const io = testing.io; | 1053 | const io = testing.io; |
| 1014 | const data = @embedFile("tar/testdata/example.tar"); | 1054 | const data = @embedFile("tar/testdata/example.tar"); |
| 1015 | var reader: Io.Reader = .fixed(data); | 1055 | var reader: Io.Reader = .fixed(data); |
| ... | @@ -1021,7 +1061,7 @@ test "pipeToFileSystem root_dir" { | ... | @@ -1021,7 +1061,7 @@ test "pipeToFileSystem root_dir" { |
| 1021 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; | 1061 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; |
| 1022 | defer diagnostics.deinit(); | 1062 | defer diagnostics.deinit(); |
| 1023 | | 1063 | |
| 1024 | pipeToFileSystem(io, tmp.dir, &reader, .{ | 1064 | extract(io, tmp.dir, &reader, .{ |
| 1025 | .strip_components = 1, | 1065 | .strip_components = 1, |
| 1026 | .diagnostics = &diagnostics, | 1066 | .diagnostics = &diagnostics, |
| 1027 | }) catch |err| { | 1067 | }) catch |err| { |
| ... | @@ -1043,7 +1083,7 @@ test "pipeToFileSystem root_dir" { | ... | @@ -1043,7 +1083,7 @@ test "pipeToFileSystem root_dir" { |
| 1043 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; | 1083 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; |
| 1044 | defer diagnostics.deinit(); | 1084 | defer diagnostics.deinit(); |
| 1045 | | 1085 | |
| 1046 | pipeToFileSystem(io, tmp.dir, &reader, .{ | 1086 | extract(io, tmp.dir, &reader, .{ |
| 1047 | .strip_components = 0, | 1087 | .strip_components = 0, |
| 1048 | .diagnostics = &diagnostics, | 1088 | .diagnostics = &diagnostics, |
| 1049 | }) catch |err| { | 1089 | }) catch |err| { |
| ... | @@ -1068,7 +1108,7 @@ test "findRoot with single file archive" { | ... | @@ -1068,7 +1108,7 @@ test "findRoot with single file archive" { |
| 1068 | | 1108 | |
| 1069 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; | 1109 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; |
| 1070 | defer diagnostics.deinit(); | 1110 | defer diagnostics.deinit(); |
| 1071 | try pipeToFileSystem(io, tmp.dir, &reader, .{ .diagnostics = &diagnostics }); | 1111 | try extract(io, tmp.dir, &reader, .{ .diagnostics = &diagnostics }); |
| 1072 | | 1112 | |
| 1073 | try testing.expectEqualStrings("", diagnostics.root_dir); | 1113 | try testing.expectEqualStrings("", diagnostics.root_dir); |
| 1074 | } | 1114 | } |
| ... | @@ -1083,12 +1123,12 @@ test "findRoot without explicit root dir" { | ... | @@ -1083,12 +1123,12 @@ test "findRoot without explicit root dir" { |
| 1083 | | 1123 | |
| 1084 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; | 1124 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; |
| 1085 | defer diagnostics.deinit(); | 1125 | defer diagnostics.deinit(); |
| 1086 | try pipeToFileSystem(io, tmp.dir, &reader, .{ .diagnostics = &diagnostics }); | 1126 | try extract(io, tmp.dir, &reader, .{ .diagnostics = &diagnostics }); |
| 1087 | | 1127 | |
| 1088 | try testing.expectEqualStrings("root", diagnostics.root_dir); | 1128 | try testing.expectEqualStrings("root", diagnostics.root_dir); |
| 1089 | } | 1129 | } |
| 1090 | | 1130 | |
| 1091 | test "pipeToFileSystem strip_components" { | 1131 | test "extract strip_components" { |
| 1092 | const io = testing.io; | 1132 | const io = testing.io; |
| 1093 | const data = @embedFile("tar/testdata/example.tar"); | 1133 | const data = @embedFile("tar/testdata/example.tar"); |
| 1094 | var reader: Io.Reader = .fixed(data); | 1134 | var reader: Io.Reader = .fixed(data); |
| ... | @@ -1098,7 +1138,7 @@ test "pipeToFileSystem strip_components" { | ... | @@ -1098,7 +1138,7 @@ test "pipeToFileSystem strip_components" { |
| 1098 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; | 1138 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; |
| 1099 | defer diagnostics.deinit(); | 1139 | defer diagnostics.deinit(); |
| 1100 | | 1140 | |
| 1101 | pipeToFileSystem(io, tmp.dir, &reader, .{ | 1141 | extract(io, tmp.dir, &reader, .{ |
| 1102 | .strip_components = 3, | 1142 | .strip_components = 3, |
| 1103 | .diagnostics = &diagnostics, | 1143 | .diagnostics = &diagnostics, |
| 1104 | }) catch |err| { | 1144 | }) catch |err| { |
| ... | @@ -1120,7 +1160,7 @@ fn normalizePath(bytes: []u8) []u8 { | ... | @@ -1120,7 +1160,7 @@ fn normalizePath(bytes: []u8) []u8 { |
| 1120 | } | 1160 | } |
| 1121 | | 1161 | |
| 1122 | // File system mode based on tar header mode and mode_mode options. | 1162 | // File system mode based on tar header mode and mode_mode options. |
| 1123 | fn filePermissions(mode: u32, options: PipeOptions) Io.File.Permissions { | 1163 | fn filePermissions(mode: u32, options: ExtractOptions) Io.File.Permissions { |
| 1124 | return if (!Io.File.Permissions.has_executable_bit or options.mode_mode == .ignore or (mode & 0o100) == 0) | 1164 | return if (!Io.File.Permissions.has_executable_bit or options.mode_mode == .ignore or (mode & 0o100) == 0) |
| 1125 | .default_file | 1165 | .default_file |
| 1126 | else | 1166 | else |
| ... | @@ -1142,13 +1182,13 @@ test "executable bit" { | ... | @@ -1142,13 +1182,13 @@ test "executable bit" { |
| 1142 | const S = std.posix.S; | 1182 | const S = std.posix.S; |
| 1143 | const data = @embedFile("tar/testdata/example.tar"); | 1183 | const data = @embedFile("tar/testdata/example.tar"); |
| 1144 | | 1184 | |
| 1145 | for ([_]PipeOptions.ModeMode{ .ignore, .executable_bit_only }) |opt| { | 1185 | for ([_]ExtractOptions.ModeMode{ .ignore, .executable_bit_only }) |opt| { |
| 1146 | var reader: Io.Reader = .fixed(data); | 1186 | var reader: Io.Reader = .fixed(data); |
| 1147 | | 1187 | |
| 1148 | var tmp = testing.tmpDir(.{ .follow_symlinks = false }); | 1188 | var tmp = testing.tmpDir(.{ .follow_symlinks = false }); |
| 1149 | //defer tmp.cleanup(); | 1189 | //defer tmp.cleanup(); |
| 1150 | | 1190 | |
| 1151 | pipeToFileSystem(io, tmp.dir, &reader, .{ | 1191 | extract(io, tmp.dir, &reader, .{ |
| 1152 | .strip_components = 1, | 1192 | .strip_components = 1, |
| 1153 | .exclude_empty_directories = true, | 1193 | .exclude_empty_directories = true, |
| 1154 | .mode_mode = opt, | 1194 | .mode_mode = opt, |