1const std = @import("std");
2const assert = std.debug.assert;
3
4pub const Fetch = @import("Fetch.zig");
5pub const Manifest = @import("Package/Manifest.zig");
6
7pub const Fingerprint = packed struct(u64) {
8 id: u32,
9 checksum: u32,
10
11 pub fn generate(rng: std.Random, name: []const u8) Fingerprint {
12 return .{
13 .id = rng.intRangeLessThan(u32, 1, 0xffffffff),
14 .checksum = std.hash.Crc32.hash(name),
15 };
16 }
17
18 pub fn validate(n: Fingerprint, name: []const u8) bool {
19 switch (n.id) {
20 0x00000000, 0xffffffff => return false,
21 else => return std.hash.Crc32.hash(name) == n.checksum,
22 }
23 }
24
25 pub fn int(n: Fingerprint) u64 {
26 return @bitCast(n);
27 }
28};
29
30/// A user-readable, file system safe hash that identifies an exact package
31/// snapshot, including file contents.
32///
33/// The hash is not only to prevent collisions but must resist attacks where
34/// the adversary fully controls the contents being hashed. Thus, it contains
35/// a full SHA-256 digest.
36///
37/// This data structure can be used to store the legacy hash format too. Legacy
38/// hash format is scheduled to be removed after 0.14.0 is tagged.
39///
40/// There's also a third way this structure is used. When using path rather than
41/// hash, a unique hash is still needed, so one is computed based on the path.
42pub const Hash = struct {
43 /// Maximum size of a package hash. Unused bytes at the end are
44 /// filled with zeroes.
45 ///
46 /// Assumed to be already validated.
47 bytes: [max_len]u8,
48
49 pub const Algo = std.crypto.hash.sha2.Sha256;
50 pub const Digest = [Algo.digest_length]u8;
51
52 /// Example: "nnnn-vvvv-hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh"
53 pub const max_len = 32 + 1 + 32 + 1 + (32 + 32 + 200) / 6;
54
55 /// Asserts `s` is valid.
56 pub fn fromSlice(s: []const u8) Hash {
57 assert(validate(s) == .ok);
58 var result: Hash = undefined;
59 @memcpy(result.bytes[0..s.len], s);
60 @memset(result.bytes[s.len..], 0);
61 return result;
62 }
63
64 pub const Validation = enum { ok, short, long, incomplete };
65
66 pub fn validate(s: []const u8) Validation {
67 if (s.len > max_len) return .long;
68 if (s.len < 44) return .short;
69 const n_dashes = std.mem.countScalar(u8, s[0 .. s.len - 44], '-');
70 if (n_dashes < 2) return .incomplete;
71 return .ok;
72 }
73
74 test validate {
75 try std.testing.expectEqual(.short, validate(""));
76 }
77
78 pub fn toSlice(ph: *const Hash) []const u8 {
79 var end: usize = ph.bytes.len;
80 while (true) {
81 end -= 1;
82 if (ph.bytes[end] != 0) return ph.bytes[0 .. end + 1];
83 }
84 }
85
86 pub fn eql(a: *const Hash, b: *const Hash) bool {
87 return std.mem.eql(u8, &a.bytes, &b.bytes);
88 }
89
90 /// Produces "$name-$semver-$hashplus".
91 /// * name is the name field from build.zig.zon, asserted to be at most 32
92 /// bytes and assumed be a valid zig identifier
93 /// * semver is the version field from build.zig.zon, asserted to be at
94 /// most 32 bytes
95 /// * hashplus is the following 33-byte array, base64 encoded using -_ to make
96 /// it filesystem safe:
97 /// - (4 bytes) LE u32 Package ID
98 /// - (4 bytes) LE u32 total decompressed size in bytes, overflow saturated
99 /// - (25 bytes) truncated SHA-256 digest of hashed files of the package
100 pub fn init(digest: Digest, name: []const u8, ver: []const u8, id: u32, size: u32) Hash {
101 assert(name.len <= 32);
102 assert(ver.len <= 32);
103 var result: Hash = undefined;
104 var buf: std.ArrayList(u8) = .initBuffer(&result.bytes);
105 buf.appendSliceAssumeCapacity(name);
106 buf.appendAssumeCapacity('-');
107 buf.appendSliceAssumeCapacity(ver);
108 buf.appendAssumeCapacity('-');
109 var hashplus: [33]u8 = undefined;
110 std.mem.writeInt(u32, hashplus[0..4], id, .little);
111 std.mem.writeInt(u32, hashplus[4..8], size, .little);
112 hashplus[8..].* = digest[0..25].*;
113 _ = std.base64.url_safe_no_pad.Encoder.encode(buf.addManyAsArrayAssumeCapacity(44), &hashplus);
114 @memset(buf.unusedCapacitySlice(), 0);
115 return result;
116 }
117
118 /// Produces a unique hash based on the path provided. The result should
119 /// not be user-visible.
120 pub fn initPath(sub_path: []const u8, is_global: bool) Hash {
121 var result: Hash = .{ .bytes = @splat(0) };
122 var i: usize = 0;
123 if (is_global) {
124 result.bytes[0] = '/';
125 i += 1;
126 }
127 if (i + sub_path.len <= result.bytes.len) {
128 @memcpy(result.bytes[i..][0..sub_path.len], sub_path);
129 return result;
130 }
131 var bin_digest: [Algo.digest_length]u8 = undefined;
132 Algo.hash(sub_path, &bin_digest, .{});
133 _ = std.mem.print(result.bytes[i..], "{x}", .{&bin_digest}) catch unreachable;
134 return result;
135 }
136
137 pub fn projectId(hash: *const Hash) ProjectId {
138 const bytes = hash.toSlice();
139 const name = std.mem.sliceTo(bytes, '-');
140 const encoded_hashplus = bytes[bytes.len - 44 ..];
141 var hashplus: [33]u8 = undefined;
142 std.base64.url_safe_no_pad.Decoder.decode(&hashplus, encoded_hashplus) catch unreachable;
143 const fingerprint_id = std.mem.readInt(u32, hashplus[0..4], .little);
144 return .init(name, fingerprint_id);
145 }
146
147 test projectId {
148 const hash: Hash = .fromSlice("pulseaudio-16.1.1-9-mk_62MZkNwBaFwiZ7ZVrYRIf_3dTqqJR5PbMRCJzSuLw");
149 const project_id = hash.projectId();
150
151 var expected_name: [32]u8 = @splat(0);
152 expected_name[0.."pulseaudio".len].* = "pulseaudio".*;
153 try std.testing.expectEqualSlices(u8, &expected_name, &project_id.padded_name);
154
155 try std.testing.expectEqual(0xd8fa4f9a, project_id.fingerprint_id);
156 }
157
158 test "projectId with dashes in the base64" {
159 const hash: Hash = .fromSlice("dvui-0.4.0-dev-AQFJmayi2gAKE7FeJoF61v5U1IV9-SupoEcFutIZYpkC");
160 const project_id = hash.projectId();
161
162 var expected_name: [32]u8 = @splat(0);
163 expected_name[0.."dvui".len].* = "dvui".*;
164 try std.testing.expectEqualSlices(u8, &expected_name, &project_id.padded_name);
165
166 try std.testing.expectEqual(0x99490101, project_id.fingerprint_id);
167 }
168};
169
170/// Minimum information required to identify whether a package is an artifact
171/// of a given project.
172pub const ProjectId = struct {
173 /// Bytes after name.len are set to zero.
174 padded_name: [32]u8,
175 fingerprint_id: u32,
176
177 pub fn init(name: []const u8, fingerprint_id: u32) ProjectId {
178 var padded_name: [32]u8 = @splat(0);
179 @memcpy(padded_name[0..name.len], name);
180 return .{
181 .padded_name = padded_name,
182 .fingerprint_id = fingerprint_id,
183 };
184 }
185
186 pub fn eql(a: *const ProjectId, b: *const ProjectId) bool {
187 return a.fingerprint_id == b.fingerprint_id and std.mem.eql(u8, &a.padded_name, &b.padded_name);
188 }
189
190 pub fn hash(a: *const ProjectId) u64 {
191 const x: u64 = @bitCast(a.padded_name[0..8].*);
192 return std.hash.int(x | a.fingerprint_id);
193 }
194};
195
196test Hash {
197 const example_digest: Hash.Digest = .{
198 0xc7, 0xf5, 0x71, 0xb7, 0xb4, 0xe7, 0x6f, 0x3c, 0xdb, 0x87, 0x7a, 0x7f, 0xdd, 0xf9, 0x77, 0x87,
199 0x9d, 0xd3, 0x86, 0xfa, 0x73, 0x57, 0x9a, 0xf7, 0x9d, 0x1e, 0xdb, 0x8f, 0x3a, 0xd9, 0xbd, 0x9f,
200 };
201 const result: Hash = .init(example_digest, "nasm", "2.16.1-3", 0xcafebabe, 10 * 1024 * 1024);
202 try std.testing.expectEqualStrings("nasm-2.16.1-3-vrr-ygAAoADH9XG3tOdvPNuHen_d-XeHndOG-nNXmved", result.toSlice());
203}
204
205test {
206 _ = Fetch;
207}