nx_untar.nx source
↩ module page · 187 lines · 9902 B
1// nx_untar.nx -- extract files from a USTAR archive (.tar or .tar.gz) into a directory INSIDE the sovereign
2// write boundary. The missing verb between nx_research_fetch (which mirrors a tarball into
3// knowledge/fetched/ with a pinned sha) and every organ that needs one of the files inside it.
4//
5// nx_untar <archive.tar|archive.tar.gz> <outdir> [name-substring]
6//
7// Composes the incumbents, never re-implements them: nx_inflate (inf_gunzip) for the .gz layer and
8// nx_tar (nxtar_read) for the entries. The decompressed size is DERIVED from the gzip ISIZE trailer --
9// the format's own declaration -- so there is no reserve to guess and no cap to raise. Entries are
10// written FLAT (basename only, 0644) into <outdir>, which must begin with knowledge/ or /tmp/ (the same
11// boundary nx_mkdirp enforces); anything else is refused before a byte is written. A substring filter
12// selects entries by name; with none, every regular file is written.
13// Every written file is announced with its byte count so absence is visible in normal output, and the
14// summary partition (entries = files + dirs + other) is printed and must sum. Exit: 0 written>0 |
15// 2 usage/boundary | 3 archive unreadable | 4 nothing matched (EMPTY is not a pass).
16// First use 2026-09-02: landing LiberationSans-Regular.ttf on the NAS for the browser face (BR26).
17// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
18import "nx_syscalls.nx"
19import "nx_inflate.nx"
20import "nx_tar.nx"
21
22const UT_MODE_644: i64 = 420
23const UT_MODE_DIR: i64 = 493
24const UT_GZ_TRAILER: i64 = 8 // gzip: CRC32 + ISIZE, little-endian, at the very end
25const UT_PATH_CAP: i64 = 4096
26const UT_FLD_SLOTS: i64 = 8
27const UT_EXIT_USAGE: i64 = 2
28const UT_EXIT_UNREADABLE: i64 = 3
29const UT_EXIT_EMPTY: i64 = 4
30
31func ut_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
32func ut_e(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(2, s, n); return 0 }
33func ut_num(v: i64) -> i64 {
34 if v == 0 { sys_write(1, "0" as *u8, 1); return 0 }
35 var m: i64 = v
36 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m }
37 let t: *u8 = sys_mmap(32)
38 var k: i64 = 0
39 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
40 let o: *u8 = sys_mmap(32)
41 var i: i64 = 0
42 while i < k { o[i] = t[k-1-i]; i = i + 1 }
43 sys_write(1, o, k)
44 return 0
45}
46func ut_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
47func ut_starts(s: *u8, p: *u8) -> i64 {
48 var i: i64 = 0
49 while p[i] != (0 as u8) { if s[i] != p[i] { return 0 } i = i + 1 }
50 return 1
51}
52func ut_ends_gz(s: *u8) -> i64 {
53 let n: i64 = ut_slen(s)
54 if n < 3 { return 0 }
55 if (s[n-3] & 0xff) != 46 { return 0 }
56 if (s[n-2] & 0xff) != 103 { return 0 }
57 if (s[n-1] & 0xff) != 122 { return 0 }
58 return 1
59}
60// does the entry name (data+off, len) contain the substring?
61func ut_contains(d: *u8, off: i64, len: i64, sub: *u8) -> i64 {
62 let sl: i64 = ut_slen(sub)
63 if sl == 0 { return 1 }
64 var i: i64 = 0
65 while i + sl <= len {
66 var k: i64 = 0
67 var ok: i64 = 1
68 while k < sl { if (d[off+i+k] & 0xff) != (sub[k] & 0xff) { ok = 0; k = sl } else { k = k + 1 } }
69 if ok == 1 { return 1 }
70 i = i + 1
71 }
72 return 0
73}
74// little-endian u32 (the gzip trailer)
75func ut_rd32le(b: *u8, o: i64) -> i64 { return (b[o] as i64) + (b[o+1] as i64)*256 + (b[o+2] as i64)*65536 + (b[o+3] as i64)*16777216 }
76
77func main(argc: i64, argv: *i64) -> i64 {
78 if argc < 3 {
79 ut_e("usage: nx_untar <archive.tar|archive.tar.gz> <outdir under knowledge/ or /tmp/> [name-substring]\n\x00" as *u8)
80 return UT_EXIT_USAGE
81 }
82 let apath: *u8 = argv[1] as *u8
83 let outdir: *u8 = argv[2] as *u8
84 var sub: *u8 = "\x00" as *u8
85 if argc >= 4 { sub = argv[3] as *u8 }
86 var inside: i64 = 0
87 if ut_starts(outdir, "knowledge/\x00" as *u8) == 1 { inside = 1 }
88 if ut_starts(outdir, "/tmp/\x00" as *u8) == 1 { inside = 1 }
89 if inside == 0 {
90 ut_e("nx_untar: REFUSED outdir outside the sovereign write boundary (knowledge/ or /tmp/): \x00" as *u8); ut_e(outdir); ut_e("\n\x00" as *u8)
91 return UT_EXIT_USAGE
92 }
93 let lp: *i64 = sys_mmap(16) as *i64
94 lp[0] = 0 - 1
95 let raw: *u8 = sys_read_file(apath, lp)
96 let rawlen: i64 = lp[0]
97 if rawlen <= 0 { ut_e("nx_untar: cannot read \x00" as *u8); ut_e(apath); ut_e("\n\x00" as *u8); return UT_EXIT_UNREADABLE }
98 var data: *u8 = raw
99 var n: i64 = rawlen
100 // DETECT BY MAGIC, NEVER BY NAME (learned on first use 2026-09-02): the sovereign fetcher had mirrored a
101 // GitHub tarball under its .tar.gz name after its transport decoder inflated the gzip payload, so the
102 // file on disk was a plain ustar archive -- a name-based test asked the gzip layer for an ISIZE that
103 // was really the last 4 bytes of a tar block and refused a perfectly readable archive. A .gz name with
104 // no 1f 8b magic is announced and read as tar.
105 var isgz: i64 = 0
106 if rawlen >= 2 { if (raw[0] & 0xff) == 31 { if (raw[1] & 0xff) == 139 { isgz = 1 } } }
107 if isgz == 0 { if ut_ends_gz(apath) == 1 { ut_w("nx_untar: name ends in .gz but the bytes carry no gzip magic -- reading as a plain ustar archive\n\x00" as *u8) } }
108 if isgz == 1 {
109 if rawlen < UT_GZ_TRAILER { ut_e("nx_untar: gzip too short for a trailer\n\x00" as *u8); return UT_EXIT_UNREADABLE }
110 // DERIVED, not guessed: the gzip ISIZE trailer declares the decompressed length (mod 2^32; an
111 // archive that large is not this organ's subject and reads as a short buffer, refused below)
112 let isize: i64 = ut_rd32le(raw, rawlen - 4)
113 if isize <= 0 { ut_e("nx_untar: gzip ISIZE trailer declares 0 bytes\n\x00" as *u8); return UT_EXIT_UNREADABLE }
114 let out: *u8 = sys_mmap(isize + 16)
115 let got: i64 = inf_gunzip(raw, rawlen, out, isize + 16)
116 if got <= 0 { ut_e("nx_untar: gunzip failed rc=\x00" as *u8); ut_num(got); ut_e("\n\x00" as *u8); return UT_EXIT_UNREADABLE }
117 if got != isize { ut_e("nx_untar: gunzip produced a length the ISIZE trailer did not declare (truncated or corrupt)\n\x00" as *u8); return UT_EXIT_UNREADABLE }
118 data = out
119 n = got
120 ut_w("gunzip: \x00" as *u8); ut_num(rawlen); ut_w(" -> \x00" as *u8); ut_num(n); ut_w(" bytes (ISIZE-derived)\n\x00" as *u8)
121 }
122 sys_mkdir(outdir, UT_MODE_DIR) // EEXIST is fine: the directory is the precondition, not the product
123 let fld: *i64 = sys_mmap(8*UT_FLD_SLOTS) as *i64
124 let path: *u8 = sys_mmap(UT_PATH_CAP)
125 var off: i64 = 0
126 var entries: i64 = 0
127 var files: i64 = 0
128 var dirs: i64 = 0
129 var other: i64 = 0
130 var matched: i64 = 0
131 var written: i64 = 0
132 var go: i64 = 1
133 while go == 1 {
134 let r: i64 = nxtar_read(data, n, off, fld)
135 if r <= 0 {
136 if r < 0 { ut_e("nx_untar: archive unreadable at offset \x00" as *u8); ut_num(off); ut_e(" (bad header or checksum)\n\x00" as *u8); return UT_EXIT_UNREADABLE }
137 go = 0
138 } else {
139 entries = entries + 1
140 let ty: i64 = fld[NX_TAR_FLD_TYPE]
141 let nlen: i64 = fld[NX_TAR_FLD_NAMELEN]
142 let size: i64 = fld[NX_TAR_FLD_SIZE]
143 let doff: i64 = fld[NX_TAR_FLD_DATAOFF]
144 var isfile: i64 = 0
145 if ty == NX_TAR_TYPE_FILE { isfile = 1 }
146 if ty == 0 { isfile = 1 }
147 if isfile == 1 { files = files + 1 } else { if ty == NX_TAR_TYPE_DIR { dirs = dirs + 1 } else { other = other + 1 } }
148 if isfile == 1 { if ut_contains(data, off, nlen, sub) == 1 {
149 matched = matched + 1
150 // basename of the entry
151 var bs: i64 = 0
152 var q: i64 = 0
153 while q < nlen { if (data[off+q] & 0xff) == 47 { bs = q + 1 } q = q + 1 }
154 var pl: i64 = 0
155 let ol: i64 = ut_slen(outdir)
156 var c: i64 = 0
157 while c < ol { path[pl] = outdir[c]; pl = pl + 1; c = c + 1 }
158 if pl > 0 { if (path[pl-1] & 0xff) != 47 { path[pl] = 47 as u8; pl = pl + 1 } }
159 var q2: i64 = bs
160 while q2 < nlen { if pl < UT_PATH_CAP - 1 { path[pl] = data[off+q2]; pl = pl + 1 } q2 = q2 + 1 }
161 path[pl] = 0 as u8
162 let fd: i64 = sys_openat_wr(path, UT_MODE_644)
163 if fd < 0 { ut_e("nx_untar: cannot open for write \x00" as *u8); ut_e(path); ut_e("\n\x00" as *u8) }
164 else {
165 var wrote: i64 = 0
166 while wrote < size {
167 let wn: i64 = sys_write(fd, (data as i64 + doff + wrote) as *u8, size - wrote)
168 if wn <= 0 { wrote = size + 1 } else { wrote = wrote + wn }
169 }
170 sys_close(fd)
171 if wrote == size {
172 written = written + 1
173 ut_w("UNTAR \x00" as *u8); sys_write(1, (data as i64 + off) as *u8, nlen); ut_w(" bytes=\x00" as *u8); ut_num(size); ut_w(" -> \x00" as *u8); ut_w(path); ut_w("\n\x00" as *u8)
174 } else {
175 ut_e("nx_untar: SHORT WRITE on \x00" as *u8); ut_e(path); ut_e("\n\x00" as *u8)
176 }
177 }
178 } }
179 off = fld[NX_TAR_FLD_NEXT]
180 }
181 }
182 ut_w("entries=\x00" as *u8); ut_num(entries); ut_w(" files=\x00" as *u8); ut_num(files); ut_w(" dirs=\x00" as *u8); ut_num(dirs); ut_w(" other=\x00" as *u8); ut_num(other)
183 ut_w(" sum=\x00" as *u8); ut_num(files + dirs + other); ut_w(" matched=\x00" as *u8); ut_num(matched); ut_w(" written=\x00" as *u8); ut_num(written)
184 if written > 0 { ut_w(" verdict=WRITTEN\n\x00" as *u8); return 0 }
185 ut_w(" verdict=EMPTY-not-a-pass\n\x00" as *u8)
186 return UT_EXIT_EMPTY
187}