nx_treehash.nx
buildroot/runtime/nx_treehash.nx
about
nx_treehash.nx -- CROSS-TREE CONTENT-IDENTITY MANIFEST. The hash half of nx_treediff.
THE GAP IT CLOSES (measured 2026-08-06, tree-canon-single-writer lane):
nx_treediff emits `<bytes> <relpath>` and nx_treediverge compares those SIZES. Both organs say
so honestly in their own output -- treediff prints "size is a SCREEN, not proof of identity" and
treediverge's usage line says "(by BYTES, a FLOOR)". They were right, and they were built that
way for a stated reason: "it needs no hash primitive on either side."
THAT REASON EXPIRED. nx_sha256.nx has shipped the whole time and nx_filehash wired it to a PATH.
Meanwhile the hourly treediverge beat reported divergent=5943 for days while a content-level
census of the same two trees reported 17,164 of 17,579. Both numbers were honest measurements of
DIFFERENT THINGS: 5,943 files differ in LENGTH; the rest differ in BYTES AT EQUAL LENGTH, which a
size screen cannot see by construction.
=> A SCREEN THAT DOCUMENTS ITS OWN FLOOR STILL GETS READ AS A TOTAL ONCE IT IS WIRED TO A BEAT.
The trend log made a floor look like a burn-down. This organ removes the excuse: content identity
is now as cheap to measure as length, so nothing has to infer identity from size again.
DESIGN: same walker, same caps, same in-band envelope as nx_treediff -- deliberately, so the two
manifests are directly comparable and this organ inherits a proven traversal rather than a new one.
The row is a STRICT SUPERSET of treediff's: `<sha256-64hex> <bytes> <relpath>`, so a consumer can
still do the cheap size compare AND the exact content compare from one file.
*UNREADABLE IS NEVER A HASH. nx_filehash banked the law: a hasher that digests an empty buffer for
a missing file emits a real-looking 64-hex answer for a file that does not exist. Here an unreadable
file emits 64 '-' (fixed width, cannot collide with hex, never equal to itself) and is COUNTED in
the envelope, so a permission error can never masquerade as convergence.
*MEMORY IS THE REAL RISK AND IT IS FREED EXPLICITLY. Hashing means READING EVERY FILE'S BYTES --
the exact shape of the leak class this estate has been bitten by repeatedly (the unfreed per-call
mmap that took 28.5GB of a 36GB host, and nx_treediff's own per-entry path buffers). Every mmap
here is matched by a munmap. THE SUBTLE ONE: sys_read_file reserves `filesize+16`, but for a file
whose lseek(END) is 0 it falls back to a 4 GiB reservation -- so a zero-byte .nx freed as "0+16"
would leak 4 GiB of address space per occurrence and hit RLIMIT_AS. That case is freed at its true
reserved size.
DIALECT: plain-if, no empty literals, <=6 params, consts above use.
license_tier: ORIGINAL expect_exit: 0 No hw writes (Rule 26).
dependencies 2 imports · 0 importers
imports: nx_syscalls.nxnx_sha256.nx
imported by: nobody (leaf or entry point)
call flow from main pre-order; caps 40 nodes / depth 6 declared; ↻ = already shown
structs
| none |
consts
| 40 | const TH_MAX_FILES: i64 = 40000 // upper bound on emitted rows; declared, never silent |
| 41 | const TH_MAX_DEPTH: i64 = 8 // recursion depth cap |
| 42 | const TH_PATH_CAP: i64 = 1024 |
| 43 | const TH_DIRENT_BUF: i64 = 65536 |
| 44 | const TH_OUTBUF: i64 = 8388608 // 8 MiB (~20k rows x ~130B, with headroom) |
| 45 | const TH_DIGEST_BYTES: i64 = 32 |
| 46 | const TH_HEX_CHARS: i64 = 64 |
| 47 | const TH_NUMBUF: i64 = 64 |
| 49 | const TH_EMPTY_RESERVE: i64 = 4294967296 |
functions
| 59 | func th_puts(s: *u8) -> i64 |
| 65 | func th_putn(v: i64) -> i64 |
| 76 | func th_cat(d: *u8, o: i64, s: *u8) -> i64 called by 1: th_walk |
| 93 | func th_ends_ext(nm: *u8) -> i64 called by 1: th_walk |
| 107 | func th_digest_hex(d: *u8) -> i64 called by 1: th_walk |
| 121 | func th_hex_unreadable() -> i64 called by 1: th_walk |
| 128 | func th_hash_file(path: *u8, out: *u8) -> i64 |
| 143 | func th_row(hx: *u8, sz: i64, rel: *u8) -> i64 called by 1: th_walk |
| 169 | func th_walk(dir: *u8, rel: *u8, depth: i64) -> i64 called by 2: th_walkmain calls 15: sys_openat_rdsys_mmapsys_getdents64dirent_reclendirent_typedirent_name+9 |
| 229 | func main(argc: i64, argv: *i64) -> i64 |