code wiki / _hdl_build / nx_treepack.nx

nx_treepack.nx source

↩ module page · 349 lines · 21559 B

1// nx_treepack.nx -- SOVEREIGN single-blob tree archive. Packs a directory tree into ONE .pack file (and unpacks 2// it) so a whole source tree (runtime/ = ~14,765 files / 72.6 MB) moves in ONE /api/upload instead of ~14,765 3// per-file SSH pushes (hours). THE keystone primitive for build-over-API + source-search-API (both need the 4// source tree on the NAS). Format (streaming, length-prefixed): "NXPK1\n" then repeat { u32 LE pathlen | path 5// (forward-slash, relative to pack root) | u64 LE size | size bytes }. Dirs are implicit (mkdir -p on unpack). 6// Additive-safe: pack reads only; unpack writes only under dest. Reuses nx_dr_tree's getdents walk pattern. 7// Buffers hoisted (shared cbuf/hbuf passed down; per-dir dbuf) -- no mmap in the per-file inner loops. 8// nx_treepack pack <src-dir> <out.pack> 9// nx_treepack unpack <in.pack> <dest-dir> 10// license_tier: ORIGINAL 11import "nx_syscalls.nx" 12import "nx_symdrop.nx" // sd_symbols / sd_has -- ONE definition of what a declaration is (rule 15) 13const K_MAGIC_3900: i64 = 3900 14const K_MAGIC_131072: i64 = 131072 15const K_MAGIC_1048576: i64 = 1048576 16const K_MAGIC_4000: i64 = 4000 17const K_MAGIC_4096: i64 = 4096 18 19func tp_w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 20func tp_wn(v: i64) -> i64 { var t:i64=v; if t<0{sys_write(1,"-" as *u8,1);t=0-t} let tm:*u8=sys_mmap(24); var k:i64=0; if t==0{tm[0]=48 as u8;k=1} while t>0{tm[k]=(48+(t%10)) as u8;t=t/10;k=k+1} let b:*u8=sys_mmap(24); var j:i64=0; while j<k{b[j]=tm[k-1-j];j=j+1} sys_write(1,b,k); return 0 } 21 22// read EXACTLY n bytes (loop over short reads); returns bytes actually read (< n only at EOF). 23func tp_readn(fd: i64, buf: *u8, n: i64) -> i64 { 24 var got: i64 = 0 25 while got < n { let r: i64 = sys_read(fd, (buf as i64 + got) as *u8, n - got); if r <= 0 { return got } got = got + r } 26 return got 27} 28func tp_put32(b: *u8, v: i64) -> i64 { b[0]=(v&0xff) as u8; b[1]=((v>>8)&0xff) as u8; b[2]=((v>>16)&0xff) as u8; b[3]=((v>>24)&0xff) as u8; return 0 } 29func tp_put64(b: *u8, v: i64) -> i64 { var i:i64=0; while i<8 { b[i]=((v>>(i*8))&0xff) as u8; i=i+1 } return 0 } 30func tp_get32(b: *u8) -> i64 { return (b[0] as i64) | ((b[1] as i64)<<8) | ((b[2] as i64)<<16) | ((b[3] as i64)<<24) } 31func tp_get64(b: *u8) -> i64 { var v:i64=0; var i:i64=0; while i<8 { v = v | ((b[i] as i64)<<(i*8)); i=i+1 } return v } 32 33func tp_isdotdot(nm: *u8) -> i64 { if nm[0]==(46 as u8){ if nm[1]==(0 as u8){return 1} if nm[1]==(46 as u8){ if nm[2]==(0 as u8){return 1} } } return 0 } 34// (canonical wrapper lives in nx_syscalls -- sys_unlinkat; kept as a thin alias for call-site clarity) 35func tp_unlink(path: *u8) -> i64 { return sys_unlinkat(path) } 36func tp_join(buf: *u8, base_n: i64, name: *u8) -> i64 { buf[base_n]=47 as u8; var o:i64=base_n+1; var i:i64=0; while name[i]!=(0 as u8){buf[o]=name[i];o=o+1;i=i+1} return o } 37func tp_size(path: *u8) -> i64 { let fd:i64=sys_openat_rd(path); if fd<0 {return 0-1} let s:i64=sys_lseek(fd,0,2); sys_close(fd); return s } 38 39// PACK walk: src[0..src_n) is the live path; rootlen = length of the original root dir (relpaths begin after 40// rootlen+1). hbuf(>=16)+cbuf(1MB) are shared scratch (used only at leaves, safe across recursion). dbuf is 41// per-call (each dir needs its own getdents buffer, since recursion happens inside the getdents loop). 42func tp_pack_walk(src: *u8, src_n: i64, rootlen: i64, packfd: i64, hbuf: *u8, cbuf: *u8, st: *i64) -> i64 { 43 if src_n > K_MAGIC_3900 { return 0 } 44 src[src_n] = 0 as u8 45 let fd: i64 = sys_openat_rd(src); if fd < 0 { return 0 } 46 let dbuf: *u8 = sys_mmap(K_MAGIC_131072) 47 var go: i64 = 1 48 while go == 1 { 49 let nr: i64 = sys_getdents64(fd, dbuf, K_MAGIC_131072) 50 if nr <= 0 { go = 0 } else { 51 var off: i64 = 0 52 while off < nr { 53 let rec: *u8 = (dbuf as i64 + off) as *u8 54 let ty: i64 = dirent_type(rec) 55 let nm: *u8 = dirent_name(rec) 56 if tp_isdotdot(nm) == 0 { 57 let cs: i64 = tp_join(src, src_n, nm) 58 if ty == 4 { st[1]=st[1]+1; tp_pack_walk(src, cs, rootlen, packfd, hbuf, cbuf, st) } 59 else { 60 src[cs] = 0 as u8 61 let sz: i64 = tp_size(src) 62 if sz < 0 { st[3]=st[3]+1 } else { 63 let rp: *u8 = (src as i64 + rootlen + 1) as *u8 64 let rlen: i64 = cs - rootlen - 1 65 tp_put32(hbuf, rlen); sys_write(packfd, hbuf, 4) 66 sys_write(packfd, rp, rlen) 67 tp_put64(hbuf, sz); sys_write(packfd, hbuf, 8) 68 let sfd: i64 = sys_openat_rd(src) 69 if sfd < 0 { st[3]=st[3]+1 } else { 70 var remain: i64 = sz 71 while remain > 0 { 72 var want: i64 = K_MAGIC_1048576; if remain < want { want = remain } 73 let n2: i64 = sys_read(sfd, cbuf, want) 74 if n2 <= 0 { remain = 0 } else { sys_write(packfd, cbuf, n2); remain = remain - n2 } 75 } 76 sys_close(sfd) 77 st[0]=st[0]+1; st[2]=st[2]+sz 78 } 79 } 80 } 81 } 82 off = off + dirent_reclen(rec) 83 } 84 } 85 } 86 sys_close(fd) 87 return 0 88} 89 90// mkdir every '/'-delimited prefix of fpath BELOW dest_n (so nested relpaths get their parent dirs). 91func tp_mkparents(fpath: *u8, dest_n: i64) -> i64 { 92 var i: i64 = dest_n + 1 93 while fpath[i] != (0 as u8) { 94 if fpath[i] == (47 as u8) { fpath[i]=0 as u8; sys_mkdir(fpath, 0x1ed); fpath[i]=47 as u8 } 95 i = i + 1 96 } 97 return 0 98} 99 100// seq1379 half-2 -- REFUSE TO BACKDATE. An unpack MERGES over the destination, so pushing a stale tree 101// silently reverted canonical sources on 2026-07-30 and re-opened FIVE eaten debts across THREE lanes 102// with no error anywhere (seq1370/1375). The rule that fixes it needs no policy and no config: 103// A DESTINATION FILE NEWER THAN THE PACK ITSELF CANNOT HAVE COME FROM THAT PACK. 104// So such a file is SKIPPED, COUNTED and NAMED rather than overwritten. `force` overrides deliberately. 105// This was unwritable until 2026-07-30 because the tree had NO way to read an mtime at all (sys_stat and 106// statx: 0 matches in 19,813 files) -- the primitive below is declared LOCALLY via __syscall (the 107// nx_movie_probe precedent) so nx_syscalls.nx, at ~14k importers, stays untouched. Offsets are VERIFIED 108// byte-exact against `stat -c '%Y %s'`, not assumed: st_mtim.tv_sec@88, st_size@48 in x86-64 struct stat. 109// ---- seq1467: content-regression refusal support ------------------------------------------------- 110// Buffers + the .nx test used by the REFUSED-WOULD-DROP-SYMBOLS rule in tp_unpack. The symbol scanner 111// itself is NOT re-implemented here -- sd_symbols/sd_has are imported from nx_symdrop (rule 15): the 112// detector that finds these reverts after the fact and the guard that prevents them must agree by 113// CONSTRUCTION, because two copies of "what counts as a declaration" would disagree exactly when it 114// matters and one of them would be silently wrong. 115const TP_SYMBUF: i64 = 4194304 // largest .nx source compared whole (nx_hostctl.nx ~281KB; 14x headroom) 116const TP_SYMCAP: i64 = 1048576 // symbol csv per side 117const TP_DOT: i64 = 46 118const TP_NAMEMAX: i64 = 256 119 120static tp_sbuf: *u8 // incoming entry, buffered so it can be compared then written 121static tp_dbuf2: *u8 // destination file 122static tp_isym: *u8 // incoming symbol csv 123static tp_dsym: *u8 // destination symbol csv 124static tp_mnm: *u8 // one symbol name 125 126func tp_sym_init() -> i64 { 127 if (tp_sbuf as i64) == 0 { 128 tp_sbuf = sys_mmap(TP_SYMBUF) 129 tp_dbuf2 = sys_mmap(TP_SYMBUF) 130 tp_isym = sys_mmap(TP_SYMCAP) 131 tp_dsym = sys_mmap(TP_SYMCAP) 132 tp_mnm = sys_mmap(TP_NAMEMAX) 133 } 134 return 0 135} 136// does this pack-relative path end in ".nx"? Only sources get the content check: a lost declaration in 137// a source is lost capability, whereas data/blob files legitimately shrink. 138func tp_is_nx(p: *u8) -> i64 { 139 var n: i64 = 0 140 while p[n] != (0 as u8) { n = n + 1 } 141 if n < 3 { return 0 } 142 if p[n-3] != (TP_DOT as u8) { return 0 } 143 if p[n-2] != (110 as u8) { return 0 } 144 if p[n-1] != (120 as u8) { return 0 } 145 return 1 146} 147// bounded whole-file read; -1 when absent 148func tp_read_file(path: *u8, b: *u8, cap: i64) -> i64 { 149 let fd: i64 = sys_openat_rd(path) 150 if fd < 0 { return 0 - 1 } 151 var n: i64 = 0 152 var go: i64 = 1 153 while go == 1 { 154 let r: i64 = sys_read(fd, (b as i64 + n) as *u8, cap - n) 155 if r > 0 { n = n + r } else { go = 0 } 156 if n >= cap { go = 0 } 157 } 158 sys_close(fd) 159 return n 160} 161 162const TP_SYS_FSTAT: i64 = 5 163const TP_SYS_NEWFSTATAT: i64 = 262 164const TP_AT_FDCWD: i64 = 0 - 100 165const TP_STATBUF: i64 = 256 166const TP_OFF_MTIME: i64 = 88 167 168// mtime of an OPEN fd, or 0 when unavailable. 0 is the FAIL-SAFE value: it makes every dest look newer 169// than the pack, which would skip everything loudly rather than overwrite anything silently. 170func tp_fd_mtime(fd: i64) -> i64 { 171 let sb: *u8 = sys_mmap(TP_STATBUF) 172 if __syscall(TP_SYS_FSTAT, fd, sb as i64, 0, 0, 0, 0) != 0 { return 0 } 173 let mp: *i64 = ((sb as i64) + TP_OFF_MTIME) as *i64 174 return mp[0] 175} 176// mtime of a PATH, or -1 when the file is absent (absent => free to create, never a backdate) 177func tp_path_mtime(path: *u8) -> i64 { 178 let sb: *u8 = sys_mmap(TP_STATBUF) 179 if __syscall(TP_SYS_NEWFSTATAT, TP_AT_FDCWD, path as i64, sb as i64, 0, 0, 0) != 0 { return 0 - 1 } 180 let mp: *i64 = ((sb as i64) + TP_OFF_MTIME) as *i64 181 return mp[0] 182} 183 184// UNPACK: fpath is prebuilt as dest + '/' (relpath is written after dest_n+1). Streams each file out. 185func tp_unpack(packfd: i64, fpath: *u8, dest_n: i64, hbuf: *u8, cbuf: *u8, st: *i64, packmt: i64, force: i64) -> i64 { 186 tp_sym_init() 187 let mg: i64 = tp_readn(packfd, hbuf, 6) 188 if mg < 6 { tp_w("BAD: truncated (no magic)\n" as *u8); return 0-1 } 189 if hbuf[0]!=(78 as u8) { tp_w("BAD: not an NXPK archive\n" as *u8); return 0-1 } 190 var go: i64 = 1 191 while go == 1 { 192 let g1: i64 = tp_readn(packfd, hbuf, 4) 193 if g1 < 4 { go = 0 } else { 194 let plen: i64 = tp_get32(hbuf) 195 if plen <= 0 { go = 0 } else { if plen > K_MAGIC_4000 { tp_w("BAD: pathlen overflow\n" as *u8); go = 0 } else { 196 let rp: *u8 = (fpath as i64 + dest_n + 1) as *u8 197 tp_readn(packfd, rp, plen) 198 rp[plen] = 0 as u8 199 let g2: i64 = tp_readn(packfd, hbuf, 8) 200 if g2 < 8 { go = 0 } else { 201 let sz: i64 = tp_get64(hbuf) 202 tp_mkparents(fpath, dest_n) 203 // overwrite-robust: an existing file can deny O_TRUNC (mixed-ownership buildroot, EACCES/ETXTBSY 204 // -- the seq142/seq124 stale-source root). Unlink-then-recreate wins wherever the DIR is writable. 205 // seq1379: a dest NEWER than the pack cannot have come from it -> skip, count, NAME it. 206 // The bytes are still consumed so the stream stays aligned (same discipline as the fail path). 207 // seq1379 half-2: a dest NEWER than the pack cannot have come from it -> skip, count, NAME. 208 // seq1467 (2026-07-30): that rule alone is NOT enough, and the hole is exactly how five 209 // debts got re-opened. It compares the dest against the PACK'S mtime, so it only catches 210 // REPLAYING AN OLD PACK. Packing a STALE TREE *now* produces a pack whose mtime is the 211 // newest thing in the system -- every destination then looks older, nothing is refused, and 212 // 07-29 content silently overwrote 07-30 sources (measured: NAS nx_syscalls.nx replaced by 213 // the laptop copy, erasing sys_ignore_sigpipe while three files still CALLED it, leaving the 214 // tree unbuildable; my parser guarantee went the same way in the same push). 215 // TIMESTAMPS CANNOT DECIDE THIS. Content can: an incoming source that DROPS top-level 216 // declarations the destination already has is a REGRESSION regardless of any mtime, and that 217 // test is immune to clock skew, touch, copy-preserved times and freshly-made packs alike. 218 // Applied to .nx sources only (where a lost symbol is lost capability), bounded by buffer 219 // size, and `force` still overrides deliberately. 220 var backdate: i64 = 0 221 var why: i64 = 0 222 if force == 0 { if tp_path_mtime(fpath) > packmt { backdate = 1; why = 1 } } 223 // CONTENT-REGRESSION check. Buffer the entry so the same bytes can be compared and then 224 // written -- the stream is consumed exactly once either way, so alignment is preserved on 225 // every path (the same discipline the skip path already keeps). 226 var buffered: i64 = 0 227 if backdate == 0 { if force == 0 { if tp_is_nx(rp) == 1 { if sz > 0 { if sz < TP_SYMBUF { 228 if tp_path_mtime(fpath) >= 0 { 229 if tp_readn(packfd, tp_sbuf, sz) == sz { 230 buffered = 1 231 let dn2: i64 = tp_read_file(fpath, tp_dbuf2, TP_SYMBUF) 232 if dn2 > 0 { 233 let li: i64 = sd_symbols(tp_sbuf, sz, tp_isym, TP_SYMCAP) 234 let ld: i64 = sd_symbols(tp_dbuf2, dn2, tp_dsym, TP_SYMCAP) 235 var miss: i64 = 0 236 var ms: i64 = 0 237 var mk: i64 = 0 238 while mk <= ld { 239 var msep: i64 = 0 240 if mk == ld { msep = 1 } else { if tp_dsym[mk] == (44 as u8) { msep = 1 } } 241 if msep == 1 { 242 let mnl: i64 = mk - ms 243 if mnl > 0 { if mnl < 200 { 244 var mz: i64 = 0 245 while mz < mnl { tp_mnm[mz] = tp_dsym[ms+mz]; mz = mz + 1 } 246 tp_mnm[mnl] = 0 as u8 247 if sd_has(tp_isym, li, tp_mnm, mnl) == 0 { miss = miss + 1 } 248 } } 249 ms = mk + 1 250 } 251 mk = mk + 1 252 } 253 if miss > 0 { backdate = 1; why = 2 } 254 } 255 } 256 } 257 } } } } } 258 if backdate == 1 { 259 st[5]=st[5]+1 260 if st[6]<8 { 261 st[6]=st[6]+1 262 if why == 2 { tp_w("REFUSED-WOULD-DROP-SYMBOLS: " as *u8) } else { tp_w("SKIP-NEWER-THAN-PACK: " as *u8) } 263 tp_w(fpath); tp_w("\n" as *u8) 264 } 265 if buffered == 0 { 266 var br: i64 = sz 267 while br > 0 { var bw: i64=K_MAGIC_1048576; if br<bw {bw=br} let n4: i64=tp_readn(packfd,cbuf,bw); if n4<=0 {br=0} else {br=br-n4} } 268 } 269 } else { 270 var ofd: i64 = sys_openat_wr(fpath, 0x1a4) 271 if ofd < 0 { tp_unlink(fpath); ofd = sys_openat_wr(fpath, 0x1a4) } 272 if ofd < 0 { 273 st[3]=st[3]+1 274 if st[4]<5 { st[4]=st[4]+1; tp_w("UNPACK-FAIL: " as *u8); tp_w(fpath); tp_w("\n" as *u8) } 275 if buffered == 0 { 276 var rr: i64 = sz 277 while rr > 0 { var w: i64=K_MAGIC_1048576; if rr<w {w=rr} let n3: i64=tp_readn(packfd,cbuf,w); if n3<=0 {rr=0} else {rr=rr-n3} } 278 } 279 } else { 280 if buffered == 1 { 281 sys_write(ofd, tp_sbuf, sz) 282 sys_close(ofd) 283 st[0]=st[0]+1; st[2]=st[2]+sz 284 } else { 285 var remain: i64 = sz 286 while remain > 0 { var want: i64=K_MAGIC_1048576; if remain<want {want=remain} let n2: i64=tp_readn(packfd,cbuf,want); if n2<=0 {remain=0} else { sys_write(ofd,cbuf,n2); remain=remain-n2 } } 287 sys_close(ofd) 288 st[0]=st[0]+1; st[2]=st[2]+sz 289 } 290 } 291 } 292 } 293 } } 294 } 295 } 296 return 0 297} 298 299func main(argc: i64, argv: *i64) -> i64 { 300 if argc < 4 { tp_w("usage: nx_treepack pack <src-dir> <out.pack> | nx_treepack unpack <in.pack> <dest-dir>\n" as *u8); sys_exit(2); return 2 } 301 let mode: *u8 = argv[1] as *u8 302 let a1: *u8 = argv[2] as *u8 303 let a2: *u8 = argv[3] as *u8 304 let hbuf: *u8 = sys_mmap(64) 305 let cbuf: *u8 = sys_mmap(K_MAGIC_1048576) 306 let st: *i64 = sys_mmap(64) as *i64; st[0]=0; st[1]=0; st[2]=0; st[3]=0; st[4]=0 307 if mode[0]==(112 as u8) { 308 let src: *u8 = sys_mmap(K_MAGIC_4096); var sn: i64=0; var i: i64=0; while a1[i]!=(0 as u8){src[sn]=a1[i];sn=sn+1;i=i+1} 309 if sn>0 { if src[sn-1]==(47 as u8) { sn=sn-1 } } 310 let packfd: i64 = sys_openat_wr(a2, 0x1a4) 311 if packfd < 0 { tp_w("FAIL: cannot open pack output\n" as *u8); sys_exit(1); return 1 } 312 hbuf[0]=78 as u8; hbuf[1]=88 as u8; hbuf[2]=80 as u8; hbuf[3]=75 as u8; hbuf[4]=49 as u8; hbuf[5]=10 as u8 313 sys_write(packfd, hbuf, 6) 314 tp_pack_walk(src, sn, sn, packfd, hbuf, cbuf, st) 315 sys_close(packfd) 316 tp_w("=== nx_treepack PACK: files=" as *u8); tp_wn(st[0]); tp_w(" dirs=" as *u8); tp_wn(st[1]); tp_w(" bytes=" as *u8); tp_wn(st[2]); tp_w(" failed=" as *u8); tp_wn(st[3]); tp_w(" -> " as *u8); tp_w(a2); tp_w("\n" as *u8) 317 if st[3]==0 { sys_exit(0); return 0 } 318 sys_exit(1); return 1 319 } 320 if mode[0]==(117 as u8) { 321 let packfd: i64 = sys_openat_rd(a1) 322 if packfd < 0 { tp_w("FAIL: cannot open pack\n" as *u8); sys_exit(1); return 1 } 323 let fpath: *u8 = sys_mmap(K_MAGIC_4096); var dn: i64=0; var i: i64=0; while a2[i]!=(0 as u8){fpath[dn]=a2[i];dn=dn+1;i=i+1} 324 if dn>0 { if fpath[dn-1]==(47 as u8) { dn=dn-1 } } 325 sys_mkdir(a2, 0x1ed) 326 fpath[dn]=47 as u8 327 // seq1379 half-2: the pack's OWN mtime is the reference clock. `force` (argv[4]) overrides. 328 let packmt: i64 = tp_fd_mtime(packfd) 329 var force: i64 = 0 330 if argc >= 5 { let f: *u8 = argv[4] as *u8; if f[0]==(102 as u8) { force = 1 } } 331 tp_unpack(packfd, fpath, dn, hbuf, cbuf, st, packmt, force) 332 sys_close(packfd) 333 tp_w("=== nx_treepack UNPACK: files=" as *u8); tp_wn(st[0]); tp_w(" bytes=" as *u8); tp_wn(st[2]); tp_w(" failed=" as *u8); tp_wn(st[3]); tp_w(" skipped-newer=" as *u8); tp_wn(st[5]); tp_w(" -> " as *u8); tp_w(a2); tp_w("\n" as *u8) 334 if st[5] > 0 { tp_w("NOTE: PRESERVED, not overwritten. Each line above says which rule fired: SKIP-NEWER-THAN-PACK = the destination is newer than the pack, so it cannot have come from it (seq1379); REFUSED-WOULD-DROP-SYMBOLS = the incoming source declares FEWER top-level symbols than the file already on disk, i.e. a stale tree would delete landed work (seq1467) -- that one is decided on CONTENT, so a freshly-made pack of an old tree cannot slip past it. Re-pack from current sources, or pass a 4th arg `force` to overwrite deliberately. 335" as *u8) } 336 // seq1519 -- A REFUSAL NOBODY SEES IS NOT A GUARD. This exited 0 whenever nothing FAILED, so a run 337 // that PRESERVED files (and therefore did NOT deliver them) reported success; nx_ship swallowed it 338 // and a sibling shipped a repair that never landed, leaving a truncated source and three gates 339 // unable to compile. A skip is an INCOMPLETE UNPACK, not a success: it now exits 5 -- distinct from 340 // 1 (write failure) so a wrapper can tell "some files were withheld on purpose" from "I/O broke". 341 if st[3] == 0 { if st[5] > 0 { 342 tp_w("UNPACK-INCOMPLETE skipped-newer=" as *u8); tp_wn(st[5]) 343 tp_w(" -- files were WITHHELD, so this unpack did NOT deliver the pack. exit=5; wrappers MUST surface this, never treat it as success. If the on-disk file is CORRUPT rather than newer-and-good (mtime says nothing about content), re-run with a 4th arg `force`.\n" as *u8) 344 sys_exit(5); return 5 345 } sys_exit(0); return 0 } 346 sys_exit(1); return 1 347 } 348 tp_w("unknown mode (use pack or unpack)\n" as *u8); sys_exit(2); return 2 349}