authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-02-23 15:58:51-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-02-26 11:42:03-08:00
log12355cfb4cb14ba78423fb38838f4485bb563c9b
treebd3f0fbfc489ccbeb87d5913a26639a8e2033d63
parente0129b387ff962c0f89d62c8ab0409145a19f453

Package: new hash format

legacy format is also supported. closes #20178

4 files changed, 269 insertions(+), 145 deletions(-)

src/Package.zig+156
......@@ -1,8 +1,164 @@
1const std = @import("std");
2const assert = std.debug.assert;
3
14pub const Module = @import("Package/Module.zig");
25pub const Fetch = @import("Package/Fetch.zig");
36pub const build_zig_basename = "build.zig";
47pub const Manifest = @import("Package/Manifest.zig");
58
9pub const multihash_len = 1 + 1 + Hash.Algo.digest_length;
10pub const multihash_hex_digest_len = 2 * multihash_len;
11pub const MultiHashHexDigest = [multihash_hex_digest_len]u8;
12
13/// A user-readable, file system safe hash that identifies an exact package
14/// snapshot, including file contents.
15///
16/// This data structure can be used to store the legacy hash format too. Legacy
17/// hash format is scheduled to be removed after 0.14.0 is tagged.
18pub const Hash = struct {
19 /// Maximum size of a package hash. Unused bytes at the end are
20 /// filled with zeroes.
21 bytes: [max_len]u8,
22
23 pub const Algo = std.crypto.hash.sha2.Sha256;
24 pub const Digest = [Algo.digest_length]u8;
25
26 pub const max_len = 32 + 1 + 32 + 1 + 12;
27
28 pub fn fromSlice(s: []const u8) Hash {
29 assert(s.len <= max_len);
30 var result: Hash = undefined;
31 @memcpy(result.bytes[0..s.len], s);
32 @memset(result.bytes[s.len..], 0);
33 return result;
34 }
35
36 pub fn toSlice(ph: *const Hash) []const u8 {
37 var end: usize = ph.bytes.len;
38 while (true) {
39 end -= 1;
40 if (ph.bytes[end] != 0) return ph.bytes[0 .. end + 1];
41 }
42 }
43
44 pub fn eql(a: *const Hash, b: *const Hash) bool {
45 return std.mem.eql(u8, &a.bytes, &b.bytes);
46 }
47
48 /// Distinguishes whether the legacy multihash format is being stored here.
49 pub fn isOld(h: *const Hash) bool {
50 if (h.bytes.len < 2) return false;
51 const their_multihash_func = std.fmt.parseInt(u8, h.bytes[0..2], 16) catch return false;
52 if (@as(MultihashFunction, @enumFromInt(their_multihash_func)) != multihash_function) return false;
53 if (h.toSlice().len != multihash_hex_digest_len) return false;
54 return std.mem.indexOfScalar(u8, &h.bytes, '-') == null;
55 }
56
57 test isOld {
58 const h: Hash = .fromSlice("1220138f4aba0c01e66b68ed9e1e1e74614c06e4743d88bc58af4f1c3dd0aae5fea7");
59 try std.testing.expect(h.isOld());
60 }
61
62 /// Produces "$name-$semver-$sizedhash".
63 /// * name is the name field from build.zig.zon, truncated at 32 bytes and must
64 /// be a valid zig identifier
65 /// * semver is the version field from build.zig.zon, truncated at 32 bytes
66 /// * sizedhash is the following 9-byte array, base64 encoded using -_ to make
67 /// it filesystem safe:
68 /// - (4 bytes) LE u32 total decompressed size in bytes
69 /// - (5 bytes) truncated SHA-256 of hashed files of the package
70 ///
71 /// example: "nasm-2.16.1-2-BWdcABvF_jM1"
72 pub fn init(digest: Digest, name: []const u8, ver: []const u8, size: u32) Hash {
73 var result: Hash = undefined;
74 var buf: std.ArrayListUnmanaged(u8) = .initBuffer(&result.bytes);
75 buf.appendSliceAssumeCapacity(name[0..@min(name.len, 32)]);
76 buf.appendAssumeCapacity('-');
77 buf.appendSliceAssumeCapacity(ver[0..@min(ver.len, 32)]);
78 buf.appendAssumeCapacity('-');
79 var sizedhash: [9]u8 = undefined;
80 std.mem.writeInt(u32, sizedhash[0..4], size, .little);
81 sizedhash[4..].* = digest[0..5].*;
82 _ = std.base64.url_safe_no_pad.Encoder.encode(buf.addManyAsArrayAssumeCapacity(12), &sizedhash);
83 @memset(buf.unusedCapacitySlice(), 0);
84 return result;
85 }
86
87 /// Produces "$hashiname-N-$sizedhash". For packages that lack "build.zig.zon" metadata.
88 /// * hashiname is [5..][0..24] bytes of the SHA-256, urlsafe-base64-encoded, for a total of 32 bytes encoded
89 /// * the semver section is replaced with a hardcoded N which stands for
90 /// "naked". It acts as a version number so that any future updates to the
91 /// hash format can tell this hash format apart. Note that "N" is an
92 /// invalid semver.
93 /// * sizedhash is the same as in `init`.
94 ///
95 /// The hash is broken up this way so that "sizedhash" can be calculated
96 /// exactly the same way in both cases, and so that "name" and "hashiname" can
97 /// be used interchangeably in both cases.
98 pub fn initNaked(digest: Digest, size: u32) Hash {
99 var name: [32]u8 = undefined;
100 _ = std.base64.url_safe_no_pad.Encoder.encode(&name, digest[5..][0..24]);
101 return init(digest, &name, "N", size);
102 }
103};
104
105pub const MultihashFunction = enum(u16) {
106 identity = 0x00,
107 sha1 = 0x11,
108 @"sha2-256" = 0x12,
109 @"sha2-512" = 0x13,
110 @"sha3-512" = 0x14,
111 @"sha3-384" = 0x15,
112 @"sha3-256" = 0x16,
113 @"sha3-224" = 0x17,
114 @"sha2-384" = 0x20,
115 @"sha2-256-trunc254-padded" = 0x1012,
116 @"sha2-224" = 0x1013,
117 @"sha2-512-224" = 0x1014,
118 @"sha2-512-256" = 0x1015,
119 @"blake2b-256" = 0xb220,
120 _,
121};
122
123pub const multihash_function: MultihashFunction = switch (Hash.Algo) {
124 std.crypto.hash.sha2.Sha256 => .@"sha2-256",
125 else => @compileError("unreachable"),
126};
127
128pub fn multiHashHexDigest(digest: Hash.Digest) MultiHashHexDigest {
129 const hex_charset = std.fmt.hex_charset;
130
131 var result: MultiHashHexDigest = undefined;
132
133 result[0] = hex_charset[@intFromEnum(multihash_function) >> 4];
134 result[1] = hex_charset[@intFromEnum(multihash_function) & 15];
135
136 result[2] = hex_charset[Hash.Algo.digest_length >> 4];
137 result[3] = hex_charset[Hash.Algo.digest_length & 15];
138
139 for (digest, 0..) |byte, i| {
140 result[4 + i * 2] = hex_charset[byte >> 4];
141 result[5 + i * 2] = hex_charset[byte & 15];
142 }
143 return result;
144}
145
146comptime {
147 // We avoid unnecessary uleb128 code in hexDigest by asserting here the
148 // values are small enough to be contained in the one-byte encoding.
149 assert(@intFromEnum(multihash_function) < 127);
150 assert(Hash.Algo.digest_length < 127);
151}
152
153test Hash {
154 const example_digest: Hash.Digest = .{
155 0xc7, 0xf5, 0x71, 0xb7, 0xb4, 0xe7, 0x6f, 0x3c, 0xdb, 0x87, 0x7a, 0x7f, 0xdd, 0xf9, 0x77, 0x87,
156 0x9d, 0xd3, 0x86, 0xfa, 0x73, 0x57, 0x9a, 0xf7, 0x9d, 0x1e, 0xdb, 0x8f, 0x3a, 0xd9, 0xbd, 0x9f,
157 };
158 const result: Hash = .init(example_digest, "nasm", "2.16.1-2", 10 * 1024 * 1024);
159 try std.testing.expectEqualStrings("nasm-2.16.1-2-AACgAMf1cbe0", result.toSlice());
160}
161
6162test {
7163 _ = Fetch;
8164}
src/Package/Fetch.zig+92-62
......@@ -56,7 +56,7 @@ package_root: Cache.Path,
5656error_bundle: ErrorBundle.Wip,
5757manifest: ?Manifest,
5858manifest_ast: std.zig.Ast,
59actual_hash: Manifest.Digest,
59computed_hash: ComputedHash,
6060/// Fetch logic notices whether a package has a build.zig file and sets this flag.
6161has_build_zig: bool,
6262/// Indicates whether the task aborted due to an out-of-memory condition.
......@@ -116,8 +116,8 @@ pub const JobQueue = struct {
116116 /// as lazy.
117117 unlazy_set: UnlazySet = .{},
118118
119 pub const Table = std.AutoArrayHashMapUnmanaged(Manifest.MultiHashHexDigest, *Fetch);
120 pub const UnlazySet = std.AutoArrayHashMapUnmanaged(Manifest.MultiHashHexDigest, void);
119 pub const Table = std.AutoArrayHashMapUnmanaged(Package.Hash, *Fetch);
120 pub const UnlazySet = std.AutoArrayHashMapUnmanaged(Package.Hash, void);
121121
122122 pub fn deinit(jq: *JobQueue) void {
123123 if (jq.all_fetches.items.len == 0) return;
......@@ -160,22 +160,24 @@ pub const JobQueue = struct {
160160
161161 // Ensure the generated .zig file is deterministic.
162162 jq.table.sortUnstable(@as(struct {
163 keys: []const Manifest.MultiHashHexDigest,
163 keys: []const Package.Hash,
164164 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
165 return std.mem.lessThan(u8, &ctx.keys[a_index], &ctx.keys[b_index]);
165 return std.mem.lessThan(u8, &ctx.keys[a_index].bytes, &ctx.keys[b_index].bytes);
166166 }
167167 }, .{ .keys = keys }));
168168
169 for (keys, jq.table.values()) |hash, fetch| {
169 for (keys, jq.table.values()) |*hash, fetch| {
170170 if (fetch == jq.all_fetches.items[0]) {
171171 // The first one is a dummy package for the current project.
172172 continue;
173173 }
174174
175 const hash_slice = hash.toSlice();
176
175177 try buf.writer().print(
176178 \\ pub const {} = struct {{
177179 \\
178 , .{std.zig.fmtId(&hash)});
180 , .{std.zig.fmtId(hash_slice)});
179181
180182 lazy: {
181183 switch (fetch.lazy_status) {
......@@ -207,7 +209,7 @@ pub const JobQueue = struct {
207209 try buf.writer().print(
208210 \\ pub const build_zig = @import("{}");
209211 \\
210 , .{std.zig.fmtEscapes(&hash)});
212 , .{std.zig.fmtEscapes(hash_slice)});
211213 }
212214
213215 if (fetch.manifest) |*manifest| {
......@@ -219,7 +221,7 @@ pub const JobQueue = struct {
219221 const h = depDigest(fetch.package_root, jq.global_cache, dep) orelse continue;
220222 try buf.writer().print(
221223 " .{{ \"{}\", \"{}\" }},\n",
222 .{ std.zig.fmtEscapes(name), std.zig.fmtEscapes(&h) },
224 .{ std.zig.fmtEscapes(name), std.zig.fmtEscapes(h.toSlice()) },
223225 );
224226 }
225227
......@@ -251,7 +253,7 @@ pub const JobQueue = struct {
251253 const h = depDigest(root_fetch.package_root, jq.global_cache, dep) orelse continue;
252254 try buf.writer().print(
253255 " .{{ \"{}\", \"{}\" }},\n",
254 .{ std.zig.fmtEscapes(name), std.zig.fmtEscapes(&h) },
256 .{ std.zig.fmtEscapes(name), std.zig.fmtEscapes(h.toSlice()) },
255257 );
256258 }
257259 try buf.appendSlice("};\n");
......@@ -283,7 +285,7 @@ pub const Location = union(enum) {
283285 url: []const u8,
284286 /// If this is null it means the user omitted the hash field from a dependency.
285287 /// It will be an error but the logic should still fetch and print the discovered hash.
286 hash: ?Manifest.MultiHashHexDigest,
288 hash: ?Package.Hash,
287289 };
288290};
289291
......@@ -325,9 +327,11 @@ pub fn run(f: *Fetch) RunError!void {
325327 // "p/$hash/foo", with possibly more directories after "foo".
326328 // We want to fail unless the resolved relative path has a
327329 // prefix of "p/$hash/".
328 const digest_len = @typeInfo(Manifest.MultiHashHexDigest).array.len;
329330 const prefix_len: usize = if (f.job_queue.read_only) 0 else "p/".len;
330 const expected_prefix = f.parent_package_root.sub_path[0 .. prefix_len + digest_len];
331 const parent_sub_path = f.parent_package_root.sub_path;
332 const end = std.mem.indexOfScalarPos(u8, parent_sub_path, prefix_len, fs.path.sep) orelse
333 parent_sub_path.len;
334 const expected_prefix = parent_sub_path[prefix_len..end];
331335 if (!std.mem.startsWith(u8, pkg_root.sub_path, expected_prefix)) {
332336 return f.fail(
333337 f.location_tok,
......@@ -367,9 +371,13 @@ pub fn run(f: *Fetch) RunError!void {
367371 },
368372 };
369373
370 const s = fs.path.sep_str;
371374 if (remote.hash) |expected_hash| {
372 const prefixed_pkg_sub_path = "p" ++ s ++ expected_hash;
375 var prefixed_pkg_sub_path_buffer: [100]u8 = undefined;
376 prefixed_pkg_sub_path_buffer[0] = 'p';
377 prefixed_pkg_sub_path_buffer[1] = fs.path.sep;
378 const hash_slice = expected_hash.toSlice();
379 @memcpy(prefixed_pkg_sub_path_buffer[2..][0..hash_slice.len], hash_slice);
380 const prefixed_pkg_sub_path = prefixed_pkg_sub_path_buffer[0 .. 2 + hash_slice.len];
373381 const prefix_len: usize = if (f.job_queue.read_only) "p/".len else 0;
374382 const pkg_sub_path = prefixed_pkg_sub_path[prefix_len..];
375383 if (cache_root.handle.access(pkg_sub_path, .{})) |_| {
......@@ -437,7 +445,7 @@ fn runResource(
437445 f: *Fetch,
438446 uri_path: []const u8,
439447 resource: *Resource,
440 remote_hash: ?Manifest.MultiHashHexDigest,
448 remote_hash: ?Package.Hash,
441449) RunError!void {
442450 defer resource.deinit();
443451 const arena = f.arena.allocator();
......@@ -499,7 +507,7 @@ fn runResource(
499507 // Empty directories have already been omitted by `unpackResource`.
500508 // Compute the package hash based on the remaining files in the temporary
501509 // directory.
502 f.actual_hash = try computeHash(f, pkg_path, filter);
510 f.computed_hash = try computeHash(f, pkg_path, filter);
503511
504512 break :blk if (unpack_result.root_dir.len > 0)
505513 try fs.path.join(arena, &.{ tmp_dir_sub_path, unpack_result.root_dir })
......@@ -507,6 +515,8 @@ fn runResource(
507515 tmp_dir_sub_path;
508516 };
509517
518 const computed_package_hash = computedPackageHash(f);
519
510520 // Rename the temporary directory into the global zig package cache
511521 // directory. If the hash already exists, delete the temporary directory
512522 // and leave the zig package cache directory untouched as it may be in use
......@@ -515,7 +525,7 @@ fn runResource(
515525
516526 f.package_root = .{
517527 .root_dir = cache_root,
518 .sub_path = try arena.dupe(u8, "p" ++ s ++ Manifest.hexDigest(f.actual_hash)),
528 .sub_path = try std.fmt.allocPrint(arena, "p" ++ s ++ "{s}", .{computed_package_hash.toSlice()}),
519529 };
520530 renameTmpIntoCache(cache_root.handle, package_sub_path, f.package_root.sub_path) catch |err| {
521531 const src = try cache_root.join(arena, &.{tmp_dir_sub_path});
......@@ -534,13 +544,22 @@ fn runResource(
534544 // Validate the computed hash against the expected hash. If invalid, this
535545 // job is done.
536546
537 const actual_hex = Manifest.hexDigest(f.actual_hash);
538547 if (remote_hash) |declared_hash| {
539 if (!std.mem.eql(u8, &declared_hash, &actual_hex)) {
540 return f.fail(f.hash_tok, try eb.printString(
541 "hash mismatch: manifest declares {s} but the fetched package has {s}",
542 .{ declared_hash, actual_hex },
543 ));
548 if (declared_hash.isOld()) {
549 const actual_hex = Package.multiHashHexDigest(f.computed_hash.digest);
550 if (!std.mem.eql(u8, declared_hash.toSlice(), &actual_hex)) {
551 return f.fail(f.hash_tok, try eb.printString(
552 "hash mismatch: manifest declares {s} but the fetched package has {s}",
553 .{ declared_hash.toSlice(), actual_hex },
554 ));
555 }
556 } else {
557 if (!computed_package_hash.eql(&declared_hash)) {
558 return f.fail(f.hash_tok, try eb.printString(
559 "hash mismatch: manifest declares {s} but the fetched package has {s}",
560 .{ declared_hash.toSlice(), computed_package_hash.toSlice() },
561 ));
562 }
544563 }
545564 } else if (!f.omit_missing_hash_error) {
546565 const notes_len = 1;
......@@ -551,7 +570,7 @@ fn runResource(
551570 });
552571 const notes_start = try eb.reserveNotes(notes_len);
553572 eb.extra.items[notes_start] = @intFromEnum(try eb.addErrorMessage(.{
554 .msg = try eb.printString("expected .hash = \"{s}\",", .{&actual_hex}),
573 .msg = try eb.printString("expected .hash = \"{s}\",", .{computed_package_hash.toSlice()}),
555574 }));
556575 return error.FetchFailed;
557576 }
......@@ -562,6 +581,16 @@ fn runResource(
562581 return queueJobsForDeps(f);
563582}
564583
584pub fn computedPackageHash(f: *const Fetch) Package.Hash {
585 const saturated_size = std.math.cast(u32, f.computed_hash.total_size) orelse std.math.maxInt(u32);
586 if (f.manifest) |man| {
587 var version_buffer: [32]u8 = undefined;
588 const version: []const u8 = std.fmt.bufPrint(&version_buffer, "{}", .{man.version}) catch &version_buffer;
589 return .init(f.computed_hash.digest, man.name, version, saturated_size);
590 }
591 return .initNaked(f.computed_hash.digest, saturated_size);
592}
593
565594/// `computeHash` gets a free check for the existence of `build.zig`, but when
566595/// not computing a hash, we need to do a syscall to check for it.
567596fn checkBuildFileExistence(f: *Fetch) RunError!void {
......@@ -673,9 +702,8 @@ fn queueJobsForDeps(f: *Fetch) RunError!void {
673702 .url = url,
674703 .hash = h: {
675704 const h = dep.hash orelse break :h null;
676 const digest_len = @typeInfo(Manifest.MultiHashHexDigest).array.len;
677 const multihash_digest = h[0..digest_len].*;
678 const gop = f.job_queue.table.getOrPutAssumeCapacity(multihash_digest);
705 const pkg_hash: Package.Hash = .fromSlice(h);
706 const gop = f.job_queue.table.getOrPutAssumeCapacity(pkg_hash);
679707 if (gop.found_existing) {
680708 if (!dep.lazy) {
681709 gop.value_ptr.*.lazy_status = .eager;
......@@ -683,15 +711,15 @@ fn queueJobsForDeps(f: *Fetch) RunError!void {
683711 continue;
684712 }
685713 gop.value_ptr.* = new_fetch;
686 break :h multihash_digest;
714 break :h pkg_hash;
687715 },
688716 } },
689717 .path => |rel_path| l: {
690718 // This might produce an invalid path, which is checked for
691719 // at the beginning of run().
692720 const new_root = try f.package_root.resolvePosix(parent_arena, rel_path);
693 const multihash_digest = relativePathDigest(new_root, cache_root);
694 const gop = f.job_queue.table.getOrPutAssumeCapacity(multihash_digest);
721 const pkg_hash = relativePathDigest(new_root, cache_root);
722 const gop = f.job_queue.table.getOrPutAssumeCapacity(pkg_hash);
695723 if (gop.found_existing) {
696724 if (!dep.lazy) {
697725 gop.value_ptr.*.lazy_status = .eager;
......@@ -724,7 +752,7 @@ fn queueJobsForDeps(f: *Fetch) RunError!void {
724752 .error_bundle = undefined,
725753 .manifest = null,
726754 .manifest_ast = undefined,
727 .actual_hash = undefined,
755 .computed_hash = undefined,
728756 .has_build_zig = false,
729757 .oom_flag = false,
730758 .latest_commit = null,
......@@ -746,11 +774,8 @@ fn queueJobsForDeps(f: *Fetch) RunError!void {
746774 }
747775}
748776
749pub fn relativePathDigest(
750 pkg_root: Cache.Path,
751 cache_root: Cache.Directory,
752) Manifest.MultiHashHexDigest {
753 var hasher = Manifest.Hash.init(.{});
777pub fn relativePathDigest(pkg_root: Cache.Path, cache_root: Cache.Directory) Package.Hash {
778 var hasher = Package.Hash.Algo.init(.{});
754779 // This hash is a tuple of:
755780 // * whether it relative to the global cache directory or to the root package
756781 // * the relative file path from there to the build root of the package
......@@ -759,7 +784,7 @@ pub fn relativePathDigest(
759784 else
760785 &package_hash_prefix_project);
761786 hasher.update(pkg_root.sub_path);
762 return Manifest.hexDigest(hasher.finalResult());
787 return .fromSlice(&hasher.finalResult());
763788}
764789
765790pub fn workerRun(f: *Fetch, prog_name: []const u8) void {
......@@ -1387,11 +1412,7 @@ fn recursiveDirectoryCopy(f: *Fetch, dir: fs.Dir, tmp_dir: fs.Dir) anyerror!void
13871412 }
13881413}
13891414
1390pub fn renameTmpIntoCache(
1391 cache_dir: fs.Dir,
1392 tmp_dir_sub_path: []const u8,
1393 dest_dir_sub_path: []const u8,
1394) !void {
1415pub fn renameTmpIntoCache(cache_dir: fs.Dir, tmp_dir_sub_path: []const u8, dest_dir_sub_path: []const u8) !void {
13951416 assert(dest_dir_sub_path[1] == fs.path.sep);
13961417 var handled_missing_dir = false;
13971418 while (true) {
......@@ -1417,16 +1438,17 @@ pub fn renameTmpIntoCache(
14171438 }
14181439}
14191440
1441const ComputedHash = struct {
1442 digest: Package.Hash.Digest,
1443 total_size: u64,
1444};
1445
14201446/// Assumes that files not included in the package have already been filtered
14211447/// prior to calling this function. This ensures that files not protected by
14221448/// the hash are not present on the file system. Empty directories are *not
14231449/// hashed* and must not be present on the file system when calling this
14241450/// function.
1425fn computeHash(
1426 f: *Fetch,
1427 pkg_path: Cache.Path,
1428 filter: Filter,
1429) RunError!Manifest.Digest {
1451fn computeHash(f: *Fetch, pkg_path: Cache.Path, filter: Filter) RunError!ComputedHash {
14301452 // All the path name strings need to be in memory for sorting.
14311453 const arena = f.arena.allocator();
14321454 const gpa = f.arena.child_allocator;
......@@ -1449,6 +1471,9 @@ fn computeHash(
14491471 var walker = try root_dir.walk(gpa);
14501472 defer walker.deinit();
14511473
1474 // Total number of bytes of file contents included in the package.
1475 var total_size: u64 = 0;
1476
14521477 {
14531478 // The final hash will be a hash of each file hashed independently. This
14541479 // allows hashing in parallel.
......@@ -1506,6 +1531,7 @@ fn computeHash(
15061531 .kind = kind,
15071532 .hash = undefined, // to be populated by the worker
15081533 .failure = undefined, // to be populated by the worker
1534 .size = undefined, // to be populated by the worker
15091535 };
15101536 thread_pool.spawnWg(&wait_group, workerHashFile, .{ root_dir, hashed_file });
15111537 try all_files.append(hashed_file);
......@@ -1544,7 +1570,7 @@ fn computeHash(
15441570
15451571 std.mem.sortUnstable(*HashedFile, all_files.items, {}, HashedFile.lessThan);
15461572
1547 var hasher = Manifest.Hash.init(.{});
1573 var hasher = Package.Hash.Algo.init(.{});
15481574 var any_failures = false;
15491575 for (all_files.items) |hashed_file| {
15501576 hashed_file.failure catch |err| {
......@@ -1556,6 +1582,7 @@ fn computeHash(
15561582 });
15571583 };
15581584 hasher.update(&hashed_file.hash);
1585 total_size += hashed_file.size;
15591586 }
15601587 for (deleted_files.items) |deleted_file| {
15611588 deleted_file.failure catch |err| {
......@@ -1580,7 +1607,10 @@ fn computeHash(
15801607 };
15811608 }
15821609
1583 return hasher.finalResult();
1610 return .{
1611 .digest = hasher.finalResult(),
1612 .total_size = total_size,
1613 };
15841614}
15851615
15861616fn dumpHashInfo(all_files: []const *const HashedFile) !void {
......@@ -1609,8 +1639,9 @@ fn workerDeleteFile(dir: fs.Dir, deleted_file: *DeletedFile) void {
16091639
16101640fn hashFileFallible(dir: fs.Dir, hashed_file: *HashedFile) HashedFile.Error!void {
16111641 var buf: [8000]u8 = undefined;
1612 var hasher = Manifest.Hash.init(.{});
1642 var hasher = Package.Hash.Algo.init(.{});
16131643 hasher.update(hashed_file.normalized_path);
1644 var file_size: u64 = 0;
16141645
16151646 switch (hashed_file.kind) {
16161647 .file => {
......@@ -1622,6 +1653,7 @@ fn hashFileFallible(dir: fs.Dir, hashed_file: *HashedFile) HashedFile.Error!void
16221653 while (true) {
16231654 const bytes_read = try file.read(&buf);
16241655 if (bytes_read == 0) break;
1656 file_size += bytes_read;
16251657 hasher.update(buf[0..bytes_read]);
16261658 file_header.update(buf[0..bytes_read]);
16271659 }
......@@ -1641,6 +1673,7 @@ fn hashFileFallible(dir: fs.Dir, hashed_file: *HashedFile) HashedFile.Error!void
16411673 },
16421674 }
16431675 hasher.final(&hashed_file.hash);
1676 hashed_file.size = file_size;
16441677}
16451678
16461679fn deleteFileFallible(dir: fs.Dir, deleted_file: *DeletedFile) DeletedFile.Error!void {
......@@ -1667,9 +1700,10 @@ const DeletedFile = struct {
16671700const HashedFile = struct {
16681701 fs_path: []const u8,
16691702 normalized_path: []const u8,
1670 hash: Manifest.Digest,
1703 hash: Package.Hash.Digest,
16711704 failure: Error!void,
16721705 kind: Kind,
1706 size: u64,
16731707
16741708 const Error =
16751709 fs.File.OpenError ||
......@@ -1744,12 +1778,8 @@ const Filter = struct {
17441778 }
17451779};
17461780
1747pub fn depDigest(
1748 pkg_root: Cache.Path,
1749 cache_root: Cache.Directory,
1750 dep: Manifest.Dependency,
1751) ?Manifest.MultiHashHexDigest {
1752 if (dep.hash) |h| return h[0..Manifest.multihash_hex_digest_len].*;
1781pub fn depDigest(pkg_root: Cache.Path, cache_root: Cache.Directory, dep: Manifest.Dependency) ?Package.Hash {
1782 if (dep.hash) |h| return .fromSlice(h);
17531783
17541784 switch (dep.location) {
17551785 .url => return null,
......@@ -2137,7 +2167,7 @@ test "tarball with excluded duplicate paths" {
21372167 defer fb.deinit();
21382168 try fetch.run();
21392169
2140 const hex_digest = Package.Manifest.hexDigest(fetch.actual_hash);
2170 const hex_digest = Package.multiHashHexDigest(fetch.computed_hash.digest);
21412171 try std.testing.expectEqualStrings(
21422172 "12200bafe035cbb453dd717741b66e9f9d1e6c674069d06121dafa1b2e62eb6b22da",
21432173 &hex_digest,
......@@ -2181,7 +2211,7 @@ test "tarball without root folder" {
21812211 defer fb.deinit();
21822212 try fetch.run();
21832213
2184 const hex_digest = Package.Manifest.hexDigest(fetch.actual_hash);
2214 const hex_digest = Package.multiHashHexDigest(fetch.computed_hash.digest);
21852215 try std.testing.expectEqualStrings(
21862216 "12209f939bfdcb8b501a61bb4a43124dfa1b2848adc60eec1e4624c560357562b793",
21872217 &hex_digest,
......@@ -2222,7 +2252,7 @@ test "set executable bit based on file content" {
22222252 try fetch.run();
22232253 try std.testing.expectEqualStrings(
22242254 "1220fecb4c06a9da8673c87fe8810e15785f1699212f01728eadce094d21effeeef3",
2225 &Manifest.hexDigest(fetch.actual_hash),
2255 &Package.multiHashHexDigest(fetch.computed_hash.digest),
22262256 );
22272257
22282258 var out = try fb.packageDir();
......@@ -2304,7 +2334,7 @@ const TestFetchBuilder = struct {
23042334 .error_bundle = undefined,
23052335 .manifest = null,
23062336 .manifest_ast = undefined,
2307 .actual_hash = undefined,
2337 .computed_hash = undefined,
23082338 .has_build_zig = false,
23092339 .oom_flag = false,
23102340 .latest_commit = null,
src/Package/Manifest.zig+3-66
......@@ -5,15 +5,10 @@ const Allocator = std.mem.Allocator;
55const assert = std.debug.assert;
66const Ast = std.zig.Ast;
77const testing = std.testing;
8const hex_charset = std.fmt.hex_charset;
8const Package = @import("../Package.zig");
99
1010pub const max_bytes = 10 * 1024 * 1024;
1111pub const basename = "build.zig.zon";
12pub const Hash = std.crypto.hash.sha2.Sha256;
13pub const Digest = [Hash.digest_length]u8;
14pub const multihash_len = 1 + 1 + Hash.digest_length;
15pub const multihash_hex_digest_len = 2 * multihash_len;
16pub const MultiHashHexDigest = [multihash_hex_digest_len]u8;
1712
1813pub const Dependency = struct {
1914 location: Location,
......@@ -38,35 +33,6 @@ pub const ErrorMessage = struct {
3833 off: u32,
3934};
4035
41pub const MultihashFunction = enum(u16) {
42 identity = 0x00,
43 sha1 = 0x11,
44 @"sha2-256" = 0x12,
45 @"sha2-512" = 0x13,
46 @"sha3-512" = 0x14,
47 @"sha3-384" = 0x15,
48 @"sha3-256" = 0x16,
49 @"sha3-224" = 0x17,
50 @"sha2-384" = 0x20,
51 @"sha2-256-trunc254-padded" = 0x1012,
52 @"sha2-224" = 0x1013,
53 @"sha2-512-224" = 0x1014,
54 @"sha2-512-256" = 0x1015,
55 @"blake2b-256" = 0xb220,
56 _,
57};
58
59pub const multihash_function: MultihashFunction = switch (Hash) {
60 std.crypto.hash.sha2.Sha256 => .@"sha2-256",
61 else => @compileError("unreachable"),
62};
63comptime {
64 // We avoid unnecessary uleb128 code in hexDigest by asserting here the
65 // values are small enough to be contained in the one-byte encoding.
66 assert(@intFromEnum(multihash_function) < 127);
67 assert(Hash.digest_length < 127);
68}
69
7036name: []const u8,
7137version: std.SemanticVersion,
7238version_node: Ast.Node.Index,
......@@ -164,22 +130,6 @@ pub fn copyErrorsIntoBundle(
164130 }
165131}
166132
167pub fn hexDigest(digest: Digest) MultiHashHexDigest {
168 var result: MultiHashHexDigest = undefined;
169
170 result[0] = hex_charset[@intFromEnum(multihash_function) >> 4];
171 result[1] = hex_charset[@intFromEnum(multihash_function) & 15];
172
173 result[2] = hex_charset[Hash.digest_length >> 4];
174 result[3] = hex_charset[Hash.digest_length & 15];
175
176 for (digest, 0..) |byte, i| {
177 result[4 + i * 2] = hex_charset[byte >> 4];
178 result[5 + i * 2] = hex_charset[byte & 15];
179 }
180 return result;
181}
182
183133const Parse = struct {
184134 gpa: Allocator,
185135 ast: Ast,
......@@ -421,21 +371,8 @@ const Parse = struct {
421371 const tok = main_tokens[node];
422372 const h = try parseString(p, node);
423373
424 if (h.len >= 2) {
425 const their_multihash_func = std.fmt.parseInt(u8, h[0..2], 16) catch |err| {
426 return fail(p, tok, "invalid multihash value: unable to parse hash function: {s}", .{
427 @errorName(err),
428 });
429 };
430 if (@as(MultihashFunction, @enumFromInt(their_multihash_func)) != multihash_function) {
431 return fail(p, tok, "unsupported hash function: only sha2-256 is supported", .{});
432 }
433 }
434
435 if (h.len != multihash_hex_digest_len) {
436 return fail(p, tok, "wrong hash size. expected: {d}, found: {d}", .{
437 multihash_hex_digest_len, h.len,
438 });
374 if (h.len > Package.Hash.max_len) {
375 return fail(p, tok, "hash length exceeds maximum: {d}", .{h.len});
439376 }
440377
441378 return h;
src/main.zig+18-17
......@@ -5197,7 +5197,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
51975197 .error_bundle = undefined,
51985198 .manifest = null,
51995199 .manifest_ast = undefined,
5200 .actual_hash = undefined,
5200 .computed_hash = undefined,
52015201 .has_build_zig = true,
52025202 .oom_flag = false,
52035203 .latest_commit = null,
......@@ -5244,13 +5244,14 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
52445244 const hashes = job_queue.table.keys();
52455245 const fetches = job_queue.table.values();
52465246 try deps_mod.deps.ensureUnusedCapacity(arena, @intCast(hashes.len));
5247 for (hashes, fetches) |hash, f| {
5247 for (hashes, fetches) |*hash, f| {
52485248 if (f == &fetch) {
52495249 // The first one is a dummy package for the current project.
52505250 continue;
52515251 }
52525252 if (!f.has_build_zig)
52535253 continue;
5254 const hash_slice = hash.toSlice();
52545255 const m = try Package.Module.create(arena, .{
52555256 .global_cache_directory = global_cache_directory,
52565257 .paths = .{
......@@ -5260,7 +5261,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
52605261 .fully_qualified_name = try std.fmt.allocPrint(
52615262 arena,
52625263 "root.@dependencies.{s}",
5263 .{&hash},
5264 .{hash_slice},
52645265 ),
52655266 .cc_argv = &.{},
52665267 .inherited = .{},
......@@ -5269,7 +5270,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
52695270 .builtin_mod = builtin_mod,
52705271 .builtin_modules = null, // `builtin_mod` is specified
52715272 });
5272 const hash_cloned = try arena.dupe(u8, &hash);
5273 const hash_cloned = try arena.dupe(u8, hash_slice);
52735274 deps_mod.deps.putAssumeCapacityNoClobber(hash_cloned, m);
52745275 f.module = m;
52755276 }
......@@ -5385,23 +5386,22 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
53855386 var any_errors = false;
53865387 while (it.next()) |hash| {
53875388 if (hash.len == 0) continue;
5388 const digest_len = @typeInfo(Package.Manifest.MultiHashHexDigest).array.len;
5389 if (hash.len != digest_len) {
5390 std.log.err("invalid digest (length {d} instead of {d}): '{s}'", .{
5391 hash.len, digest_len, hash,
5389 if (hash.len > Package.Hash.max_len) {
5390 std.log.err("invalid digest (length {d} exceeds maximum): '{s}'", .{
5391 hash.len, hash,
53925392 });
53935393 any_errors = true;
53945394 continue;
53955395 }
5396 try unlazy_set.put(arena, hash[0..digest_len].*, {});
5396 try unlazy_set.put(arena, .fromSlice(hash), {});
53975397 }
53985398 if (any_errors) process.exit(3);
53995399 if (system_pkg_dir_path) |p| {
54005400 // In this mode, the system needs to provide these packages; they
54015401 // cannot be fetched by Zig.
5402 for (unlazy_set.keys()) |hash| {
5402 for (unlazy_set.keys()) |*hash| {
54035403 std.log.err("lazy dependency package not found: {s}" ++ s ++ "{s}", .{
5404 p, hash,
5404 p, hash.toSlice(),
54055405 });
54065406 }
54075407 std.log.info("remote package fetching disabled due to --system mode", .{});
......@@ -7097,7 +7097,7 @@ fn cmdFetch(
70977097 .error_bundle = undefined,
70987098 .manifest = null,
70997099 .manifest_ast = undefined,
7100 .actual_hash = undefined,
7100 .computed_hash = undefined,
71017101 .has_build_zig = false,
71027102 .oom_flag = false,
71037103 .latest_commit = null,
......@@ -7117,14 +7117,15 @@ fn cmdFetch(
71177117 process.exit(1);
71187118 }
71197119
7120 const hex_digest = Package.Manifest.hexDigest(fetch.actual_hash);
7120 const package_hash = fetch.computedPackageHash();
7121 const package_hash_slice = package_hash.toSlice();
71217122
71227123 root_prog_node.end();
71237124 root_prog_node = .{ .index = .none };
71247125
71257126 const name = switch (save) {
71267127 .no => {
7127 try io.getStdOut().writeAll(hex_digest ++ "\n");
7128 try io.getStdOut().writer().print("{s}\n", .{package_hash_slice});
71287129 return cleanExit();
71297130 },
71307131 .yes, .exact => |name| name: {
......@@ -7194,7 +7195,7 @@ fn cmdFetch(
71947195 \\ }}
71957196 , .{
71967197 std.zig.fmtEscapes(saved_path_or_url),
7197 std.zig.fmtEscapes(&hex_digest),
7198 std.zig.fmtEscapes(package_hash_slice),
71987199 });
71997200
72007201 const new_node_text = try std.fmt.allocPrint(arena, ".{p_} = {s},\n", .{
......@@ -7213,7 +7214,7 @@ fn cmdFetch(
72137214 if (dep.hash) |h| {
72147215 switch (dep.location) {
72157216 .url => |u| {
7216 if (mem.eql(u8, h, &hex_digest) and mem.eql(u8, u, saved_path_or_url)) {
7217 if (mem.eql(u8, h, package_hash_slice) and mem.eql(u8, u, saved_path_or_url)) {
72177218 std.log.info("existing dependency named '{s}' is up-to-date", .{name});
72187219 process.exit(0);
72197220 }
......@@ -7230,7 +7231,7 @@ fn cmdFetch(
72307231 const hash_replace = try std.fmt.allocPrint(
72317232 arena,
72327233 "\"{}\"",
7233 .{std.zig.fmtEscapes(&hex_digest)},
7234 .{std.zig.fmtEscapes(package_hash_slice)},
72347235 );
72357236
72367237 warn("overwriting existing dependency named '{s}'", .{name});