nx_swhid_lib.nx source
↩ module page · 126 lines · 5745 B
1// nx_swhid_lib.nx -- SWHID, the SoftWare Hash IDentifier. ISO/IEC 18670:2025 (spec V1.2).
2//
3// WHY THE LIBRARIAN NEEDS THIS AND OUR OWN CID CANNOT SUBSTITUTE.
4// Our nxc1- identifier is a plain sha256 of the bytes: excellent, and MEANINGLESS TO ANYONE ELSE.
5// A SWHID is computed the same way by everyone, from the bytes alone, with NO central registry and NO
6// hosting forge involved -- so a third party can independently confirm that the file we hold is the
7// same object Software Heritage holds, without trusting us OR them. That is the anti-rot property the
8// whole library exists for: the pointer can die and the identity survives.
9// It is also the interop currency: SPDX 2.3 Annex F accepts `swh` as a Persistent-Id external
10// repository identifier, and DataCite Metadata Schema 4.7 (2026-03-03) added SWHID as a
11// relatedIdentifierType. We emit one identifier and two ecosystems can read it.
12//
13// THE CONSTRUCTION, and it is deliberately git's:
14// swh:1:cnt:<sha1 hex of ( "blob " + decimal(length) + NUL + the file bytes )>
15// `cnt` hashes ONLY the file bytes -- no filename, no path, no mtime, no metadata -- which is exactly
16// why it is stable across forges, renames and re-hosting.
17//
18// ⚠SHA-1 IS THE STANDARD'S CHOICE, NOT OURS, AND IT IS NOT OUR INTEGRITY GUARANTEE.
19// SHAttered (2017) produced chosen-prefix collisions, so a SWHID is an INTEROPERABLE NAME, never a
20// tamper-evidence claim. Our integrity story stays sha256 + byte length (aq_fixity). Carrying both is
21// the point: the SWHID is what the world can check, the sha256+length is what we can trust. Emitting
22// only the SWHID would be a downgrade wearing a standard's number.
23//
24// ⚠NAMED BOUND, NOT HIDDEN: the git object hash requires the header PREPENDED to the content, and the
25// estate's sha1 has no streaming/update API -- it takes one contiguous buffer. So this materialises
26// header||data and its memory cost is O(n). That is fine for a licence file or a source tarball and it
27// is NOT fine for a multi-gigabyte model weight. A streaming variant needs a streaming SHA-1 the
28// estate does not have; that is a real gap and it is stated here rather than discovered later.
29// license_tier: ORIGINAL
30import "nx_syscalls.nx"
31import "nx_sha1.nx"
32
33const SW_DIGEST_BYTES: i64 = 20 // SHA-1 is 160 bits
34const SW_HEXLEN: i64 = 40 // DERIVED shape: 2 hex chars per digest byte
35const SW_PREFIX_LEN: i64 = 10 // "swh:1:cnt:"
36const SW_IDLEN: i64 = 50 // DERIVED: SW_PREFIX_LEN + SW_HEXLEN
37// "blob " (5) + at most 19 decimal digits of an i64 length + the NUL. Sized from the TYPE, not guessed.
38const SW_HDR_MAX: i64 = 32
39const SW_B10: i64 = 10
40const SW_ZERO: i64 = 48
41const SW_NINE: i64 = 57
42const SW_LC_A: i64 = 97
43const SW_LC_F: i64 = 102
44
45func sw_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
46
47func sw_cat(d: *u8, at: i64, s: *u8) -> i64 {
48 var p: i64 = at
49 var i: i64 = 0
50 while s[i] != (0 as u8) { d[p] = s[i]; p = p + 1; i = i + 1 }
51 return p
52}
53
54// lowercase hex, which is what the specification's syntax requires.
55func sw_hex(inp: *u8, n: i64, out: *u8, at: i64) -> i64 {
56 let hx: *u8 = "0123456789abcdef" as *u8
57 var i: i64 = 0
58 var p: i64 = at
59 while i < n {
60 let b: i64 = inp[i] as i64
61 out[p] = hx[(b / 16) % 16]
62 out[p + 1] = hx[b % 16]
63 p = p + 2
64 i = i + 1
65 }
66 return p
67}
68
69// decimal, no padding, no sign. Zero is "0" -- the one case a digit loop drops.
70func sw_decimal(v: i64, d: *u8, at: i64) -> i64 {
71 if v == 0 { d[at] = SW_ZERO as u8; return at + 1 }
72 let t: *u8 = sys_mmap(SW_HDR_MAX)
73 var m: i64 = v
74 var k: i64 = 0
75 while m > 0 { t[k] = (SW_ZERO + (m % SW_B10)) as u8; m = m / SW_B10; k = k + 1 }
76 var p: i64 = at
77 var i: i64 = 0
78 while i < k { d[p] = t[k - 1 - i]; p = p + 1; i = i + 1 }
79 sys_munmap(t, SW_HDR_MAX)
80 return p
81}
82
83// THE IDENTIFIER. Writes "swh:1:cnt:<40 lowercase hex>" plus NUL into out (needs SW_IDLEN + 1 bytes).
84// Returns the length written, or -1 if the length is negative or the buffer could not be obtained.
85func swhid_cnt(data: *u8, n: i64, out: *u8) -> i64 {
86 if n < 0 { out[0] = 0 as u8; return 0 - 1 }
87 let cap: i64 = SW_HDR_MAX + n
88 let buf: *u8 = sys_mmap(cap)
89 if (buf as i64) == 0 { out[0] = 0 as u8; return 0 - 1 }
90 var o: i64 = sw_cat(buf, 0, "blob " as *u8)
91 o = sw_decimal(n, buf, o)
92 buf[o] = 0 as u8
93 o = o + 1
94 var i: i64 = 0
95 while i < n { buf[o + i] = data[i]; i = i + 1 }
96 let total: i64 = o + n
97 let dg: *u8 = sys_mmap(SW_DIGEST_BYTES)
98 sha1(buf, total, dg)
99 var p: i64 = sw_cat(out, 0, "swh:1:cnt:" as *u8)
100 p = sw_hex(dg, SW_DIGEST_BYTES, out, p)
101 out[p] = 0 as u8
102 sys_munmap(buf, cap)
103 sys_munmap(dg, SW_DIGEST_BYTES)
104 return p
105}
106
107// WELL-FORMEDNESS, checked by SHAPE rather than trusted by origin. A SWHID arriving from anywhere --
108// a .refs row, an upstream page, another estate -- is a STRING until this passes. The hex length is
109// derived from the digest size, and uppercase is REFUSED because the specification's syntax is
110// lowercase and a case-insensitive reader is how two identifiers for one object get created.
111func swhid_is_wellformed(s: *u8) -> i64 {
112 if sw_len(s) != SW_IDLEN { return 0 }
113 let pfx: *u8 = "swh:1:cnt:" as *u8
114 var i: i64 = 0
115 while i < SW_PREFIX_LEN { if s[i] != pfx[i] { return 0 } i = i + 1 }
116 var j: i64 = SW_PREFIX_LEN
117 while j < SW_IDLEN {
118 let c: i64 = s[j] as i64
119 var ok: i64 = 0
120 if c >= SW_ZERO { if c <= SW_NINE { ok = 1 } }
121 if c >= SW_LC_A { if c <= SW_LC_F { ok = 1 } }
122 if ok == 0 { return 0 }
123 j = j + 1
124 }
125 return 1
126}