authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-02-01 18:42:29-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-02-01 18:42:29-07:00
logea6e0e33a7630fb78550a5567a98420c3377350c
treebee5ca7e7c42188b4764493d90b4695116622df8
parent1fba88450db12f184d76f0651f7ca933322c1fc0

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

3 files changed, 48 insertions(+), 2 deletions(-)

lib/std/fs/file.zig+2-1
...@@ -179,7 +179,7 @@ pub const File = struct {...@@ -179,7 +179,7 @@ pub const File = struct {
179 lock_nonblocking: bool = false,179 lock_nonblocking: bool = false,
180180
181 /// For POSIX systems this is the file system mode the file will181 /// For POSIX systems this is the file system mode the file will
182 /// be created with.182 /// be created with. On other systems this is always 0.
183 mode: Mode = default_mode,183 mode: Mode = default_mode,
184184
185 /// Setting this to `.blocking` prevents `O.NONBLOCK` from being passed even185 /// Setting this to `.blocking` prevents `O.NONBLOCK` from being passed even
...@@ -307,6 +307,7 @@ pub const File = struct {...@@ -307,6 +307,7 @@ pub const File = struct {
307 /// is unique to each filesystem.307 /// is unique to each filesystem.
308 inode: INode,308 inode: INode,
309 size: u64,309 size: u64,
310 /// This is available on POSIX systems and is always 0 otherwise.
310 mode: Mode,311 mode: Mode,
311 kind: Kind,312 kind: Kind,
312313
lib/std/tar.zig+23
...@@ -1,6 +1,18 @@...@@ -1,6 +1,18 @@
1pub const Options = struct {1pub const Options = struct {
2 /// Number of directory levels to skip when extracting files.2 /// Number of directory levels to skip when extracting files.
3 strip_components: u32 = 0,3 strip_components: u32 = 0,
4 /// How to handle the "mode" property of files from within the tar file.
5 mode_mode: ModeMode = .executable_bit_only,
6
7 const ModeMode = enum {
8 /// The mode from the tar file is completely ignored. Files are created
9 /// with the default mode when creating files.
10 ignore,
11 /// The mode from the tar file is inspected for the owner executable bit
12 /// only. This bit is copied to the group and other executable bits.
13 /// Other bits of the mode are left as the default when creating files.
14 executable_bit_only,
15 };
4};16};
517
6pub const Header = struct {18pub const Header = struct {
...@@ -72,6 +84,17 @@ pub const Header = struct {...@@ -72,6 +84,17 @@ pub const Header = struct {
72};84};
7385
74pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !void {86pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !void {
87 switch (options.mode_mode) {
88 .ignore => {},
89 .executable_bit_only => {
90 // This code does not look at the mode bits yet. To implement this feature,
91 // the implementation must be adjusted to look at the mode, and check the
92 // user executable bit, then call fchmod on newly created files when
93 // the executable bit is supposed to be set.
94 // It also needs to properly deal with ACLs on Windows.
95 @panic("TODO: unimplemented: tar ModeMode.executable_bit_only");
96 },
97 }
75 var file_name_buffer: [255]u8 = undefined;98 var file_name_buffer: [255]u8 = undefined;
76 var buffer: [512 * 8]u8 = undefined;99 var buffer: [512 * 8]u8 = undefined;
77 var start: usize = 0;100 var start: usize = 0;
src/Package.zig+23-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1const Package = @This();1const Package = @This();
22
3const builtin = @import("builtin");
3const std = @import("std");4const std = @import("std");
4const fs = std.fs;5const fs = std.fs;
5const mem = std.mem;6const mem = std.mem;
...@@ -440,6 +441,12 @@ fn unpackTarball(...@@ -440,6 +441,12 @@ fn unpackTarball(
440441
441 try std.tar.pipeToFileSystem(out_dir, decompress.reader(), .{442 try std.tar.pipeToFileSystem(out_dir, decompress.reader(), .{
442 .strip_components = 1,443 .strip_components = 1,
444 // TODO: we would like to set this to executable_bit_only, but two
445 // things need to happen before that:
446 // 1. the tar implementation needs to support it
447 // 2. the hashing algorithm here needs to support detecting the is_executable
448 // bit on Windows from the ACLs (see the isExecutable function).
449 .mode_mode = .ignore,
443 });450 });
444}451}
445452
...@@ -468,7 +475,7 @@ const HashedFile = struct {...@@ -468,7 +475,7 @@ const HashedFile = struct {
468 hash: [Hash.digest_length]u8,475 hash: [Hash.digest_length]u8,
469 failure: Error!void,476 failure: Error!void,
470477
471 const Error = fs.File.OpenError || fs.File.ReadError;478 const Error = fs.File.OpenError || fs.File.ReadError || fs.File.StatError;
472479
473 fn lessThan(context: void, lhs: *const HashedFile, rhs: *const HashedFile) bool {480 fn lessThan(context: void, lhs: *const HashedFile, rhs: *const HashedFile) bool {
474 _ = context;481 _ = context;
...@@ -544,6 +551,8 @@ fn hashFileFallible(dir: fs.Dir, hashed_file: *HashedFile) HashedFile.Error!void...@@ -544,6 +551,8 @@ fn hashFileFallible(dir: fs.Dir, hashed_file: *HashedFile) HashedFile.Error!void
544 var buf: [8000]u8 = undefined;551 var buf: [8000]u8 = undefined;
545 var file = try dir.openFile(hashed_file.path, .{});552 var file = try dir.openFile(hashed_file.path, .{});
546 var hasher = Hash.init(.{});553 var hasher = Hash.init(.{});
554 hasher.update(hashed_file.path);
555 hasher.update(&.{ 0, @boolToInt(try isExecutable(file)) });
547 while (true) {556 while (true) {
548 const bytes_read = try file.read(&buf);557 const bytes_read = try file.read(&buf);
549 if (bytes_read == 0) break;558 if (bytes_read == 0) break;
...@@ -552,6 +561,19 @@ fn hashFileFallible(dir: fs.Dir, hashed_file: *HashedFile) HashedFile.Error!void...@@ -552,6 +561,19 @@ fn hashFileFallible(dir: fs.Dir, hashed_file: *HashedFile) HashedFile.Error!void
552 hasher.final(&hashed_file.hash);561 hasher.final(&hashed_file.hash);
553}562}
554563
564fn isExecutable(file: fs.File) !bool {
565 if (builtin.os.tag == .windows) {
566 // TODO check the ACL on Windows.
567 // Until this is implemented, this could be a false negative on
568 // Windows, which is why we do not yet set executable_bit_only above
569 // when unpacking the tarball.
570 return false;
571 } else {
572 const stat = try file.stat();
573 return (stat.mode & std.os.S.IXUSR) != 0;
574 }
575}
576
555const hex_charset = "0123456789abcdef";577const hex_charset = "0123456789abcdef";
556578
557fn hex64(x: u64) [16]u8 {579fn hex64(x: u64) [16]u8 {