code wiki / (root) / nx_swhid_lib.nx

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}