code wiki / (root) / nx_assetprobe.nx

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}