Commit 035c1b6522

Igor Anić <igor.anic@gmail.com>
2024-04-11 16:32:07
std.tar: add strip components error to diagnostics
This was the only kind of error which was raised in pipeToFileSystem and not added to Diagnostics. Shell tar silently ignores paths which are stripped out when used with `--strip-components` switch. This enables that same behavior, errors will be collected in diagnostics but caller is free to ignore that type of diagnostics errors. Enables use case where caller knows structure of the tar file and want to extract only some deeply nested folders ignoring upper files/folders. Fixes: #17620 by giving caller options: - not provide diagnostic and get errors - provide diagnostics and analyze errors - provide diagnostics and ignore errors
1 parent fe66a12
Changed files (2)
lib
src
Package
lib/std/tar.zig
@@ -46,6 +46,9 @@ pub const Diagnostics = struct {
             file_name: []const u8,
             file_type: Header.Kind,
         },
+        components_outside_stripped_prefix: struct {
+            file_name: []const u8,
+        },
     };
 
     fn findRoot(d: *Diagnostics, path: []const u8) !void {
@@ -97,6 +100,9 @@ pub const Diagnostics = struct {
                 .unsupported_file_type => |info| {
                     d.allocator.free(info.file_name);
                 },
+                .components_outside_stripped_prefix => |info| {
+                    d.allocator.free(info.file_name);
+                },
             }
         }
         d.errors.deinit(d.allocator);
@@ -623,18 +629,24 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: PipeOptions)
 
     while (try iter.next()) |file| {
         const file_name = stripComponents(file.name, options.strip_components);
+        if (file_name.len == 0 and file.kind != .directory) {
+            const d = options.diagnostics orelse return error.TarComponentsOutsideStrippedPrefix;
+            try d.errors.append(d.allocator, .{ .components_outside_stripped_prefix = .{
+                .file_name = try d.allocator.dupe(u8, file.name),
+            } });
+            continue;
+        }
         if (options.diagnostics) |d| {
             try d.findRoot(file_name);
         }
 
         switch (file.kind) {
             .directory => {
-                if (file_name.len != 0 and !options.exclude_empty_directories) {
+                if (file_name.len > 0 and !options.exclude_empty_directories) {
                     try dir.makePath(file_name);
                 }
             },
             .file => {
-                if (file_name.len == 0) return error.BadFileName;
                 if (createDirAndFile(dir, file_name, fileMode(file.mode, options))) |fs_file| {
                     defer fs_file.close();
                     try file.writeAll(fs_file);
@@ -647,7 +659,6 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: PipeOptions)
                 }
             },
             .sym_link => {
-                if (file_name.len == 0) return error.BadFileName;
                 const link_name = file.link_name;
                 createDirAndSymlink(dir, link_name, file_name) catch |err| {
                     const d = options.diagnostics orelse return error.UnableToCreateSymLink;
@@ -1096,6 +1107,30 @@ test "findRoot without explicit root dir" {
     try testing.expectEqualStrings("root", diagnostics.root_dir);
 }
 
+test "pipeToFileSystem strip_components" {
+    const data = @embedFile("tar/testdata/example.tar");
+    var fbs = std.io.fixedBufferStream(data);
+    const reader = fbs.reader();
+
+    var tmp = testing.tmpDir(.{ .no_follow = true });
+    defer tmp.cleanup();
+    var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
+    defer diagnostics.deinit();
+
+    pipeToFileSystem(tmp.dir, reader, .{
+        .strip_components = 3,
+        .diagnostics = &diagnostics,
+    }) catch |err| {
+        // Skip on platform which don't support symlinks
+        if (err == error.UnableToCreateSymLink) return error.SkipZigTest;
+        return err;
+    };
+
+    try testing.expectEqual(2, diagnostics.errors.items.len);
+    try testing.expectEqualStrings("example/b/symlink", diagnostics.errors.items[0].components_outside_stripped_prefix.file_name);
+    try testing.expectEqualStrings("example/a/file", diagnostics.errors.items[1].components_outside_stripped_prefix.file_name);
+}
+
 fn normalizePath(bytes: []u8) []u8 {
     const canonical_sep = std.fs.path.sep_posix;
     if (std.fs.path.sep == canonical_sep) return bytes;
src/Package/Fetch.zig
@@ -1189,6 +1189,7 @@ fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!UnpackRes
                 .unable_to_create_file => |i| res.unableToCreateFile(stripRoot(i.file_name, res.root_dir), i.code),
                 .unable_to_create_sym_link => |i| res.unableToCreateSymLink(stripRoot(i.file_name, res.root_dir), i.link_name, i.code),
                 .unsupported_file_type => |i| res.unsupportedFileType(stripRoot(i.file_name, res.root_dir), @intFromEnum(i.file_type)),
+                .components_outside_stripped_prefix => {}, // impossible with strip_components = 0
             }
         }
     }