From ea6e0e33a7630fb78550a5567a98420c3377350c Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 1 Feb 2023 18:42:29 -0700 Subject: [PATCH] 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 --- lib/std/fs/file.zig | 3 ++- lib/std/tar.zig | 23 +++++++++++++++++++++++ src/Package.zig | 24 +++++++++++++++++++++++- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/lib/std/fs/file.zig b/lib/std/fs/file.zig index a6ecc37d92..1ba4bc18fd 100644 --- a/lib/std/fs/file.zig +++ b/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, diff --git a/lib/std/tar.zig b/lib/std/tar.zig index 4f6a77c6ba..91772d7319 100644 --- a/lib/std/tar.zig +++ b/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; diff --git a/src/Package.zig b/src/Package.zig index ebe84b8444..0f036b9ef5 100644 --- a/src/Package.zig +++ b/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 {