Commit b88ae8dbd8

Igor Anić <igor.anic@gmail.com>
2024-04-01 17:17:36
std.tar: implement executable bit only
1 parent 2a7cedf
Changed files (2)
lib
std
tar
testdata
lib/std/tar/testdata/example.tar
Binary file
lib/std/tar.zig
@@ -573,18 +573,6 @@ fn PaxIterator(comptime ReaderType: type) type {
 
 /// Saves tar file content to the file systems.
 pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: PipeOptions) !void {
-    switch (options.mode_mode) {
-        .ignore => {},
-        .executable_bit_only => {
-            // This code does not look at the mode bits yet. To implement this feature,
-            // the implementation must be adjusted to look at the mode, and check the
-            // user executable bit, then call fchmod on newly created files when
-            // the executable bit is supposed to be set.
-            // It also needs to properly deal with ACLs on Windows.
-            @panic("TODO: unimplemented: tar ModeMode.executable_bit_only");
-        },
-    }
-
     var file_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
     var link_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
     var iter = iterator(reader, .{
@@ -605,7 +593,7 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: PipeOptions)
                 const file_name = stripComponents(file.name, options.strip_components);
                 if (file_name.len == 0) return error.BadFileName;
 
-                if (createDirAndFile(dir, file_name)) |fs_file| {
+                if (createDirAndFile(dir, file_name, fileMode(file.mode, options))) |fs_file| {
                     defer fs_file.close();
                     try file.writeAll(fs_file);
                 } else |err| {
@@ -636,12 +624,12 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: PipeOptions)
     }
 }
 
-fn createDirAndFile(dir: std.fs.Dir, file_name: []const u8) !std.fs.File {
-    const fs_file = dir.createFile(file_name, .{ .exclusive = true }) catch |err| {
+fn createDirAndFile(dir: std.fs.Dir, file_name: []const u8, mode: std.fs.File.Mode) !std.fs.File {
+    const fs_file = dir.createFile(file_name, .{ .exclusive = true, .mode = mode }) catch |err| {
         if (err == error.FileNotFound) {
             if (std.fs.path.dirname(file_name)) |dir_name| {
                 try dir.makePath(dir_name);
-                return try dir.createFile(file_name, .{ .exclusive = true });
+                return try dir.createFile(file_name, .{ .exclusive = true, .mode = mode });
             }
         }
         return err;
@@ -877,9 +865,9 @@ test "create file and symlink" {
     var root = testing.tmpDir(.{});
     defer root.cleanup();
 
-    var file = try createDirAndFile(root.dir, "file1");
+    var file = try createDirAndFile(root.dir, "file1", default_mode);
     file.close();
-    file = try createDirAndFile(root.dir, "a/b/c/file2");
+    file = try createDirAndFile(root.dir, "a/b/c/file2", default_mode);
     file.close();
 
     createDirAndSymlink(root.dir, "a/b/c/file2", "symlink1") catch |err| {
@@ -891,7 +879,7 @@ test "create file and symlink" {
 
     // Danglink symlnik, file created later
     try createDirAndSymlink(root.dir, "../../../g/h/i/file4", "j/k/l/symlink3");
-    file = try createDirAndFile(root.dir, "g/h/i/file4");
+    file = try createDirAndFile(root.dir, "g/h/i/file4", default_mode);
     file.close();
 }
 
@@ -1011,3 +999,69 @@ fn normalizePath(bytes: []u8) []u8 {
     std.mem.replaceScalar(u8, bytes, std.fs.path.sep, canonical_sep);
     return bytes;
 }
+
+const default_mode = std.fs.File.default_mode;
+
+// File system mode based on tar header mode and mode_mode options.
+fn fileMode(mode: u32, options: PipeOptions) std.fs.File.Mode {
+    if (!std.fs.has_executable_bit or options.mode_mode == .ignore)
+        return default_mode;
+
+    const S = std.posix.S;
+
+    // The mode from the tar file is inspected for the owner executable bit.
+    if (mode & S.IXUSR == 0)
+        return default_mode;
+
+    // This bit is copied to the group and other executable bits.
+    // Other bits of the mode are left as the default when creating files.
+    return default_mode | S.IXUSR | S.IXGRP | S.IXOTH;
+}
+
+test fileMode {
+    if (!std.fs.has_executable_bit) return error.SkipZigTest;
+    try testing.expectEqual(default_mode, fileMode(0o744, PipeOptions{ .mode_mode = .ignore }));
+    try testing.expectEqual(0o777, fileMode(0o744, PipeOptions{}));
+    try testing.expectEqual(0o666, fileMode(0o644, PipeOptions{}));
+    try testing.expectEqual(0o666, fileMode(0o655, PipeOptions{}));
+}
+
+test "executable bit" {
+    if (!std.fs.has_executable_bit) return error.SkipZigTest;
+
+    const S = std.posix.S;
+    const data = @embedFile("tar/testdata/example.tar");
+
+    for ([_]PipeOptions.ModeMode{ .ignore, .executable_bit_only }) |opt| {
+        var fbs = std.io.fixedBufferStream(data);
+        const reader = fbs.reader();
+
+        var tmp = testing.tmpDir(.{ .no_follow = true });
+        //defer tmp.cleanup();
+
+        pipeToFileSystem(tmp.dir, reader, .{
+            .strip_components = 1,
+            .exclude_empty_directories = true,
+            .mode_mode = opt,
+        }) catch |err| {
+            // Skip on platform which don't support symlinks
+            if (err == error.UnableToCreateSymLink) return error.SkipZigTest;
+            return err;
+        };
+
+        const fs = try tmp.dir.statFile("a/file");
+        try testing.expect(fs.kind == .file);
+
+        if (opt == .executable_bit_only) {
+            // Executable bit is set for user, group and others
+            try testing.expect(fs.mode & S.IXUSR > 0);
+            try testing.expect(fs.mode & S.IXGRP > 0);
+            try testing.expect(fs.mode & S.IXOTH > 0);
+        }
+        if (opt == .ignore) {
+            try testing.expect(fs.mode & S.IXUSR == 0);
+            try testing.expect(fs.mode & S.IXGRP == 0);
+            try testing.expect(fs.mode & S.IXOTH == 0);
+        }
+    }
+}