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