nx_assetprobe.nx source
↩ module page · 295 lines · 14440 B
1// nx_assetprobe.nx -- CONTENT-TYPED ASSET IDENTIFICATION FOR INGESTION.
2//
3// WHY (measured 2026-08-07 from vrstormlab.com/dancexr/preparecontent, their own docs): DanceXR
4// types assets by WHERE THEY SIT and WHAT THEY ARE NAMED -- typed subfolders (actors/ motion/
5// texture/), same-basename pairing (dance.vmd + dance.ogg), alternative-texture discovery by
6// matching filename. Every one of those decisions trusts the filename.
7//
8// *AN EXTENSION IS A CLAIM. THE MAGIC BYTES ARE THE MEASUREMENT. The same law cost us once today:
9// a grep for bvh matched nx_bvh (a Bounding Volume Hierarchy) and reported a motion format as
10// supported. A mislabelled asset in a name-driven pipeline fails at PLAY time, deep in a loader,
11// as a crash or an invisible model. Probed at INGEST time it is one honest row.
12//
13// THE RULER LIVES IN nx_assetprobe_lib (extracted 2026-09-06): identify / name / cat / claim are ONE function each, shared
14// with the bundle walker (/compare/modding MD1), so the CLI and the walker cannot drift. This file is the CLI, the zip
15// central-directory walk and the KAT.
16//
17// usage: nx_assetprobe probe <file> | zip <file> | --kat
18// exit 0 ok | 2 unreadable | 3 usage | 4 REFUSED malformed | 5 DISAGREE | 1 KAT RED
19// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
20import "nx_syscalls.nx"
21import "nx_assetprobe_lib.nx"
22
23const AP_KATBUF: i64 = 8192
24const AP_MAXMEM: i64 = 512
25
26const AP_EOCD_MIN: i64 = 22
27const AP_CD_MIN: i64 = 46
28
29func ap_puts(s: *u8) -> i64 { sys_write(1, s, ap_slen(s)); return 0 }
30func ap_num(v: i64) -> i64 {
31 let t: *u8 = sys_mmap(32)
32 var m: i64 = v
33 var w: i64 = 0
34 if m < 0 { t[0] = 45 as u8; sys_write(1, t, 1); m = 0 - m }
35 if m == 0 { t[0] = 48 as u8; sys_write(1, t, 1); return 0 }
36 let d: *u8 = sys_mmap(32)
37 var k: i64 = 0
38 while m > 0 { d[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
39 var j: i64 = 0
40 while j < k { t[w] = d[k - 1 - j]; w = w + 1; j = j + 1 }
41 sys_write(1, t, w)
42 return 0
43}
44
45func ap_probe(path: *u8, b: *u8, len: i64) -> i64 {
46 let actual: i64 = ap_identify(b, len)
47 let claim: i64 = ap_claim(path)
48 ap_puts("bytes=" as *u8); ap_num(len)
49 ap_puts(" magic=" as *u8); ap_puts(ap_name(actual))
50 ap_puts(" category=" as *u8); ap_puts(ap_catname(ap_cat(actual)))
51 ap_puts(" ext_claim=" as *u8); ap_puts(ap_name(claim))
52 if actual == AP_UNKNOWN {
53 ap_puts(" verdict=UNIDENTIFIED\n" as *u8)
54 return 0
55 }
56 if claim == AP_UNKNOWN {
57 ap_puts(" verdict=UNCLAIMED-typed-by-content\n" as *u8)
58 return 0
59 }
60 if claim == actual { ap_puts(" verdict=AGREE\n" as *u8); return 0 }
61 ap_puts(" verdict=DISAGREE extension-lies\n" as *u8)
62 return 5
63}
64
65// ---- ZIP central directory (the correct enumeration; scanning local headers misreads
66// deleted/spanned entries). A container whose EOCD is absent is REFUSED, never
67// reported as zero members -- an empty answer from an unread structure is not a measurement.
68func ap_eocd(b: *u8, len: i64) -> i64 {
69 if len < AP_EOCD_MIN { return 0 - 1 }
70 var i: i64 = len - AP_EOCD_MIN
71 var found: i64 = 0 - 1
72 var done: i64 = 0
73 while done == 0 {
74 if i < 0 { done = 1 }
75 else {
76 if (b[i] & 0xff) as i64 == 0x50 {
77 if (b[i+1] & 0xff) as i64 == 0x4b {
78 if (b[i+2] & 0xff) as i64 == 0x05 {
79 if (b[i+3] & 0xff) as i64 == 0x06 { found = i; done = 1 }
80 }
81 }
82 }
83 if done == 0 { i = i - 1 }
84 }
85 }
86 return found
87}
88
89func ap_zip(b: *u8, len: i64) -> i64 {
90 let e: i64 = ap_eocd(b, len)
91 if e < 0 { ap_puts("nx_assetprobe REFUSED no-EOCD not-a-zip-or-truncated\n" as *u8); return 4 }
92 let total: i64 = ap_u16(b, e + 10)
93 let cdoff: i64 = ap_u32(b, e + 16)
94 ap_puts("zip_members=" as *u8); ap_num(total); ap_puts("\n" as *u8)
95 ap_puts("idx\tcsize\tusize\tmethod\tclaim\tname\n" as *u8)
96 var p: i64 = cdoff
97 var k: i64 = 0
98 var models: i64 = 0
99 var motions: i64 = 0
100 var audio: i64 = 0
101 while k < total {
102 if p + AP_CD_MIN > len { ap_puts("REFUSED central-dir-overruns-file\n" as *u8); return 4 }
103 if (b[p] & 0xff) as i64 != 0x50 { ap_puts("REFUSED bad-central-sig\n" as *u8); return 4 }
104 let method: i64 = ap_u16(b, p + 10)
105 let csize: i64 = ap_u32(b, p + 20)
106 let usize: i64 = ap_u32(b, p + 24)
107 let fnlen: i64 = ap_u16(b, p + 28)
108 let exlen: i64 = ap_u16(b, p + 30)
109 let cmlen: i64 = ap_u16(b, p + 32)
110 if p + AP_CD_MIN + fnlen > len { ap_puts("REFUSED name-overruns-file\n" as *u8); return 4 }
111 let nm: *u8 = sys_mmap(fnlen + 2)
112 var q: i64 = 0
113 while q < fnlen { nm[q] = b[p + AP_CD_MIN + q]; q = q + 1 }
114 nm[fnlen] = 0 as u8
115 let cl: i64 = ap_claim(nm)
116 let cat: i64 = ap_cat(cl)
117 if cat == AP_C_MODEL { models = models + 1 }
118 if cat == AP_C_MOTION { motions = motions + 1 }
119 if cat == AP_C_AUDIO { audio = audio + 1 }
120 ap_num(k); ap_puts("\t" as *u8); ap_num(csize); ap_puts("\t" as *u8); ap_num(usize)
121 ap_puts("\t" as *u8); ap_num(method); ap_puts("\t" as *u8); ap_puts(ap_name(cl))
122 ap_puts("\t" as *u8); ap_puts(nm); ap_puts("\n" as *u8)
123 p = p + AP_CD_MIN + fnlen + exlen + cmlen
124 k = k + 1
125 }
126 // DanceXR calls audio+motion(+camera) co-located a "dance set", with a one-audio-file rule.
127 // We report the composition so the caller can decide, and we NAME the ambiguity rather than
128 // silently picking one.
129 ap_puts("composition models=" as *u8); ap_num(models)
130 ap_puts(" motions=" as *u8); ap_num(motions)
131 ap_puts(" audio=" as *u8); ap_num(audio)
132 if motions > 0 {
133 if audio == 1 { ap_puts(" danceset=YES\n" as *u8) }
134 else {
135 if audio == 0 { ap_puts(" danceset=NO no-audio\n" as *u8) }
136 else { ap_puts(" danceset=AMBIGUOUS multi-audio\n" as *u8) }
137 }
138 } else { ap_puts(" danceset=NO no-motion\n" as *u8) }
139 return 0
140}
141
142// ===== KAT ========================================================
143func ap_put(b: *u8, o: i64, s: *u8) -> i64 {
144 let n: i64 = ap_slen(s)
145 var i: i64 = 0
146 while i < n { b[o + i] = s[i]; i = i + 1 }
147 return o + n
148}
149func ap_p16(b: *u8, o: i64, v: i64) -> i64 {
150 b[o] = (v & 0xff) as u8; b[o+1] = ((v >> 8) & 0xff) as u8; return 0
151}
152func ap_p32(b: *u8, o: i64, v: i64) -> i64 {
153 b[o] = (v & 0xff) as u8; b[o+1] = ((v >> 8) & 0xff) as u8
154 b[o+2] = ((v >> 16) & 0xff) as u8; b[o+3] = ((v >> 24) & 0xff) as u8; return 0
155}
156
157// a REAL minimal zip: 2 stored members, proper local headers + central dir + EOCD
158func ap_kat_zip(b: *u8) -> i64 {
159 var o: i64 = 0
160 let lo1: i64 = o
161 ap_p32(b, o, 0x04034b50); ap_p16(b, o+4, 10); ap_p16(b, o+6, 0); ap_p16(b, o+8, 0)
162 ap_p16(b, o+10, 0); ap_p16(b, o+12, 0); ap_p32(b, o+14, 0)
163 ap_p32(b, o+18, 3); ap_p32(b, o+22, 3); ap_p16(b, o+26, 9); ap_p16(b, o+28, 0)
164 o = ap_put(b, 30, "dance.vmd" as *u8)
165 o = ap_put(b, o, "abc" as *u8)
166 let lo2: i64 = o
167 ap_p32(b, o, 0x04034b50); ap_p16(b, o+4, 10); ap_p16(b, o+6, 0); ap_p16(b, o+8, 0)
168 ap_p16(b, o+10, 0); ap_p16(b, o+12, 0); ap_p32(b, o+14, 0)
169 ap_p32(b, o+18, 3); ap_p32(b, o+22, 3); ap_p16(b, o+26, 9); ap_p16(b, o+28, 0)
170 o = ap_put(b, o + 30, "dance.ogg" as *u8)
171 o = ap_put(b, o, "xyz" as *u8)
172 let cd: i64 = o
173 ap_p32(b, o, 0x02014b50); ap_p16(b, o+4, 20); ap_p16(b, o+6, 10); ap_p16(b, o+8, 0)
174 ap_p16(b, o+10, 0); ap_p16(b, o+12, 0); ap_p16(b, o+14, 0); ap_p32(b, o+16, 0)
175 ap_p32(b, o+20, 3); ap_p32(b, o+24, 3); ap_p16(b, o+28, 9); ap_p16(b, o+30, 0)
176 ap_p16(b, o+32, 0); ap_p16(b, o+34, 0); ap_p16(b, o+36, 0); ap_p32(b, o+38, 0)
177 ap_p32(b, o+42, lo1)
178 o = ap_put(b, o + 46, "dance.vmd" as *u8)
179 ap_p32(b, o, 0x02014b50); ap_p16(b, o+4, 20); ap_p16(b, o+6, 10); ap_p16(b, o+8, 0)
180 ap_p16(b, o+10, 0); ap_p16(b, o+12, 0); ap_p16(b, o+14, 0); ap_p32(b, o+16, 0)
181 ap_p32(b, o+20, 3); ap_p32(b, o+24, 3); ap_p16(b, o+28, 9); ap_p16(b, o+30, 0)
182 ap_p16(b, o+32, 0); ap_p16(b, o+34, 0); ap_p16(b, o+36, 0); ap_p32(b, o+38, 0)
183 ap_p32(b, o+42, lo2)
184 o = ap_put(b, o + 46, "dance.ogg" as *u8)
185 let cdsize: i64 = o - cd
186 ap_p32(b, o, 0x06054b50); ap_p16(b, o+4, 0); ap_p16(b, o+6, 0)
187 ap_p16(b, o+8, 2); ap_p16(b, o+10, 2)
188 ap_p32(b, o+12, cdsize); ap_p32(b, o+16, cd); ap_p16(b, o+20, 0)
189 return o + AP_EOCD_MIN
190}
191
192func ap_kat() -> i64 {
193 var red: i64 = 0
194 let b: *u8 = sys_mmap(AP_KATBUF)
195
196 // T1 binary magic
197 b[0] = 0x89 as u8; ap_put(b, 1, "PNG" as *u8)
198 if ap_identify(b, 8) != AP_PNG { ap_puts("T1 RED png\n" as *u8); red = red + 1 }
199 else { ap_puts("T1 GREEN png-magic\n" as *u8) }
200
201 // T2 ANTI-VACUITY: a PNG named model.pmx. Extension-driven ingestion accepts this and
202 // fails later inside the model loader; only a content probe can catch it here.
203 if ap_probe("model.pmx" as *u8, b, 8) != 5 {
204 ap_puts("T2 RED disagree-not-caught\n" as *u8); red = red + 1
205 } else { ap_puts("T2 GREEN extension-lie-caught\n" as *u8) }
206
207 // T3 VMD magic + agreement
208 var i: i64 = 0
209 while i < AP_KATBUF { b[i] = 0 as u8; i = i + 1 }
210 ap_put(b, 0, "Vocaloid Motion Data 0002" as *u8)
211 if ap_identify(b, 64) != AP_VMD { ap_puts("T3 RED vmd\n" as *u8); red = red + 1 }
212 else { ap_puts("T3 GREEN vmd-magic\n" as *u8) }
213
214 // T4 text format found AFTER a transport preamble (our own fetch lane emits one)
215 var j: i64 = 0
216 while j < AP_KATBUF { b[j] = 0 as u8; j = j + 1 }
217 let po: i64 = ap_put(b, 0, "nishi-xfer recv=367ms cl-stop\n" as *u8)
218 ap_put(b, po, "HIERARCHY\nROOT Hips\n" as *u8)
219 if ap_identify(b, 64) != AP_BVH { ap_puts("T4 RED bvh-after-preamble\n" as *u8); red = red + 1 }
220 else { ap_puts("T4 GREEN bvh-found-past-preamble\n" as *u8) }
221
222 // T5 real zip: central dir walked, both members named
223 var z: i64 = 0
224 while z < AP_KATBUF { b[z] = 0 as u8; z = z + 1 }
225 let zn: i64 = ap_kat_zip(b)
226 if ap_identify(b, zn) != AP_ZIP { ap_puts("T5 RED zip-magic\n" as *u8); red = red + 1 }
227 if ap_eocd(b, zn) < 0 { ap_puts("T5 RED eocd\n" as *u8); red = red + 1 }
228 else { ap_puts("T5 GREEN zip eocd_at=" as *u8); ap_num(ap_eocd(b, zn)); ap_puts("\n" as *u8) }
229 if ap_zip(b, zn) != 0 { ap_puts("T5 RED zip-walk\n" as *u8); red = red + 1 }
230
231 // T6 ANTI-VACUITY: truncate away the EOCD. The local headers and names are all still
232 // perfectly readable, so a local-header scanner would happily report members.
233 if ap_zip(b, zn - AP_EOCD_MIN) != 4 {
234 ap_puts("T6 RED truncated-zip-accepted\n" as *u8); red = red + 1
235 } else { ap_puts("T6 GREEN truncated-zip-refused\n" as *u8) }
236
237 // T7 ANTI-VACUITY: honest UNKNOWN. A matcher that defaults to a plausible type is worse
238 // than one that admits it does not know.
239 var q: i64 = 0
240 while q < AP_KATBUF { b[q] = 0 as u8; q = q + 1 }
241 ap_put(b, 0, "qqqq not any known asset qqqq" as *u8)
242 if ap_identify(b, 28) != AP_UNKNOWN { ap_puts("T7 RED false-positive\n" as *u8); red = red + 1 }
243 else { ap_puts("T7 GREEN unknown-is-honest\n" as *u8) }
244
245 // T8 Wavefront by CONTENT (2026-09-06): an OBJ is vertices plus faces at line starts, an MTL declares newmtl; a
246 // comment that merely mentions "v " mid-line must not type a file as a mesh
247 var w8: i64 = 0
248 while w8 < AP_KATBUF { b[w8] = 0 as u8; w8 = w8 + 1 }
249 ap_put(b, 0, "# a comment with v 1 2 3 inside\nmtllib cube.mtl\nv 0 0 0\nv 1 0 0\nf 1 2 3\n" as *u8)
250 if ap_identify(b, 80) != AP_OBJ { ap_puts("T8 RED obj-by-content\n" as *u8); red = red + 1 }
251 else { ap_puts("T8 GREEN obj-by-content\n" as *u8) }
252 var w9: i64 = 0
253 while w9 < AP_KATBUF { b[w9] = 0 as u8; w9 = w9 + 1 }
254 ap_put(b, 0, "# a comment with v 1 2 3 inside and nothing else\n" as *u8)
255 if ap_identify(b, 52) != AP_UNKNOWN { ap_puts("T8 RED comment-typed-as-obj\n" as *u8); red = red + 1 }
256 else { ap_puts("T8 GREEN comment-not-a-mesh\n" as *u8) }
257 var w10: i64 = 0
258 while w10 < AP_KATBUF { b[w10] = 0 as u8; w10 = w10 + 1 }
259 ap_put(b, 0, "newmtl skin\nmap_Kd skin.png\n" as *u8)
260 if ap_identify(b, 28) != AP_MTL { ap_puts("T8 RED mtl-by-content\n" as *u8); red = red + 1 }
261 else { ap_puts("T8 GREEN mtl-by-content\n" as *u8) }
262
263 // T9 the Bethesda and archive magics (2026-09-06): exact-offset, each one line
264 var w11: i64 = 0
265 while w11 < AP_KATBUF { b[w11] = 0 as u8; w11 = w11 + 1 }
266 ap_put(b, 0, "DDS |" as *u8)
267 if ap_identify(b, 8) != AP_DDS { ap_puts("T9 RED dds\n" as *u8); red = red + 1 } else { ap_puts("T9 GREEN dds-magic\n" as *u8) }
268 ap_put(b, 0, "Gamebryo File Format, Version 20.2.0.7" as *u8)
269 if ap_identify(b, 40) != AP_NIF { ap_puts("T9 RED nif\n" as *u8); red = red + 1 } else { ap_puts("T9 GREEN nif-magic\n" as *u8) }
270 b[0] = 0x37 as u8; b[1] = 0x7a as u8; b[2] = 0xbc as u8; b[3] = 0xaf as u8; b[4] = 0x27 as u8; b[5] = 0x1c as u8
271 if ap_identify(b, 8) != AP_7Z { ap_puts("T9 RED 7z\n" as *u8); red = red + 1 } else { ap_puts("T9 GREEN 7z-magic\n" as *u8) }
272 ap_put(b, 0, "Rar!" as *u8)
273 if ap_identify(b, 8) != AP_RAR { ap_puts("T9 RED rar\n" as *u8); red = red + 1 } else { ap_puts("T9 GREEN rar-magic\n" as *u8) }
274 if ap_cat(AP_7Z) != AP_C_BUNDLE { ap_puts("T9 RED 7z-not-bundle\n" as *u8); red = red + 1 }
275 if ap_cat(AP_MTL) != AP_C_MATERIAL { ap_puts("T9 RED mtl-not-material\n" as *u8); red = red + 1 }
276
277 if red > 0 { ap_puts("nx_assetprobe KAT RED teeth_failed=" as *u8); ap_num(red); ap_puts("\n" as *u8); return 1 }
278 ap_puts("nx_assetprobe KAT GREEN 9/9\n" as *u8)
279 return 0
280}
281
282func main(argc: i64, argv: *i64) -> i64 {
283 if argc < 2 { ap_puts("usage: nx_assetprobe probe|zip <file> | --kat\n" as *u8); return 3 }
284 let verb: *u8 = argv[1] as *u8
285 if ap_slen(verb) == 5 { if verb[0] == (45 as u8) { return ap_kat() } }
286 if argc < 3 { ap_puts("usage: nx_assetprobe probe|zip <file> | --kat\n" as *u8); return 3 }
287 let path: *u8 = argv[2] as *u8
288 let lenp: *i64 = sys_mmap(16) as *i64
289 let buf: *u8 = sys_read_file(path, lenp)
290 if (buf as i64) == 0 { ap_puts("nx_assetprobe unreadable\n" as *u8); return 2 }
291 if verb[0] == (112 as u8) { return ap_probe(path, buf, lenp[0]) }
292 if verb[0] == (122 as u8) { return ap_zip(buf, lenp[0]) }
293 ap_puts("nx_assetprobe unknown verb\n" as *u8)
294 return 3
295}