| 1 | const std = @import("std"); |
| 2 | const assert = std.debug.assert; |
| 3 | |
| 4 | pub const Fetch = @import("Fetch.zig"); |
| 5 | pub const Manifest = @import("Package/Manifest.zig"); |
| 6 | |
| 7 | pub 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. |
| 42 | pub 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. |
| 172 | pub 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 | |
| 196 | test 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 | |
| 205 | test { |
| 206 | _ = Fetch; |
| 207 | } |