code wiki / _hdl_build / nx_filehash.nx

nx_filehash.nx source

↩ module page · 185 lines · 8955 B

1// nx_filehash.nx -- SHA-256 OF A FILE, AND THE ONE-CALL "ARE THESE TWO ARTIFACTS THE SAME BYTES" TEST. 2// 3// ★WHY THIS EXISTS, and it is a gap the ecosystem's own laws made expensive. This codebase carries a 4// standing rule -- VERIFY THE INSTRUMENT AND THE ARTIFACT -- and a banked correction where a session 5// claimed two meshes were "byte-identical", was wrong, and recorded the reason: "I inferred byte-identity 6// from a matching tri count and matching bench scores and NEVER HASHED THE FILES." The reason nobody 7// hashed the files is that there was no way to: nx_sha256 exists as a LIBRARY with a dozen gates and 8// wasm builds, and `sha256_digest(bytes,n,out)` has been sitting in it the whole time, but NO ORGAN 9// exposed it over a PATH. So every "byte-identical" claim in this program has been rhetorical. 10// ★This is the adoption gap in its purest form: the primitive was built, gated and never wired to the 11// one caller shape that every other lane needs. It is 90 lines. 12// 13// nx_filehash <path> -> {"sha256":"<64 hex>","bytes":N} 14// nx_filehash cmp <a> <b> -> {"identical":0|1, both digests, both sizes} 15// nx_filehash selftest -> KAT + non-vacuity teeth 16// 17// license_tier: ORIGINAL expect_exit: 0 No hw writes (Rule 26). 18import "nx_syscalls.nx" 19import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc) 20import "nx_sha256.nx" 21 22const FH_DIGEST_BYTES: i64 = 32 23const FH_LENSLOT: i64 = 16 24 25func fh_puts(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 26// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 27// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the 28// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls). 29// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign. 30func fh_pn(v: i64) -> i64 { nxi_out(v); return 0 } 31func fh_streq(a: *u8, b: *u8) -> i64 { 32 var i: i64=0; var go: i64=1; var eq: i64=1 33 while go==1 { if a[i]!=b[i] { eq=0; go=0 } else { if a[i]==(0 as u8) { go=0 } else { i=i+1 } } } 34 return eq 35} 36// lowercase hex, emitted straight from the digest bytes 37func fh_hex(d: *u8) -> i64 { 38 let out: *u8 = sys_mmap(FH_DIGEST_BYTES*2 + 8) 39 var i: i64 = 0 40 while i < FH_DIGEST_BYTES { 41 let v: i64 = d[i] as i64 42 let hi: i64 = (v >> 4) & 15 43 let lo: i64 = v & 15 44 if hi < 10 { out[i*2] = (48+hi) as u8 } else { out[i*2] = (87+hi) as u8 } 45 if lo < 10 { out[i*2+1] = (48+lo) as u8 } else { out[i*2+1] = (87+lo) as u8 } 46 i = i + 1 47 } 48 sys_write(1, out, FH_DIGEST_BYTES*2) 49 return 0 50} 51// ★RETURNS THE LENGTH, and -1 when the file cannot be read. A hasher that silently digests an empty 52// buffer would report the sha256 of nothing for a missing file -- a real, valid-looking 64-hex answer 53// for a file that does not exist, which is exactly the shape of a lying instrument. Fail loud instead. 54func fh_hash_file(path: *u8, out: *u8) -> i64 { 55 let ln: *i64 = sys_mmap(FH_LENSLOT) as *i64 56 let buf: *u8 = sys_read_file(path, ln) 57 if (buf as i64) == 0 { return 0-1 } 58 let n: i64 = ln[0] 59 sha256_digest(buf, n, out) 60 return n 61} 62 63func fh_one(path: *u8) -> i64 { 64 let d: *u8 = sys_mmap(FH_DIGEST_BYTES + 8) 65 let n: i64 = fh_hash_file(path, d) 66 if n < 0 { fh_puts("{\x22error\x22:\x22cannot read file\x22}\n" as *u8); return 3 } 67 fh_puts("{\x22organ\x22:\x22nx_filehash\x22,\x22sha256\x22:\x22" as *u8); fh_hex(d) 68 fh_puts("\x22,\x22bytes\x22:" as *u8); fh_pn(n) 69 fh_puts("}\n" as *u8) 70 return 0 71} 72 73// ★THE VERB THE LAWS ACTUALLY NEED. Two paths in, one boolean out, both digests shown so the answer is 74// auditable rather than trusted. Sizes travel too: a size mismatch alone already proves difference, and 75// printing it means a reader can tell a same-size-different-content case (the one a CAS `expect=<size>` 76// guard cannot catch -- banked) from a plain length change. 77func fh_cmp(pa: *u8, pb: *u8) -> i64 { 78 let da: *u8 = sys_mmap(FH_DIGEST_BYTES + 8) 79 let db: *u8 = sys_mmap(FH_DIGEST_BYTES + 8) 80 let na: i64 = fh_hash_file(pa, da) 81 if na < 0 { fh_puts("{\x22error\x22:\x22cannot read file a\x22}\n" as *u8); return 3 } 82 let nb: i64 = fh_hash_file(pb, db) 83 if nb < 0 { fh_puts("{\x22error\x22:\x22cannot read file b\x22}\n" as *u8); return 4 } 84 var same: i64 = 1 85 var i: i64 = 0 86 while i < FH_DIGEST_BYTES { if da[i] != db[i] { same = 0 } i = i + 1 } 87 fh_puts("{\x22organ\x22:\x22nx_filehash\x22,\x22verb\x22:\x22cmp\x22,\x22identical\x22:" as *u8); fh_pn(same) 88 fh_puts(",\x22a_sha256\x22:\x22" as *u8); fh_hex(da) 89 fh_puts("\x22,\x22a_bytes\x22:" as *u8); fh_pn(na) 90 fh_puts(",\x22b_sha256\x22:\x22" as *u8); fh_hex(db) 91 fh_puts("\x22,\x22b_bytes\x22:" as *u8); fh_pn(nb) 92 fh_puts("}\n" as *u8) 93 return 0 94} 95 96// ★KAT AGAINST AN ABSOLUTE, NOT AGAINST OURSELVES. The digest of "abc" is a published FIPS-180-4 test 97// vector; checking our output against it is the only tooth here that can fail if the whole hash is 98// wrong. A self-consistency check (hash the same file twice, compare) passes for a function that 99// returns a constant, which is precisely the vacuity this lane keeps convicting. 100const FH_KAT: *u8 = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" as *u8 101func fh_hex_into(d: *u8, out: *u8) -> i64 { 102 var i: i64 = 0 103 while i < FH_DIGEST_BYTES { 104 let v: i64 = d[i] as i64 105 let hi: i64 = (v >> 4) & 15 106 let lo: i64 = v & 15 107 if hi < 10 { out[i*2] = (48+hi) as u8 } else { out[i*2] = (87+hi) as u8 } 108 if lo < 10 { out[i*2+1] = (48+lo) as u8 } else { out[i*2+1] = (87+lo) as u8 } 109 i = i + 1 110 } 111 out[FH_DIGEST_BYTES*2] = 0 as u8 112 return 0 113} 114func fh_selftest() -> i64 { 115 var pass: i64 = 0 116 var total: i64 = 0 117 let d: *u8 = sys_mmap(FH_DIGEST_BYTES + 8) 118 let hx: *u8 = sys_mmap(FH_DIGEST_BYTES*2 + 8) 119 120 // T1 -- KAT: sha256("abc") must equal the published FIPS-180-4 vector. 121 total = total + 1 122 sha256_digest("abc" as *u8, 3, d) 123 fh_hex_into(d, hx) 124 var t1: i64 = fh_streq(hx, FH_KAT) 125 if t1 == 1 { pass = pass + 1 } 126 fh_puts("T1 kat_abc=" as *u8); fh_pn(t1); fh_puts("\n" as *u8) 127 128 // T2 -- NON-VACUITY: a DIFFERENT message must NOT produce the KAT digest. Without this, a digest 129 // function hard-wired to return the vector passes T1. 130 total = total + 1 131 sha256_digest("abd" as *u8, 3, d) 132 fh_hex_into(d, hx) 133 var t2: i64 = 0 134 if fh_streq(hx, FH_KAT) == 0 { t2 = 1 } 135 if t2 == 1 { pass = pass + 1 } 136 fh_puts("T2 differs_on_different_input=" as *u8); fh_pn(t2); fh_puts("\n" as *u8) 137 138 // T3 -- DETERMINISM: the same bytes hash the same twice. Cheap, and it is the property every 139 // "reproduce the baseline" claim rests on. 140 total = total + 1 141 let d2: *u8 = sys_mmap(FH_DIGEST_BYTES + 8) 142 sha256_digest("abc" as *u8, 3, d) 143 sha256_digest("abc" as *u8, 3, d2) 144 var t3: i64 = 1 145 var i: i64 = 0 146 while i < FH_DIGEST_BYTES { if d[i] != d2[i] { t3 = 0 } i = i + 1 } 147 if t3 == 1 { pass = pass + 1 } 148 fh_puts("T3 deterministic=" as *u8); fh_pn(t3); fh_puts("\n" as *u8) 149 150 // T4 -- LENGTH SENSITIVITY: a PREFIX of a message must not collide with it. This is the tooth that 151 // catches a hasher ignoring its n argument, which would make every truncated artifact look intact. 152 total = total + 1 153 sha256_digest("abc" as *u8, 2, d) 154 fh_hex_into(d, hx) 155 var t4: i64 = 0 156 if fh_streq(hx, FH_KAT) == 0 { t4 = 1 } 157 if t4 == 1 { pass = pass + 1 } 158 fh_puts("T4 length_sensitive=" as *u8); fh_pn(t4); fh_puts("\n" as *u8) 159 160 // T5 -- MISSING FILE REFUSES. A hasher that returns the digest of an empty buffer for an absent 161 // path emits a valid-looking answer about a file that does not exist. 162 total = total + 1 163 var t5: i64 = 0 164 if fh_hash_file("nx_filehash_no_such_file_zzz" as *u8, d) < 0 { t5 = 1 } 165 if t5 == 1 { pass = pass + 1 } 166 fh_puts("T5 missing_file_refuses=" as *u8); fh_pn(t5); fh_puts("\n" as *u8) 167 168 fh_puts("{\x22organ\x22:\x22nx_filehash\x22,\x22verb\x22:\x22selftest\x22,\x22pass\x22:" as *u8); fh_pn(pass) 169 fh_puts(",\x22total\x22:" as *u8); fh_pn(total) 170 fh_puts(",\x22verdict\x22:\x22" as *u8) 171 if pass == total { fh_puts("GREEN" as *u8) } else { fh_puts("RED" as *u8) } 172 fh_puts("\x22}\n" as *u8) 173 if pass == total { return 0 } 174 return 1 175} 176 177func main(argc: i64, argv: *i64) -> i64 { 178 if argc < 2 { fh_puts("usage: nx_filehash <path> | cmp <a> <b> | selftest\n" as *u8); return 2 } 179 if fh_streq(argv[1] as *u8, "selftest" as *u8) == 1 { return fh_selftest() } 180 if fh_streq(argv[1] as *u8, "cmp" as *u8) == 1 { 181 if argc < 4 { fh_puts("usage: nx_filehash cmp <a> <b>\n" as *u8); return 2 } 182 return fh_cmp(argv[2] as *u8, argv[3] as *u8) 183 } 184 return fh_one(argv[1] as *u8) 185}