Commit ea6e0e33a7

Andrew Kelley <andrew@ziglang.org>
2023-02-02 02:42:29
zig build: add executable bit and file path to package hash
Unfortunately, due to the Windows equivalent of executable permissions being a bit tricky, there is follow-up work to be done. What is done in this commit is the hash modifications. At the fetch layer, executable bits inside packages are ignored. In the hash computation layer, executable bit is implemented for POSIX but not yet for Windows. This means that the hash will not break again in the future for packages that do not have any executable files, but it will break for packages that do. This is a hash-breaking change. Closes #14308
1 parent 1fba884
Changed files (3)
lib/std/fs/file.zig
@@ -179,7 +179,7 @@ pub const File = struct {
         lock_nonblocking: bool = false,
 
         /// For POSIX systems this is the file system mode the file will
-        /// be created with.
+        /// be created with. On other systems this is always 0.
         mode: Mode = default_mode,
 
         /// Setting this to `.blocking` prevents `O.NONBLOCK` from being passed even
@@ -307,6 +307,7 @@ pub const File = struct {
         /// is unique to each filesystem.
         inode: INode,
         size: u64,
+        /// This is available on POSIX systems and is always 0 otherwise.
         mode: Mode,
         kind: Kind,
 
lib/std/tar.zig
@@ -1,6 +1,18 @@
 pub const Options = struct {
     /// Number of directory levels to skip when extracting files.
     strip_components: u32 = 0,
+    /// How to handle the "mode" property of files from within the tar file.
+    mode_mode: ModeMode = .executable_bit_only,
+
+    const ModeMode = enum {
+        /// The mode from the tar file is completely ignored. Files are created
+        /// with the default mode when creating files.
+        ignore,
+        /// The mode from the tar file is inspected for the owner executable bit
+        /// only. This bit is copied to the group and other executable bits.
+        /// Other bits of the mode are left as the default when creating files.
+        executable_bit_only,
+    };
 };
 
 pub const Header = struct {
@@ -72,6 +84,17 @@ pub const Header = struct {
 };
 
 pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !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: [255]u8 = undefined;
     var buffer: [512 * 8]u8 = undefined;
     var start: usize = 0;
src/Package.zig
@@ -1,5 +1,6 @@
 const Package = @This();
 
+const builtin = @import("builtin");
 const std = @import("std");
 const fs = std.fs;
 const mem = std.mem;
@@ -440,6 +441,12 @@ fn unpackTarball(
 
     try std.tar.pipeToFileSystem(out_dir, decompress.reader(), .{
         .strip_components = 1,
+        // TODO: we would like to set this to executable_bit_only, but two
+        // things need to happen before that:
+        // 1. the tar implementation needs to support it
+        // 2. the hashing algorithm here needs to support detecting the is_executable
+        //    bit on Windows from the ACLs (see the isExecutable function).
+        .mode_mode = .ignore,
     });
 }
 
@@ -468,7 +475,7 @@ const HashedFile = struct {
     hash: [Hash.digest_length]u8,
     failure: Error!void,
 
-    const Error = fs.File.OpenError || fs.File.ReadError;
+    const Error = fs.File.OpenError || fs.File.ReadError || fs.File.StatError;
 
     fn lessThan(context: void, lhs: *const HashedFile, rhs: *const HashedFile) bool {
         _ = context;
@@ -544,6 +551,8 @@ fn hashFileFallible(dir: fs.Dir, hashed_file: *HashedFile) HashedFile.Error!void
     var buf: [8000]u8 = undefined;
     var file = try dir.openFile(hashed_file.path, .{});
     var hasher = Hash.init(.{});
+    hasher.update(hashed_file.path);
+    hasher.update(&.{ 0, @boolToInt(try isExecutable(file)) });
     while (true) {
         const bytes_read = try file.read(&buf);
         if (bytes_read == 0) break;
@@ -552,6 +561,19 @@ fn hashFileFallible(dir: fs.Dir, hashed_file: *HashedFile) HashedFile.Error!void
     hasher.final(&hashed_file.hash);
 }
 
+fn isExecutable(file: fs.File) !bool {
+    if (builtin.os.tag == .windows) {
+        // TODO check the ACL on Windows.
+        // Until this is implemented, this could be a false negative on
+        // Windows, which is why we do not yet set executable_bit_only above
+        // when unpacking the tarball.
+        return false;
+    } else {
+        const stat = try file.stat();
+        return (stat.mode & std.os.S.IXUSR) != 0;
+    }
+}
+
 const hex_charset = "0123456789abcdef";
 
 fn hex64(x: u64) [16]u8 {