authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-02-28 04:40:05-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2025-02-28 04:40:05-05:00
log6c3cbb0c87a33f4ae408874f6ceb40e372b65914
tree2892f3585939f9f66dcf8e78fcc8606ad861ae6a
parent6b6c1b1b0e04d70a4917f073a6a8bc87a5e8abb3
parentde43f5eb6ae4a569efe15e3469b3de76a86d9cd1
signaturebadge-check Signed by PGP key B5690EEEBB952194

Merge pull request #22994 from ziglang/newhash

implement new package hash format: `$name-$semver-$hash`

10 files changed, 526 insertions(+), 182 deletions(-)

build.zig.zon+2-1
......@@ -1,7 +1,7 @@
11// The Zig compiler is not intended to be consumed as a package.
22// The sole purpose of this manifest file is to test the compiler.
33.{
4 .name = "zig",
4 .name = .zig,
55 .version = "0.0.0",
66 .dependencies = .{
77 .standalone_test_cases = .{
......@@ -12,4 +12,5 @@
1212 },
1313 },
1414 .paths = .{""},
15 .fingerprint = 0xc1ce108124179e16,
1516}
doc/build.zig.zon.md+31-1
......@@ -10,7 +10,7 @@ build.zig.
1010
1111### `name`
1212
13String. Required.
13Enum literal. Required.
1414
1515This is the default name used by packages depending on this one. For example,
1616when a user runs `zig fetch --save <url>`, this field is used as the key in the
......@@ -20,12 +20,42 @@ will stick with this provided value.
2020It is redundant to include "zig" in this name because it is already within the
2121Zig package namespace.
2222
23Must be a valid bare Zig identifier (don't `@` me), limited to 32 bytes.
24
25Together with `fingerprint`, this represents a globally unique package identifier.
26
27### `fingerprint`
28
29Together with `name`, this represents a globally unique package identifier. This
30field is auto-initialized by the toolchain when the package is first created,
31and then *never changes*. This allows Zig to unambiguously detect when one
32package is an updated version of another.
33
34When forking a Zig project, this fingerprint should be regenerated if the upstream
35project is still maintained. Otherwise, the fork is *hostile*, attempting to
36take control over the original project's identity. The fingerprint can be regenerated
37by deleting the field and running `zig build`.
38
39This 64-bit integer is the combination of a 32-bit id component and a 32-bit
40checksum.
41
42The id component within the fingerprint has these restrictions:
43
44`0x00000000` is reserved for legacy packages.
45
46`0xffffffff` is reserved to represent "naked" packages.
47
48The checksum is computed from `name` and serves to protect Zig users from
49accidental id collisions.
50
2351### `version`
2452
2553String. Required.
2654
2755[semver](https://semver.org/)
2856
57Limited to 32 bytes.
58
2959### `minimum_zig_version`
3060
3161String. Optional.
lib/init/build.zig+3-3
......@@ -42,14 +42,14 @@ pub fn build(b: *std.Build) void {
4242 // Modules can depend on one another using the `std.Build.Module.addImport` function.
4343 // This is what allows Zig source code to use `@import("foo")` where 'foo' is not a
4444 // file path. In this case, we set up `exe_mod` to import `lib_mod`.
45 exe_mod.addImport("$_lib", lib_mod);
45 exe_mod.addImport(".NAME_lib", lib_mod);
4646
4747 // Now, we will create a static library based on the module we created above.
4848 // This creates a `std.Build.Step.Compile`, which is the build step responsible
4949 // for actually invoking the compiler.
5050 const lib = b.addLibrary(.{
5151 .linkage = .static,
52 .name = "$",
52 .name = ".NAME",
5353 .root_module = lib_mod,
5454 });
5555
......@@ -61,7 +61,7 @@ pub fn build(b: *std.Build) void {
6161 // This creates another `std.Build.Step.Compile`, but this one builds an executable
6262 // rather than a static library.
6363 const exe = b.addExecutable(.{
64 .name = "$",
64 .name = ".NAME",
6565 .root_module = exe_mod,
6666 });
6767
lib/init/build.zig.zon+19-1
......@@ -6,12 +6,30 @@
66 //
77 // It is redundant to include "zig" in this name because it is already
88 // within the Zig package namespace.
9 .name = "$",
9 .name = .LITNAME,
1010
1111 // This is a [Semantic Version](https://semver.org/).
1212 // In a future version of Zig it will be used for package deduplication.
1313 .version = "0.0.0",
1414
15 // Together with name, this represents a globally unique package
16 // identifier. This field is generated by the Zig toolchain when the
17 // package is first created, and then *never changes*. This allows
18 // unambiguous detection of one package being an updated version of
19 // another.
20 //
21 // When forking a Zig project, this id should be regenerated (delete the
22 // field and run `zig build`) if the upstream project is still maintained.
23 // Otherwise, the fork is *hostile*, attempting to take control over the
24 // original project's identity. Thus it is recommended to leave the comment
25 // on the following line intact, so that it shows up in code reviews that
26 // modify the field.
27 .fingerprint = .FINGERPRINT, // Changing this has security and trust implications.
28
29 // Tracks the earliest Zig version that the package considers to be a
30 // supported use case.
31 .minimum_zig_version = ".ZIGVER",
32
1533 // This field is optional.
1634 // This is currently advisory only; Zig does not yet do anything
1735 // with this value.
lib/init/src/main.zig+1-1
......@@ -43,4 +43,4 @@ test "fuzz example" {
4343const std = @import("std");
4444
4545/// This imports the separate module containing `root.zig`. Take a look in `build.zig` for details.
46const lib = @import("$_lib");
46const lib = @import(".NAME_lib");
lib/std/array_list.zig-7
......@@ -2250,10 +2250,3 @@ test "return OutOfMemory when capacity would exceed maximum usize integer value"
22502250 try testing.expectError(error.OutOfMemory, list.ensureUnusedCapacity(2));
22512251 }
22522252}
2253
2254test "ArrayListAligned with non-native alignment compiles unusedCapabitySlice" {
2255 var list = ArrayListAligned(u8, 4).init(testing.allocator);
2256 defer list.deinit();
2257 try list.appendNTimes(1, 4);
2258 _ = list.unusedCapacitySlice();
2259}
src/Package.zig+192
......@@ -1,8 +1,200 @@
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
13pub const Fingerprint = packed struct(u64) {
14 id: u32,
15 checksum: u32,
16
17 pub fn generate(name: []const u8) Fingerprint {
18 return .{
19 .id = std.crypto.random.intRangeLessThan(u32, 1, 0xffffffff),
20 .checksum = std.hash.Crc32.hash(name),
21 };
22 }
23
24 pub fn validate(n: Fingerprint, name: []const u8) bool {
25 switch (n.id) {
26 0x00000000, 0xffffffff => return false,
27 else => return std.hash.Crc32.hash(name) == n.checksum,
28 }
29 }
30
31 pub fn int(n: Fingerprint) u64 {
32 return @bitCast(n);
33 }
34};
35
36/// A user-readable, file system safe hash that identifies an exact package
37/// snapshot, including file contents.
38///
39/// The hash is not only to prevent collisions but must resist attacks where
40/// the adversary fully controls the contents being hashed. Thus, it contains
41/// a full SHA-256 digest.
42///
43/// This data structure can be used to store the legacy hash format too. Legacy
44/// hash format is scheduled to be removed after 0.14.0 is tagged.
45///
46/// There's also a third way this structure is used. When using path rather than
47/// hash, a unique hash is still needed, so one is computed based on the path.
48pub const Hash = struct {
49 /// Maximum size of a package hash. Unused bytes at the end are
50 /// filled with zeroes.
51 bytes: [max_len]u8,
52
53 pub const Algo = std.crypto.hash.sha2.Sha256;
54 pub const Digest = [Algo.digest_length]u8;
55
56 /// Example: "nnnn-vvvv-hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh"
57 pub const max_len = 32 + 1 + 32 + 1 + (32 + 32 + 200) / 6;
58
59 pub fn fromSlice(s: []const u8) Hash {
60 assert(s.len <= max_len);
61 var result: Hash = undefined;
62 @memcpy(result.bytes[0..s.len], s);
63 @memset(result.bytes[s.len..], 0);
64 return result;
65 }
66
67 pub fn toSlice(ph: *const Hash) []const u8 {
68 var end: usize = ph.bytes.len;
69 while (true) {
70 end -= 1;
71 if (ph.bytes[end] != 0) return ph.bytes[0 .. end + 1];
72 }
73 }
74
75 pub fn eql(a: *const Hash, b: *const Hash) bool {
76 return std.mem.eql(u8, &a.bytes, &b.bytes);
77 }
78
79 /// Distinguishes whether the legacy multihash format is being stored here.
80 pub fn isOld(h: *const Hash) bool {
81 if (h.bytes.len < 2) return false;
82 const their_multihash_func = std.fmt.parseInt(u8, h.bytes[0..2], 16) catch return false;
83 if (@as(MultihashFunction, @enumFromInt(their_multihash_func)) != multihash_function) return false;
84 if (h.toSlice().len != multihash_hex_digest_len) return false;
85 return std.mem.indexOfScalar(u8, &h.bytes, '-') == null;
86 }
87
88 test isOld {
89 const h: Hash = .fromSlice("1220138f4aba0c01e66b68ed9e1e1e74614c06e4743d88bc58af4f1c3dd0aae5fea7");
90 try std.testing.expect(h.isOld());
91 }
92
93 /// Produces "$name-$semver-$hashplus".
94 /// * name is the name field from build.zig.zon, asserted to be at most 32
95 /// bytes and assumed be a valid zig identifier
96 /// * semver is the version field from build.zig.zon, asserted to be at
97 /// most 32 bytes
98 /// * hashplus is the following 33-byte array, base64 encoded using -_ to make
99 /// it filesystem safe:
100 /// - (4 bytes) LE u32 Package ID
101 /// - (4 bytes) LE u32 total decompressed size in bytes, overflow saturated
102 /// - (25 bytes) truncated SHA-256 digest of hashed files of the package
103 pub fn init(digest: Digest, name: []const u8, ver: []const u8, id: u32, size: u32) Hash {
104 assert(name.len <= 32);
105 assert(ver.len <= 32);
106 var result: Hash = undefined;
107 var buf: std.ArrayListUnmanaged(u8) = .initBuffer(&result.bytes);
108 buf.appendSliceAssumeCapacity(name);
109 buf.appendAssumeCapacity('-');
110 buf.appendSliceAssumeCapacity(ver);
111 buf.appendAssumeCapacity('-');
112 var hashplus: [33]u8 = undefined;
113 std.mem.writeInt(u32, hashplus[0..4], id, .little);
114 std.mem.writeInt(u32, hashplus[4..8], size, .little);
115 hashplus[8..].* = digest[0..25].*;
116 _ = std.base64.url_safe_no_pad.Encoder.encode(buf.addManyAsArrayAssumeCapacity(44), &hashplus);
117 @memset(buf.unusedCapacitySlice(), 0);
118 return result;
119 }
120
121 /// Produces a unique hash based on the path provided. The result should
122 /// not be user-visible.
123 pub fn initPath(sub_path: []const u8, is_global: bool) Hash {
124 var result: Hash = .{ .bytes = @splat(0) };
125 var i: usize = 0;
126 if (is_global) {
127 result.bytes[0] = '/';
128 i += 1;
129 }
130 if (i + sub_path.len <= result.bytes.len) {
131 @memcpy(result.bytes[i..][0..sub_path.len], sub_path);
132 return result;
133 }
134 var bin_digest: [Algo.digest_length]u8 = undefined;
135 Algo.hash(sub_path, &bin_digest, .{});
136 _ = std.fmt.bufPrint(result.bytes[i..], "{}", .{std.fmt.fmtSliceHexLower(&bin_digest)}) catch unreachable;
137 return result;
138 }
139};
140
141pub const MultihashFunction = enum(u16) {
142 identity = 0x00,
143 sha1 = 0x11,
144 @"sha2-256" = 0x12,
145 @"sha2-512" = 0x13,
146 @"sha3-512" = 0x14,
147 @"sha3-384" = 0x15,
148 @"sha3-256" = 0x16,
149 @"sha3-224" = 0x17,
150 @"sha2-384" = 0x20,
151 @"sha2-256-trunc254-padded" = 0x1012,
152 @"sha2-224" = 0x1013,
153 @"sha2-512-224" = 0x1014,
154 @"sha2-512-256" = 0x1015,
155 @"blake2b-256" = 0xb220,
156 _,
157};
158
159pub const multihash_function: MultihashFunction = switch (Hash.Algo) {
160 std.crypto.hash.sha2.Sha256 => .@"sha2-256",
161 else => unreachable,
162};
163
164pub fn multiHashHexDigest(digest: Hash.Digest) MultiHashHexDigest {
165 const hex_charset = std.fmt.hex_charset;
166
167 var result: MultiHashHexDigest = undefined;
168
169 result[0] = hex_charset[@intFromEnum(multihash_function) >> 4];
170 result[1] = hex_charset[@intFromEnum(multihash_function) & 15];
171
172 result[2] = hex_charset[Hash.Algo.digest_length >> 4];
173 result[3] = hex_charset[Hash.Algo.digest_length & 15];
174
175 for (digest, 0..) |byte, i| {
176 result[4 + i * 2] = hex_charset[byte >> 4];
177 result[5 + i * 2] = hex_charset[byte & 15];
178 }
179 return result;
180}
181
182comptime {
183 // We avoid unnecessary uleb128 code in hexDigest by asserting here the
184 // values are small enough to be contained in the one-byte encoding.
185 assert(@intFromEnum(multihash_function) < 127);
186 assert(Hash.Algo.digest_length < 127);
187}
188
189test Hash {
190 const example_digest: Hash.Digest = .{
191 0xc7, 0xf5, 0x71, 0xb7, 0xb4, 0xe7, 0x6f, 0x3c, 0xdb, 0x87, 0x7a, 0x7f, 0xdd, 0xf9, 0x77, 0x87,
192 0x9d, 0xd3, 0x86, 0xfa, 0x73, 0x57, 0x9a, 0xf7, 0x9d, 0x1e, 0xdb, 0x8f, 0x3a, 0xd9, 0xbd, 0x9f,
193 };
194 const result: Hash = .init(example_digest, "nasm", "2.16.1-3", 0xcafebabe, 10 * 1024 * 1024);
195 try std.testing.expectEqualStrings("nasm-2.16.1-3-vrr-ygAAoADH9XG3tOdvPNuHen_d-XeHndOG-nNXmved", result.toSlice());
196}
197
6198test {
7199 _ = Fetch;
8200}
src/Package/Fetch.zig+102-75
......@@ -44,6 +44,8 @@ omit_missing_hash_error: bool,
4444/// which specifies inclusion rules. This is intended to be true for the first
4545/// fetch task and false for the recursive dependencies.
4646allow_missing_paths_field: bool,
47allow_missing_fingerprint: bool,
48allow_name_string: bool,
4749/// If true and URL points to a Git repository, will use the latest commit.
4850use_latest_commit: bool,
4951
......@@ -56,7 +58,7 @@ package_root: Cache.Path,
5658error_bundle: ErrorBundle.Wip,
5759manifest: ?Manifest,
5860manifest_ast: std.zig.Ast,
59actual_hash: Manifest.Digest,
61computed_hash: ComputedHash,
6062/// Fetch logic notices whether a package has a build.zig file and sets this flag.
6163has_build_zig: bool,
6264/// Indicates whether the task aborted due to an out-of-memory condition.
......@@ -116,8 +118,8 @@ pub const JobQueue = struct {
116118 /// as lazy.
117119 unlazy_set: UnlazySet = .{},
118120
119 pub const Table = std.AutoArrayHashMapUnmanaged(Manifest.MultiHashHexDigest, *Fetch);
120 pub const UnlazySet = std.AutoArrayHashMapUnmanaged(Manifest.MultiHashHexDigest, void);
121 pub const Table = std.AutoArrayHashMapUnmanaged(Package.Hash, *Fetch);
122 pub const UnlazySet = std.AutoArrayHashMapUnmanaged(Package.Hash, void);
121123
122124 pub fn deinit(jq: *JobQueue) void {
123125 if (jq.all_fetches.items.len == 0) return;
......@@ -160,22 +162,24 @@ pub const JobQueue = struct {
160162
161163 // Ensure the generated .zig file is deterministic.
162164 jq.table.sortUnstable(@as(struct {
163 keys: []const Manifest.MultiHashHexDigest,
165 keys: []const Package.Hash,
164166 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]);
167 return std.mem.lessThan(u8, &ctx.keys[a_index].bytes, &ctx.keys[b_index].bytes);
166168 }
167169 }, .{ .keys = keys }));
168170
169 for (keys, jq.table.values()) |hash, fetch| {
171 for (keys, jq.table.values()) |*hash, fetch| {
170172 if (fetch == jq.all_fetches.items[0]) {
171173 // The first one is a dummy package for the current project.
172174 continue;
173175 }
174176
177 const hash_slice = hash.toSlice();
178
175179 try buf.writer().print(
176180 \\ pub const {} = struct {{
177181 \\
178 , .{std.zig.fmtId(&hash)});
182 , .{std.zig.fmtId(hash_slice)});
179183
180184 lazy: {
181185 switch (fetch.lazy_status) {
......@@ -207,7 +211,7 @@ pub const JobQueue = struct {
207211 try buf.writer().print(
208212 \\ pub const build_zig = @import("{}");
209213 \\
210 , .{std.zig.fmtEscapes(&hash)});
214 , .{std.zig.fmtEscapes(hash_slice)});
211215 }
212216
213217 if (fetch.manifest) |*manifest| {
......@@ -219,7 +223,7 @@ pub const JobQueue = struct {
219223 const h = depDigest(fetch.package_root, jq.global_cache, dep) orelse continue;
220224 try buf.writer().print(
221225 " .{{ \"{}\", \"{}\" }},\n",
222 .{ std.zig.fmtEscapes(name), std.zig.fmtEscapes(&h) },
226 .{ std.zig.fmtEscapes(name), std.zig.fmtEscapes(h.toSlice()) },
223227 );
224228 }
225229
......@@ -251,7 +255,7 @@ pub const JobQueue = struct {
251255 const h = depDigest(root_fetch.package_root, jq.global_cache, dep) orelse continue;
252256 try buf.writer().print(
253257 " .{{ \"{}\", \"{}\" }},\n",
254 .{ std.zig.fmtEscapes(name), std.zig.fmtEscapes(&h) },
258 .{ std.zig.fmtEscapes(name), std.zig.fmtEscapes(h.toSlice()) },
255259 );
256260 }
257261 try buf.appendSlice("};\n");
......@@ -283,7 +287,7 @@ pub const Location = union(enum) {
283287 url: []const u8,
284288 /// If this is null it means the user omitted the hash field from a dependency.
285289 /// It will be an error but the logic should still fetch and print the discovered hash.
286 hash: ?Manifest.MultiHashHexDigest,
290 hash: ?Package.Hash,
287291 };
288292};
289293
......@@ -325,9 +329,11 @@ pub fn run(f: *Fetch) RunError!void {
325329 // "p/$hash/foo", with possibly more directories after "foo".
326330 // We want to fail unless the resolved relative path has a
327331 // prefix of "p/$hash/".
328 const digest_len = @typeInfo(Manifest.MultiHashHexDigest).array.len;
329332 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];
333 const parent_sub_path = f.parent_package_root.sub_path;
334 const end = std.mem.indexOfScalarPos(u8, parent_sub_path, prefix_len, fs.path.sep) orelse
335 parent_sub_path.len;
336 const expected_prefix = parent_sub_path[prefix_len..end];
331337 if (!std.mem.startsWith(u8, pkg_root.sub_path, expected_prefix)) {
332338 return f.fail(
333339 f.location_tok,
......@@ -367,9 +373,13 @@ pub fn run(f: *Fetch) RunError!void {
367373 },
368374 };
369375
370 const s = fs.path.sep_str;
371376 if (remote.hash) |expected_hash| {
372 const prefixed_pkg_sub_path = "p" ++ s ++ expected_hash;
377 var prefixed_pkg_sub_path_buffer: [Package.Hash.max_len + 2]u8 = undefined;
378 prefixed_pkg_sub_path_buffer[0] = 'p';
379 prefixed_pkg_sub_path_buffer[1] = fs.path.sep;
380 const hash_slice = expected_hash.toSlice();
381 @memcpy(prefixed_pkg_sub_path_buffer[2..][0..hash_slice.len], hash_slice);
382 const prefixed_pkg_sub_path = prefixed_pkg_sub_path_buffer[0 .. 2 + hash_slice.len];
373383 const prefix_len: usize = if (f.job_queue.read_only) "p/".len else 0;
374384 const pkg_sub_path = prefixed_pkg_sub_path[prefix_len..];
375385 if (cache_root.handle.access(pkg_sub_path, .{})) |_| {
......@@ -437,7 +447,7 @@ fn runResource(
437447 f: *Fetch,
438448 uri_path: []const u8,
439449 resource: *Resource,
440 remote_hash: ?Manifest.MultiHashHexDigest,
450 remote_hash: ?Package.Hash,
441451) RunError!void {
442452 defer resource.deinit();
443453 const arena = f.arena.allocator();
......@@ -499,7 +509,7 @@ fn runResource(
499509 // Empty directories have already been omitted by `unpackResource`.
500510 // Compute the package hash based on the remaining files in the temporary
501511 // directory.
502 f.actual_hash = try computeHash(f, pkg_path, filter);
512 f.computed_hash = try computeHash(f, pkg_path, filter);
503513
504514 break :blk if (unpack_result.root_dir.len > 0)
505515 try fs.path.join(arena, &.{ tmp_dir_sub_path, unpack_result.root_dir })
......@@ -507,6 +517,8 @@ fn runResource(
507517 tmp_dir_sub_path;
508518 };
509519
520 const computed_package_hash = computedPackageHash(f);
521
510522 // Rename the temporary directory into the global zig package cache
511523 // directory. If the hash already exists, delete the temporary directory
512524 // and leave the zig package cache directory untouched as it may be in use
......@@ -515,7 +527,7 @@ fn runResource(
515527
516528 f.package_root = .{
517529 .root_dir = cache_root,
518 .sub_path = try arena.dupe(u8, "p" ++ s ++ Manifest.hexDigest(f.actual_hash)),
530 .sub_path = try std.fmt.allocPrint(arena, "p" ++ s ++ "{s}", .{computed_package_hash.toSlice()}),
519531 };
520532 renameTmpIntoCache(cache_root.handle, package_sub_path, f.package_root.sub_path) catch |err| {
521533 const src = try cache_root.join(arena, &.{tmp_dir_sub_path});
......@@ -534,13 +546,22 @@ fn runResource(
534546 // Validate the computed hash against the expected hash. If invalid, this
535547 // job is done.
536548
537 const actual_hex = Manifest.hexDigest(f.actual_hash);
538549 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 ));
550 if (declared_hash.isOld()) {
551 const actual_hex = Package.multiHashHexDigest(f.computed_hash.digest);
552 if (!std.mem.eql(u8, declared_hash.toSlice(), &actual_hex)) {
553 return f.fail(f.hash_tok, try eb.printString(
554 "hash mismatch: manifest declares {s} but the fetched package has {s}",
555 .{ declared_hash.toSlice(), actual_hex },
556 ));
557 }
558 } else {
559 if (!computed_package_hash.eql(&declared_hash)) {
560 return f.fail(f.hash_tok, try eb.printString(
561 "hash mismatch: manifest declares {s} but the fetched package has {s}",
562 .{ declared_hash.toSlice(), computed_package_hash.toSlice() },
563 ));
564 }
544565 }
545566 } else if (!f.omit_missing_hash_error) {
546567 const notes_len = 1;
......@@ -551,7 +572,7 @@ fn runResource(
551572 });
552573 const notes_start = try eb.reserveNotes(notes_len);
553574 eb.extra.items[notes_start] = @intFromEnum(try eb.addErrorMessage(.{
554 .msg = try eb.printString("expected .hash = \"{s}\",", .{&actual_hex}),
575 .msg = try eb.printString("expected .hash = \"{s}\",", .{computed_package_hash.toSlice()}),
555576 }));
556577 return error.FetchFailed;
557578 }
......@@ -562,6 +583,18 @@ fn runResource(
562583 return queueJobsForDeps(f);
563584}
564585
586pub fn computedPackageHash(f: *const Fetch) Package.Hash {
587 const saturated_size = std.math.cast(u32, f.computed_hash.total_size) orelse std.math.maxInt(u32);
588 if (f.manifest) |man| {
589 var version_buffer: [32]u8 = undefined;
590 const version: []const u8 = std.fmt.bufPrint(&version_buffer, "{}", .{man.version}) catch &version_buffer;
591 return .init(f.computed_hash.digest, man.name, version, man.id, saturated_size);
592 }
593 // In the future build.zig.zon fields will be added to allow overriding these values
594 // for naked tarballs.
595 return .init(f.computed_hash.digest, "N", "V", 0xffff, saturated_size);
596}
597
565598/// `computeHash` gets a free check for the existence of `build.zig`, but when
566599/// not computing a hash, we need to do a syscall to check for it.
567600fn checkBuildFileExistence(f: *Fetch) RunError!void {
......@@ -616,11 +649,13 @@ fn loadManifest(f: *Fetch, pkg_root: Cache.Path) RunError!void {
616649
617650 f.manifest = try Manifest.parse(arena, ast.*, .{
618651 .allow_missing_paths_field = f.allow_missing_paths_field,
652 .allow_missing_fingerprint = f.allow_missing_fingerprint,
653 .allow_name_string = f.allow_name_string,
619654 });
620655 const manifest = &f.manifest.?;
621656
622657 if (manifest.errors.len > 0) {
623 const src_path = try eb.printString("{}{s}", .{ pkg_root, Manifest.basename });
658 const src_path = try eb.printString("{}" ++ fs.path.sep_str ++ "{s}", .{ pkg_root, Manifest.basename });
624659 try manifest.copyErrorsIntoBundle(ast.*, src_path, eb);
625660 return error.FetchFailed;
626661 }
......@@ -673,9 +708,8 @@ fn queueJobsForDeps(f: *Fetch) RunError!void {
673708 .url = url,
674709 .hash = h: {
675710 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);
711 const pkg_hash: Package.Hash = .fromSlice(h);
712 const gop = f.job_queue.table.getOrPutAssumeCapacity(pkg_hash);
679713 if (gop.found_existing) {
680714 if (!dep.lazy) {
681715 gop.value_ptr.*.lazy_status = .eager;
......@@ -683,15 +717,15 @@ fn queueJobsForDeps(f: *Fetch) RunError!void {
683717 continue;
684718 }
685719 gop.value_ptr.* = new_fetch;
686 break :h multihash_digest;
720 break :h pkg_hash;
687721 },
688722 } },
689723 .path => |rel_path| l: {
690724 // This might produce an invalid path, which is checked for
691725 // at the beginning of run().
692726 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);
727 const pkg_hash = relativePathDigest(new_root, cache_root);
728 const gop = f.job_queue.table.getOrPutAssumeCapacity(pkg_hash);
695729 if (gop.found_existing) {
696730 if (!dep.lazy) {
697731 gop.value_ptr.*.lazy_status = .eager;
......@@ -718,13 +752,15 @@ fn queueJobsForDeps(f: *Fetch) RunError!void {
718752 .job_queue = f.job_queue,
719753 .omit_missing_hash_error = false,
720754 .allow_missing_paths_field = true,
755 .allow_missing_fingerprint = true,
756 .allow_name_string = true,
721757 .use_latest_commit = false,
722758
723759 .package_root = undefined,
724760 .error_bundle = undefined,
725761 .manifest = null,
726762 .manifest_ast = undefined,
727 .actual_hash = undefined,
763 .computed_hash = undefined,
728764 .has_build_zig = false,
729765 .oom_flag = false,
730766 .latest_commit = null,
......@@ -746,20 +782,8 @@ fn queueJobsForDeps(f: *Fetch) RunError!void {
746782 }
747783}
748784
749pub fn relativePathDigest(
750 pkg_root: Cache.Path,
751 cache_root: Cache.Directory,
752) Manifest.MultiHashHexDigest {
753 var hasher = Manifest.Hash.init(.{});
754 // This hash is a tuple of:
755 // * whether it relative to the global cache directory or to the root package
756 // * the relative file path from there to the build root of the package
757 hasher.update(if (pkg_root.root_dir.eql(cache_root))
758 &package_hash_prefix_cached
759 else
760 &package_hash_prefix_project);
761 hasher.update(pkg_root.sub_path);
762 return Manifest.hexDigest(hasher.finalResult());
785pub fn relativePathDigest(pkg_root: Cache.Path, cache_root: Cache.Directory) Package.Hash {
786 return .initPath(pkg_root.sub_path, pkg_root.root_dir.eql(cache_root));
763787}
764788
765789pub fn workerRun(f: *Fetch, prog_name: []const u8) void {
......@@ -1387,11 +1411,7 @@ fn recursiveDirectoryCopy(f: *Fetch, dir: fs.Dir, tmp_dir: fs.Dir) anyerror!void
13871411 }
13881412}
13891413
1390pub fn renameTmpIntoCache(
1391 cache_dir: fs.Dir,
1392 tmp_dir_sub_path: []const u8,
1393 dest_dir_sub_path: []const u8,
1394) !void {
1414pub fn renameTmpIntoCache(cache_dir: fs.Dir, tmp_dir_sub_path: []const u8, dest_dir_sub_path: []const u8) !void {
13951415 assert(dest_dir_sub_path[1] == fs.path.sep);
13961416 var handled_missing_dir = false;
13971417 while (true) {
......@@ -1417,16 +1437,17 @@ pub fn renameTmpIntoCache(
14171437 }
14181438}
14191439
1440const ComputedHash = struct {
1441 digest: Package.Hash.Digest,
1442 total_size: u64,
1443};
1444
14201445/// Assumes that files not included in the package have already been filtered
14211446/// prior to calling this function. This ensures that files not protected by
14221447/// the hash are not present on the file system. Empty directories are *not
14231448/// hashed* and must not be present on the file system when calling this
14241449/// function.
1425fn computeHash(
1426 f: *Fetch,
1427 pkg_path: Cache.Path,
1428 filter: Filter,
1429) RunError!Manifest.Digest {
1450fn computeHash(f: *Fetch, pkg_path: Cache.Path, filter: Filter) RunError!ComputedHash {
14301451 // All the path name strings need to be in memory for sorting.
14311452 const arena = f.arena.allocator();
14321453 const gpa = f.arena.child_allocator;
......@@ -1449,6 +1470,9 @@ fn computeHash(
14491470 var walker = try root_dir.walk(gpa);
14501471 defer walker.deinit();
14511472
1473 // Total number of bytes of file contents included in the package.
1474 var total_size: u64 = 0;
1475
14521476 {
14531477 // The final hash will be a hash of each file hashed independently. This
14541478 // allows hashing in parallel.
......@@ -1506,6 +1530,7 @@ fn computeHash(
15061530 .kind = kind,
15071531 .hash = undefined, // to be populated by the worker
15081532 .failure = undefined, // to be populated by the worker
1533 .size = undefined, // to be populated by the worker
15091534 };
15101535 thread_pool.spawnWg(&wait_group, workerHashFile, .{ root_dir, hashed_file });
15111536 try all_files.append(hashed_file);
......@@ -1544,7 +1569,7 @@ fn computeHash(
15441569
15451570 std.mem.sortUnstable(*HashedFile, all_files.items, {}, HashedFile.lessThan);
15461571
1547 var hasher = Manifest.Hash.init(.{});
1572 var hasher = Package.Hash.Algo.init(.{});
15481573 var any_failures = false;
15491574 for (all_files.items) |hashed_file| {
15501575 hashed_file.failure catch |err| {
......@@ -1556,6 +1581,7 @@ fn computeHash(
15561581 });
15571582 };
15581583 hasher.update(&hashed_file.hash);
1584 total_size += hashed_file.size;
15591585 }
15601586 for (deleted_files.items) |deleted_file| {
15611587 deleted_file.failure catch |err| {
......@@ -1580,7 +1606,10 @@ fn computeHash(
15801606 };
15811607 }
15821608
1583 return hasher.finalResult();
1609 return .{
1610 .digest = hasher.finalResult(),
1611 .total_size = total_size,
1612 };
15841613}
15851614
15861615fn dumpHashInfo(all_files: []const *const HashedFile) !void {
......@@ -1609,8 +1638,9 @@ fn workerDeleteFile(dir: fs.Dir, deleted_file: *DeletedFile) void {
16091638
16101639fn hashFileFallible(dir: fs.Dir, hashed_file: *HashedFile) HashedFile.Error!void {
16111640 var buf: [8000]u8 = undefined;
1612 var hasher = Manifest.Hash.init(.{});
1641 var hasher = Package.Hash.Algo.init(.{});
16131642 hasher.update(hashed_file.normalized_path);
1643 var file_size: u64 = 0;
16141644
16151645 switch (hashed_file.kind) {
16161646 .file => {
......@@ -1622,6 +1652,7 @@ fn hashFileFallible(dir: fs.Dir, hashed_file: *HashedFile) HashedFile.Error!void
16221652 while (true) {
16231653 const bytes_read = try file.read(&buf);
16241654 if (bytes_read == 0) break;
1655 file_size += bytes_read;
16251656 hasher.update(buf[0..bytes_read]);
16261657 file_header.update(buf[0..bytes_read]);
16271658 }
......@@ -1641,6 +1672,7 @@ fn hashFileFallible(dir: fs.Dir, hashed_file: *HashedFile) HashedFile.Error!void
16411672 },
16421673 }
16431674 hasher.final(&hashed_file.hash);
1675 hashed_file.size = file_size;
16441676}
16451677
16461678fn deleteFileFallible(dir: fs.Dir, deleted_file: *DeletedFile) DeletedFile.Error!void {
......@@ -1667,9 +1699,10 @@ const DeletedFile = struct {
16671699const HashedFile = struct {
16681700 fs_path: []const u8,
16691701 normalized_path: []const u8,
1670 hash: Manifest.Digest,
1702 hash: Package.Hash.Digest,
16711703 failure: Error!void,
16721704 kind: Kind,
1705 size: u64,
16731706
16741707 const Error =
16751708 fs.File.OpenError ||
......@@ -1744,12 +1777,8 @@ const Filter = struct {
17441777 }
17451778};
17461779
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].*;
1780pub fn depDigest(pkg_root: Cache.Path, cache_root: Cache.Directory, dep: Manifest.Dependency) ?Package.Hash {
1781 if (dep.hash) |h| return .fromSlice(h);
17531782
17541783 switch (dep.location) {
17551784 .url => return null,
......@@ -1763,10 +1792,6 @@ pub fn depDigest(
17631792 }
17641793}
17651794
1766// These are random bytes.
1767const package_hash_prefix_cached = [8]u8{ 0x53, 0x7e, 0xfa, 0x94, 0x65, 0xe9, 0xf8, 0x73 };
1768const package_hash_prefix_project = [8]u8{ 0xe1, 0x25, 0xee, 0xfa, 0xa6, 0x17, 0x38, 0xcc };
1769
17701795const builtin = @import("builtin");
17711796const std = @import("std");
17721797const fs = std.fs;
......@@ -2137,7 +2162,7 @@ test "tarball with excluded duplicate paths" {
21372162 defer fb.deinit();
21382163 try fetch.run();
21392164
2140 const hex_digest = Package.Manifest.hexDigest(fetch.actual_hash);
2165 const hex_digest = Package.multiHashHexDigest(fetch.computed_hash.digest);
21412166 try std.testing.expectEqualStrings(
21422167 "12200bafe035cbb453dd717741b66e9f9d1e6c674069d06121dafa1b2e62eb6b22da",
21432168 &hex_digest,
......@@ -2181,7 +2206,7 @@ test "tarball without root folder" {
21812206 defer fb.deinit();
21822207 try fetch.run();
21832208
2184 const hex_digest = Package.Manifest.hexDigest(fetch.actual_hash);
2209 const hex_digest = Package.multiHashHexDigest(fetch.computed_hash.digest);
21852210 try std.testing.expectEqualStrings(
21862211 "12209f939bfdcb8b501a61bb4a43124dfa1b2848adc60eec1e4624c560357562b793",
21872212 &hex_digest,
......@@ -2222,7 +2247,7 @@ test "set executable bit based on file content" {
22222247 try fetch.run();
22232248 try std.testing.expectEqualStrings(
22242249 "1220fecb4c06a9da8673c87fe8810e15785f1699212f01728eadce094d21effeeef3",
2225 &Manifest.hexDigest(fetch.actual_hash),
2250 &Package.multiHashHexDigest(fetch.computed_hash.digest),
22262251 );
22272252
22282253 var out = try fb.packageDir();
......@@ -2298,13 +2323,15 @@ const TestFetchBuilder = struct {
22982323 .job_queue = &self.job_queue,
22992324 .omit_missing_hash_error = true,
23002325 .allow_missing_paths_field = false,
2326 .allow_missing_fingerprint = true, // so we can keep using the old testdata .tar.gz
2327 .allow_name_string = true, // so we can keep using the old testdata .tar.gz
23012328 .use_latest_commit = true,
23022329
23032330 .package_root = undefined,
23042331 .error_bundle = undefined,
23052332 .manifest = null,
23062333 .manifest_ast = undefined,
2307 .actual_hash = undefined,
2334 .computed_hash = undefined,
23082335 .has_build_zig = false,
23092336 .oom_flag = false,
23102337 .latest_commit = null,
src/Package/Manifest.zig+92-67
......@@ -5,15 +5,12 @@ 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;
12pub const max_name_len = 32;
13pub const max_version_len = 32;
1714
1815pub const Dependency = struct {
1916 location: Location,
......@@ -38,36 +35,8 @@ pub const ErrorMessage = struct {
3835 off: u32,
3936};
4037
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
7038name: []const u8,
39id: u32,
7140version: std.SemanticVersion,
7241version_node: Ast.Node.Index,
7342dependencies: std.StringArrayHashMapUnmanaged(Dependency),
......@@ -80,6 +49,10 @@ arena_state: std.heap.ArenaAllocator.State,
8049
8150pub const ParseOptions = struct {
8251 allow_missing_paths_field: bool = false,
52 /// Deprecated, to be removed after 0.14.0 is tagged.
53 allow_name_string: bool = true,
54 /// Deprecated, to be removed after 0.14.0 is tagged.
55 allow_missing_fingerprint: bool = true,
8356};
8457
8558pub const Error = Allocator.Error;
......@@ -100,12 +73,15 @@ pub fn parse(gpa: Allocator, ast: Ast, options: ParseOptions) Error!Manifest {
10073 .errors = .{},
10174
10275 .name = undefined,
76 .id = 0,
10377 .version = undefined,
10478 .version_node = 0,
10579 .dependencies = .{},
10680 .dependencies_node = 0,
10781 .paths = .{},
10882 .allow_missing_paths_field = options.allow_missing_paths_field,
83 .allow_name_string = options.allow_name_string,
84 .allow_missing_fingerprint = options.allow_missing_fingerprint,
10985 .minimum_zig_version = null,
11086 .buf = .{},
11187 };
......@@ -121,6 +97,7 @@ pub fn parse(gpa: Allocator, ast: Ast, options: ParseOptions) Error!Manifest {
12197
12298 return .{
12399 .name = p.name,
100 .id = p.id,
124101 .version = p.version,
125102 .version_node = p.version_node,
126103 .dependencies = try p.dependencies.clone(p.arena),
......@@ -164,22 +141,6 @@ pub fn copyErrorsIntoBundle(
164141 }
165142}
166143
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
183144const Parse = struct {
184145 gpa: Allocator,
185146 ast: Ast,
......@@ -188,12 +149,15 @@ const Parse = struct {
188149 errors: std.ArrayListUnmanaged(ErrorMessage),
189150
190151 name: []const u8,
152 id: u32,
191153 version: std.SemanticVersion,
192154 version_node: Ast.Node.Index,
193155 dependencies: std.StringArrayHashMapUnmanaged(Dependency),
194156 dependencies_node: Ast.Node.Index,
195157 paths: std.StringArrayHashMapUnmanaged(void),
196158 allow_missing_paths_field: bool,
159 allow_name_string: bool,
160 allow_missing_fingerprint: bool,
197161 minimum_zig_version: ?std.SemanticVersion,
198162
199163 const InnerError = error{ ParseFailure, OutOfMemory };
......@@ -211,6 +175,7 @@ const Parse = struct {
211175 var have_name = false;
212176 var have_version = false;
213177 var have_included_paths = false;
178 var fingerprint: ?Package.Fingerprint = null;
214179
215180 for (struct_init.ast.fields) |field_init| {
216181 const name_token = ast.firstToken(field_init) - 2;
......@@ -225,11 +190,16 @@ const Parse = struct {
225190 have_included_paths = true;
226191 try parseIncludedPaths(p, field_init);
227192 } else if (mem.eql(u8, field_name, "name")) {
228 p.name = try parseString(p, field_init);
193 p.name = try parseName(p, field_init);
229194 have_name = true;
195 } else if (mem.eql(u8, field_name, "fingerprint")) {
196 fingerprint = try parseFingerprint(p, field_init);
230197 } else if (mem.eql(u8, field_name, "version")) {
231198 p.version_node = field_init;
232199 const version_text = try parseString(p, field_init);
200 if (version_text.len > max_version_len) {
201 try appendError(p, main_tokens[field_init], "version string length {d} exceeds maximum of {d}", .{ version_text.len, max_version_len });
202 }
233203 p.version = std.SemanticVersion.parse(version_text) catch |err| v: {
234204 try appendError(p, main_tokens[field_init], "unable to parse semantic version: {s}", .{@errorName(err)});
235205 break :v undefined;
......@@ -249,6 +219,21 @@ const Parse = struct {
249219
250220 if (!have_name) {
251221 try appendError(p, main_token, "missing top-level 'name' field", .{});
222 } else {
223 if (fingerprint) |n| {
224 if (!n.validate(p.name)) {
225 return fail(p, main_token, "invalid fingerprint: 0x{x}; if this is a new or forked package, use this value: 0x{x}", .{
226 n.int(), Package.Fingerprint.generate(p.name).int(),
227 });
228 }
229 p.id = n.id;
230 } else if (!p.allow_missing_fingerprint) {
231 try appendError(p, main_token, "missing top-level 'fingerprint' field; suggested value: 0x{x}", .{
232 Package.Fingerprint.generate(p.name).int(),
233 });
234 } else {
235 p.id = 0;
236 }
252237 }
253238
254239 if (!have_version) {
......@@ -400,6 +385,59 @@ const Parse = struct {
400385 }
401386 }
402387
388 fn parseFingerprint(p: *Parse, node: Ast.Node.Index) !Package.Fingerprint {
389 const ast = p.ast;
390 const node_tags = ast.nodes.items(.tag);
391 const main_tokens = ast.nodes.items(.main_token);
392 const main_token = main_tokens[node];
393 if (node_tags[node] != .number_literal) {
394 return fail(p, main_token, "expected integer literal", .{});
395 }
396 const token_bytes = ast.tokenSlice(main_token);
397 const parsed = std.zig.parseNumberLiteral(token_bytes);
398 switch (parsed) {
399 .int => |n| return @bitCast(n),
400 .big_int, .float => return fail(p, main_token, "expected u64 integer literal, found {s}", .{
401 @tagName(parsed),
402 }),
403 .failure => |err| return fail(p, main_token, "bad integer literal: {s}", .{@tagName(err)}),
404 }
405 }
406
407 fn parseName(p: *Parse, node: Ast.Node.Index) ![]const u8 {
408 const ast = p.ast;
409 const node_tags = ast.nodes.items(.tag);
410 const main_tokens = ast.nodes.items(.main_token);
411 const main_token = main_tokens[node];
412
413 if (p.allow_name_string and node_tags[node] == .string_literal) {
414 const name = try parseString(p, node);
415 if (!std.zig.isValidId(name))
416 return fail(p, main_token, "name must be a valid bare zig identifier (hint: switch from string to enum literal)", .{});
417
418 if (name.len > max_name_len)
419 return fail(p, main_token, "name '{}' exceeds max length of {d}", .{
420 std.zig.fmtId(name), max_name_len,
421 });
422
423 return name;
424 }
425
426 if (node_tags[node] != .enum_literal)
427 return fail(p, main_token, "expected enum literal", .{});
428
429 const ident_name = ast.tokenSlice(main_token);
430 if (mem.startsWith(u8, ident_name, "@"))
431 return fail(p, main_token, "name must be a valid bare zig identifier", .{});
432
433 if (ident_name.len > max_name_len)
434 return fail(p, main_token, "name '{}' exceeds max length of {d}", .{
435 std.zig.fmtId(ident_name), max_name_len,
436 });
437
438 return ident_name;
439 }
440
403441 fn parseString(p: *Parse, node: Ast.Node.Index) ![]const u8 {
404442 const ast = p.ast;
405443 const node_tags = ast.nodes.items(.tag);
......@@ -421,21 +459,8 @@ const Parse = struct {
421459 const tok = main_tokens[node];
422460 const h = try parseString(p, node);
423461
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 });
462 if (h.len > Package.Hash.max_len) {
463 return fail(p, tok, "hash length exceeds maximum: {d}", .{h.len});
439464 }
440465
441466 return h;
src/main.zig+84-26
......@@ -4741,6 +4741,7 @@ fn cmdInit(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
47414741
47424742 const cwd_path = try process.getCwdAlloc(arena);
47434743 const cwd_basename = fs.path.basename(cwd_path);
4744 const sanitized_root_name = try sanitizeExampleName(arena, cwd_basename);
47444745
47454746 const s = fs.path.sep_str;
47464747 const template_paths = [_][]const u8{
......@@ -4751,8 +4752,10 @@ fn cmdInit(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
47514752 };
47524753 var ok_count: usize = 0;
47534754
4755 const fingerprint: Package.Fingerprint = .generate(sanitized_root_name);
4756
47544757 for (template_paths) |template_path| {
4755 if (templates.write(arena, fs.cwd(), cwd_basename, template_path)) |_| {
4758 if (templates.write(arena, fs.cwd(), sanitized_root_name, template_path, fingerprint)) |_| {
47564759 std.log.info("created {s}", .{template_path});
47574760 ok_count += 1;
47584761 } else |err| switch (err) {
......@@ -4769,6 +4772,37 @@ fn cmdInit(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
47694772 return cleanExit();
47704773}
47714774
4775fn sanitizeExampleName(arena: Allocator, bytes: []const u8) error{OutOfMemory}![]const u8 {
4776 var result: std.ArrayListUnmanaged(u8) = .empty;
4777 for (bytes, 0..) |byte, i| switch (byte) {
4778 '0'...'9' => {
4779 if (i == 0) try result.append(arena, '_');
4780 try result.append(arena, byte);
4781 },
4782 '_', 'a'...'z', 'A'...'Z' => try result.append(arena, byte),
4783 '-', '.', ' ' => try result.append(arena, '_'),
4784 else => continue,
4785 };
4786 if (result.items.len == 0) return "foo";
4787 if (result.items.len > Package.Manifest.max_name_len)
4788 result.shrinkRetainingCapacity(Package.Manifest.max_name_len);
4789
4790 return result.toOwnedSlice(arena);
4791}
4792
4793test sanitizeExampleName {
4794 var arena_instance = std.heap.ArenaAllocator.init(std.testing.allocator);
4795 defer arena_instance.deinit();
4796 const arena = arena_instance.allocator();
4797
4798 try std.testing.expectEqualStrings("foo_bar", try sanitizeExampleName(arena, "foo bar+"));
4799 try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, ""));
4800 try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, "!"));
4801 try std.testing.expectEqualStrings("a", try sanitizeExampleName(arena, "!a"));
4802 try std.testing.expectEqualStrings("a_b", try sanitizeExampleName(arena, "a.b!"));
4803 try std.testing.expectEqualStrings("_01234", try sanitizeExampleName(arena, "01234"));
4804}
4805
47724806fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
47734807 dev.check(.build_command);
47744808
......@@ -5191,13 +5225,15 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
51915225 .job_queue = &job_queue,
51925226 .omit_missing_hash_error = true,
51935227 .allow_missing_paths_field = false,
5228 .allow_missing_fingerprint = false,
5229 .allow_name_string = false,
51945230 .use_latest_commit = false,
51955231
51965232 .package_root = undefined,
51975233 .error_bundle = undefined,
51985234 .manifest = null,
51995235 .manifest_ast = undefined,
5200 .actual_hash = undefined,
5236 .computed_hash = undefined,
52015237 .has_build_zig = true,
52025238 .oom_flag = false,
52035239 .latest_commit = null,
......@@ -5244,13 +5280,14 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
52445280 const hashes = job_queue.table.keys();
52455281 const fetches = job_queue.table.values();
52465282 try deps_mod.deps.ensureUnusedCapacity(arena, @intCast(hashes.len));
5247 for (hashes, fetches) |hash, f| {
5283 for (hashes, fetches) |*hash, f| {
52485284 if (f == &fetch) {
52495285 // The first one is a dummy package for the current project.
52505286 continue;
52515287 }
52525288 if (!f.has_build_zig)
52535289 continue;
5290 const hash_slice = hash.toSlice();
52545291 const m = try Package.Module.create(arena, .{
52555292 .global_cache_directory = global_cache_directory,
52565293 .paths = .{
......@@ -5260,7 +5297,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
52605297 .fully_qualified_name = try std.fmt.allocPrint(
52615298 arena,
52625299 "root.@dependencies.{s}",
5263 .{&hash},
5300 .{hash_slice},
52645301 ),
52655302 .cc_argv = &.{},
52665303 .inherited = .{},
......@@ -5269,7 +5306,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
52695306 .builtin_mod = builtin_mod,
52705307 .builtin_modules = null, // `builtin_mod` is specified
52715308 });
5272 const hash_cloned = try arena.dupe(u8, &hash);
5309 const hash_cloned = try arena.dupe(u8, hash_slice);
52735310 deps_mod.deps.putAssumeCapacityNoClobber(hash_cloned, m);
52745311 f.module = m;
52755312 }
......@@ -5385,23 +5422,22 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
53855422 var any_errors = false;
53865423 while (it.next()) |hash| {
53875424 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,
5425 if (hash.len > Package.Hash.max_len) {
5426 std.log.err("invalid digest (length {d} exceeds maximum): '{s}'", .{
5427 hash.len, hash,
53925428 });
53935429 any_errors = true;
53945430 continue;
53955431 }
5396 try unlazy_set.put(arena, hash[0..digest_len].*, {});
5432 try unlazy_set.put(arena, .fromSlice(hash), {});
53975433 }
53985434 if (any_errors) process.exit(3);
53995435 if (system_pkg_dir_path) |p| {
54005436 // In this mode, the system needs to provide these packages; they
54015437 // cannot be fetched by Zig.
5402 for (unlazy_set.keys()) |hash| {
5438 for (unlazy_set.keys()) |*hash| {
54035439 std.log.err("lazy dependency package not found: {s}" ++ s ++ "{s}", .{
5404 p, hash,
5440 p, hash.toSlice(),
54055441 });
54065442 }
54075443 std.log.info("remote package fetching disabled due to --system mode", .{});
......@@ -7091,13 +7127,15 @@ fn cmdFetch(
70917127 .job_queue = &job_queue,
70927128 .omit_missing_hash_error = true,
70937129 .allow_missing_paths_field = false,
7130 .allow_missing_fingerprint = true,
7131 .allow_name_string = true,
70947132 .use_latest_commit = true,
70957133
70967134 .package_root = undefined,
70977135 .error_bundle = undefined,
70987136 .manifest = null,
70997137 .manifest_ast = undefined,
7100 .actual_hash = undefined,
7138 .computed_hash = undefined,
71017139 .has_build_zig = false,
71027140 .oom_flag = false,
71037141 .latest_commit = null,
......@@ -7117,14 +7155,15 @@ fn cmdFetch(
71177155 process.exit(1);
71187156 }
71197157
7120 const hex_digest = Package.Manifest.hexDigest(fetch.actual_hash);
7158 const package_hash = fetch.computedPackageHash();
7159 const package_hash_slice = package_hash.toSlice();
71217160
71227161 root_prog_node.end();
71237162 root_prog_node = .{ .index = .none };
71247163
71257164 const name = switch (save) {
71267165 .no => {
7127 try io.getStdOut().writeAll(hex_digest ++ "\n");
7166 try io.getStdOut().writer().print("{s}\n", .{package_hash_slice});
71287167 return cleanExit();
71297168 },
71307169 .yes, .exact => |name| name: {
......@@ -7145,7 +7184,7 @@ fn cmdFetch(
71457184 // The name to use in case the manifest file needs to be created now.
71467185 const init_root_name = fs.path.basename(build_root.directory.path orelse cwd_path);
71477186 var manifest, var ast = try loadManifest(gpa, arena, .{
7148 .root_name = init_root_name,
7187 .root_name = try sanitizeExampleName(arena, init_root_name),
71497188 .dir = build_root.directory.handle,
71507189 .color = color,
71517190 });
......@@ -7194,7 +7233,7 @@ fn cmdFetch(
71947233 \\ }}
71957234 , .{
71967235 std.zig.fmtEscapes(saved_path_or_url),
7197 std.zig.fmtEscapes(&hex_digest),
7236 std.zig.fmtEscapes(package_hash_slice),
71987237 });
71997238
72007239 const new_node_text = try std.fmt.allocPrint(arena, ".{p_} = {s},\n", .{
......@@ -7213,7 +7252,7 @@ fn cmdFetch(
72137252 if (dep.hash) |h| {
72147253 switch (dep.location) {
72157254 .url => |u| {
7216 if (mem.eql(u8, h, &hex_digest) and mem.eql(u8, u, saved_path_or_url)) {
7255 if (mem.eql(u8, h, package_hash_slice) and mem.eql(u8, u, saved_path_or_url)) {
72177256 std.log.info("existing dependency named '{s}' is up-to-date", .{name});
72187257 process.exit(0);
72197258 }
......@@ -7230,7 +7269,7 @@ fn cmdFetch(
72307269 const hash_replace = try std.fmt.allocPrint(
72317270 arena,
72327271 "\"{}\"",
7233 .{std.zig.fmtEscapes(&hex_digest)},
7272 .{std.zig.fmtEscapes(package_hash_slice)},
72347273 );
72357274
72367275 warn("overwriting existing dependency named '{s}'", .{name});
......@@ -7429,10 +7468,10 @@ fn loadManifest(
74297468 0,
74307469 ) catch |err| switch (err) {
74317470 error.FileNotFound => {
7471 const fingerprint: Package.Fingerprint = .generate(options.root_name);
74327472 var templates = findTemplates(gpa, arena);
74337473 defer templates.deinit();
7434
7435 templates.write(arena, options.dir, options.root_name, Package.Manifest.basename) catch |e| {
7474 templates.write(arena, options.dir, options.root_name, Package.Manifest.basename, fingerprint) catch |e| {
74367475 fatal("unable to write {s}: {s}", .{
74377476 Package.Manifest.basename, @errorName(e),
74387477 });
......@@ -7490,6 +7529,7 @@ const Templates = struct {
74907529 out_dir: fs.Dir,
74917530 root_name: []const u8,
74927531 template_path: []const u8,
7532 fingerprint: Package.Fingerprint,
74937533 ) !void {
74947534 if (fs.path.dirname(template_path)) |dirname| {
74957535 out_dir.makePath(dirname) catch |err| {
......@@ -7503,12 +7543,30 @@ const Templates = struct {
75037543 };
75047544 templates.buffer.clearRetainingCapacity();
75057545 try templates.buffer.ensureUnusedCapacity(contents.len);
7506 for (contents) |c| {
7507 if (c == '$') {
7508 try templates.buffer.appendSlice(root_name);
7509 } else {
7510 try templates.buffer.append(c);
7546 var i: usize = 0;
7547 while (i < contents.len) {
7548 if (contents[i] == '.') {
7549 if (std.mem.startsWith(u8, contents[i..], ".LITNAME")) {
7550 try templates.buffer.append('.');
7551 try templates.buffer.appendSlice(root_name);
7552 i += ".LITNAME".len;
7553 continue;
7554 } else if (std.mem.startsWith(u8, contents[i..], ".NAME")) {
7555 try templates.buffer.appendSlice(root_name);
7556 i += ".NAME".len;
7557 continue;
7558 } else if (std.mem.startsWith(u8, contents[i..], ".FINGERPRINT")) {
7559 try templates.buffer.writer().print("0x{x}", .{fingerprint.int()});
7560 i += ".FINGERPRINT".len;
7561 continue;
7562 } else if (std.mem.startsWith(u8, contents[i..], ".ZIGVER")) {
7563 try templates.buffer.appendSlice(build_options.version);
7564 i += ".ZIGVER".len;
7565 continue;
7566 }
75117567 }
7568 try templates.buffer.append(contents[i]);
7569 i += 1;
75127570 }
75137571
75147572 return out_dir.writeFile(.{