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}