code wiki / (root) / nx_installed_census_lib.nx

nx_installed_census_lib.nx source

↩ module page · 668 lines · 29918 B

1// nx_installed_census_lib.nx -- THE INSTALLED MODE OF THE INTELMINE MINER (/compare/intelmine IM20 ic_steam_library, 2// IM22 ic_fingerprint, IM24 ic_tieout). LIB: reads a Steam client's library folders and app manifests, walks every 3// install tree with a bounded depth and budget, fingerprints what the product SHIPS (engine, anti-cheat, VR runtime, 4// audio and physics middleware, netcode, video, scripting, mod hooks, redistributables) from a DATA conf of name 5// signatures, and ties each title to the review rubric already journaled for the same appid. The thin program 6// nx_installed_census prints and writes; the gate drives this lib on a planted library under /tmp. 7// 8// WHY (operator 2026-09-05): "can our miner also mine installed games ... applications and systems and os ... to get a 9// better more detailed idea on what needs to live on our /compare board vs broad strokes and have it tied out to 10// sentiment". Reviews say WHAT PLAYERS FEEL; the install tree says WHAT THE PRODUCT IS MADE OF. Joined by appid they 11// answer the board's question in rows a small model can read: VR HOT ships Unity + OpenXR and its reviewers complain 12// about controls and tracking; Palworld ships Unreal + Wwise at 41 GB and its reviewers complain about optimisation. 13// 14// FORMATS READ (Valve KeyValues text, measured on this box 2026-09-05): steamapps/libraryfolders.vdf carries one 15// "path" per library (Windows paths with doubled backslashes); steamapps/appmanifest_<appid>.acf carries "appid", 16// "name", "installdir", "SizeOnDisk", "buildid", "LastUpdated", "LastPlayed" and a UserConfig "language". The install 17// tree is <library>/steamapps/common/<installdir>. Under WSL a Windows library path is mapped to /mnt/<drive>/.... 18// license_tier: ORIGINAL 19import "nx_syscalls.nx" 20import "nx_reviewmine_lib.nx" 21 22const IC_DIRBUF: i64 = 65536 23const IC_PATH_CAP: i64 = 4096 24const IC_MAXDEPTH_MAX: i64 = 8 25const IC_MAXDEPTH_DEFAULT: i64 = 4 26// walk budget per install: a AAA tree runs to tens of thousands of entries; past this the walk STOPS DESCENDING and 27// the row says walk_capped=1 (coverage incomplete for that title), never a silent partial. 28const IC_MAX_ENTRIES: i64 = 200000 29const IC_CLASS_N: i64 = 10 30const IC_FP_MAX: i64 = 512 31const IC_FP_ARENA: i64 = 65536 32const IC_LABELS_CAP: i64 = 512 33const IC_NAME_CAP: i64 = 512 34const IC_LIB_MAX: i64 = 32 35const IC_DT_DIR: i64 = 4 36const IC_DT_REG: i64 = 8 37const IC_DT_LNK: i64 = 10 38const IC_CH_DOT: i64 = 46 39const IC_CH_COLON: i64 = 58 40const IC_CH_BSLASH: i64 = 92 41const IC_CH_SEMI: i64 = 59 42const IC_SUMMARY_FIELDS: i64 = 7 43const IC_MANIFEST_PREFIX: *u8 = "appmanifest_" 44const IC_MANIFEST_SUFFIX: *u8 = ".acf" 45// asset-format census: files by extension with their byte totals, so a title's models, textures and audio read as 46// format:count:bytes rows (a Unity title is .assets/.resS/.bundle, an Unreal title is .pak/.utoc, a Ren'Py title is .rpa) 47const IC_EXT_MAX: i64 = 128 48const IC_EXT_CAP: i64 = 12 49const IC_EXT_STRIDE: i64 = 16 50const IC_ASSETS_TOP: i64 = 8 51const IC_SEEK_END: i64 = 2 52 53// ---- output record slots ---- 54const IC_R_APPID: i64 = 0 55const IC_R_SIZE: i64 = 1 56const IC_R_BUILDID: i64 = 2 57const IC_R_UPDATED: i64 = 3 58const IC_R_PLAYED: i64 = 4 59const IC_R_FILES: i64 = 5 60const IC_R_DIRS: i64 = 6 61const IC_R_CAPPED: i64 = 7 62const IC_R_ON_DISK: i64 = 8 63const IC_R_REVIEWS: i64 = 9 64const IC_R_SCORE: i64 = 10 65const IC_R_POS: i64 = 11 66const IC_R_NEG: i64 = 12 67const IC_R_TOTAL: i64 = 13 68const IC_R_BYTES: i64 = 14 69const IC_R_N: i64 = 16 70const IC_REC_BYTES: i64 = 128 71// ---- class indices: the census columns engine..redist in order (IC_CLASS_N of them; the last is redist) ---- 72const IC_CLASS_ENGINE: i64 = 0 73const IC_CLASS_ANTICHEAT: i64 = 1 74const IC_CLASS_VR: i64 = 2 75const IC_CLASS_AUDIO: i64 = 3 76const IC_CLASS_NET: i64 = 4 77const IC_CLASS_VIDEO: i64 = 5 78const IC_CLASS_PHYSICS: i64 = 6 79const IC_CLASS_SCRIPT: i64 = 7 80const IC_CLASS_MOD: i64 = 8 81// ---- widths and shapes the code spells out (rule 11): each named for its meaning, never its value ---- 82const IC_DEPTH_SLOTS_SPARE: i64 = 2 // per-depth buffers: depths 0..IC_MAXDEPTH_MAX inclusive, plus one spare 83const IC_NUL_PAIR: i64 = 2 // a fingerprint row stores its pattern and its label, each NUL-terminated 84const IC_SEP_AND_NUL: i64 = 2 // appending a label costs its joiner plus the NUL 85const IC_EXT_ROW_NUMS: i64 = 48 // room beside an extension name for :files:bytes (two decimal i64 with colons) 86const IC_NEEDLE_PATH_LEN: i64 = 6 // the six bytes of the quoted key path 87const IC_NEEDLE_CAP: i64 = 16 88const IC_DOTDOT_LEN: i64 = 2 // the entry name of two dots 89const IC_DRIVE_PREFIX_LEN: i64 = 2 // the drive letter and its colon before a Windows path 90const IC_ROW_MARK: i64 = 105 // the byte i that opens every census row 91const IC_SUM_F_TOTAL: i64 = 1 // <appid>.summary fields: appid|total|pos|neg|score 92const IC_SUM_F_POS: i64 = 2 93const IC_SUM_F_NEG: i64 = 3 94const IC_SUM_F_SCORE: i64 = 4 95 96static ic_dbuf: *u8 97static ic_pbuf: *u8 98static ic_fp_class: *i64 99static ic_fp_pat_off: *i64 100static ic_fp_pat_len: *i64 101static ic_fp_label_off: *i64 102static ic_fp_found: *i64 103static ic_fp_arena: *u8 104static ic_fp_used: i64 105static ic_fp_n: i64 106static ic_walk_entries: i64 107static ic_walk_files: i64 108static ic_walk_dirs: i64 109static ic_walk_capped: i64 110static ic_maxdepth: i64 111static ic_labels: *u8 112static ic_name: *u8 113static ic_installdir: *u8 114static ic_lang: *u8 115static ic_rec: *i64 116static ic_libs: *u8 117static ic_lib_n: i64 118static ic_ext_names: *u8 119static ic_ext_count: *i64 120static ic_ext_bytes: *i64 121static ic_ext_n: i64 122static ic_ext_overflow: i64 123static ic_walk_bytes: i64 124static ic_ext_scratch: *u8 125 126func ic_init() -> i64 { 127 if (ic_dbuf as i64) != 0 { return 0 } 128 ic_dbuf = sys_mmap(IC_DIRBUF * (IC_MAXDEPTH_MAX + IC_DEPTH_SLOTS_SPARE)) 129 ic_pbuf = sys_mmap(IC_PATH_CAP * (IC_MAXDEPTH_MAX + IC_DEPTH_SLOTS_SPARE)) 130 ic_fp_class = sys_mmap(IC_FP_MAX * RM_I64_BYTES) as *i64 131 ic_fp_pat_off = sys_mmap(IC_FP_MAX * RM_I64_BYTES) as *i64 132 ic_fp_pat_len = sys_mmap(IC_FP_MAX * RM_I64_BYTES) as *i64 133 ic_fp_label_off = sys_mmap(IC_FP_MAX * RM_I64_BYTES) as *i64 134 ic_fp_found = sys_mmap(IC_FP_MAX * RM_I64_BYTES) as *i64 135 ic_fp_arena = sys_mmap(IC_FP_ARENA) 136 ic_labels = sys_mmap(IC_LABELS_CAP) 137 ic_name = sys_mmap(IC_NAME_CAP) 138 ic_installdir = sys_mmap(IC_NAME_CAP) 139 ic_lang = sys_mmap(IC_NAME_CAP) 140 ic_rec = sys_mmap(IC_REC_BYTES) as *i64 141 ic_libs = sys_mmap(IC_LIB_MAX * IC_PATH_CAP) 142 ic_ext_names = sys_mmap(IC_EXT_MAX * IC_EXT_STRIDE) 143 ic_ext_count = sys_mmap(IC_EXT_MAX * RM_I64_BYTES) as *i64 144 ic_ext_bytes = sys_mmap(IC_EXT_MAX * RM_I64_BYTES) as *i64 145 ic_ext_scratch = sys_mmap(IC_EXT_STRIDE) 146 ic_ext_n = 0; ic_ext_overflow = 0; ic_walk_bytes = 0 147 ic_fp_used = 0; ic_fp_n = 0; ic_lib_n = 0 148 ic_maxdepth = IC_MAXDEPTH_DEFAULT 149 return 0 150} 151func ic_lc(c: i64) -> i64 { if c >= RM_UPPER_A { if c <= RM_UPPER_Z { return c + RM_CASE_DELTA } } return c } 152func ic_class_name(c: i64) -> *u8 { 153 if c == IC_CLASS_ENGINE { return "engine" as *u8 } 154 if c == IC_CLASS_ANTICHEAT { return "anticheat" as *u8 } 155 if c == IC_CLASS_VR { return "vr" as *u8 } 156 if c == IC_CLASS_AUDIO { return "audio" as *u8 } 157 if c == IC_CLASS_NET { return "net" as *u8 } 158 if c == IC_CLASS_VIDEO { return "video" as *u8 } 159 if c == IC_CLASS_PHYSICS { return "physics" as *u8 } 160 if c == IC_CLASS_SCRIPT { return "script" as *u8 } 161 if c == IC_CLASS_MOD { return "mod" as *u8 } 162 return "redist" as *u8 163} 164func ic_class_index(s: *u8, n: i64) -> i64 { 165 var c: i64 = 0 166 while c < IC_CLASS_N { 167 let nm: *u8 = ic_class_name(c) 168 if rm_slen(nm) == n { 169 var same: i64 = 1 170 var i: i64 = 0 171 while i < n { if nm[i] != s[i] { same = 0 } i = i + 1 } 172 if same == 1 { return c } 173 } 174 c = c + 1 175 } 176 return 0 - 1 177} 178// ---- fingerprint conf: class|substring|label ---- 179func ic_fp_load(path: *u8) -> i64 { 180 ic_init() 181 ic_fp_n = 0; ic_fp_used = 0 182 let lp: *i64 = sys_mmap(RM_I64_PAIR) as *i64 183 lp[0] = 0 184 let b: *u8 = sys_read_file(path, lp) 185 if (b as i64) == 0 { return 0 } 186 let n: i64 = lp[0] 187 var i: i64 = 0 188 while i < n { 189 let e: i64 = rm_line_end(b, n, i) 190 var t: i64 = e 191 if t > i { if (b[t - 1] as i64) == RM_CR { t = t - 1 } } 192 if t > i { if (b[i] as i64) != IC_CH_SEMI { if ic_fp_n < IC_FP_MAX { 193 // split on two pipes 194 var p1: i64 = 0 - 1 195 var p2: i64 = 0 - 1 196 var k: i64 = i 197 while k < t { if (b[k] as i64) == RM_PIPE { if p1 < 0 { p1 = k } else { if p2 < 0 { p2 = k } } } k = k + 1 } 198 if p1 > i { if p2 > p1 { if t > p2 + 1 { 199 let cls: i64 = ic_class_index((b as i64 + i) as *u8, p1 - i) 200 let plen: i64 = p2 - p1 - 1 201 let llen: i64 = t - p2 - 1 202 if cls >= 0 { if plen > 0 { if ic_fp_used + plen + llen + IC_NUL_PAIR < IC_FP_ARENA { 203 ic_fp_class[ic_fp_n] = cls 204 ic_fp_pat_off[ic_fp_n] = ic_fp_used 205 ic_fp_pat_len[ic_fp_n] = plen 206 var q: i64 = 0 207 while q < plen { ic_fp_arena[ic_fp_used + q] = ic_lc(b[p1 + 1 + q] as i64) as u8; q = q + 1 } 208 ic_fp_used = ic_fp_used + plen 209 ic_fp_arena[ic_fp_used] = 0 as u8; ic_fp_used = ic_fp_used + 1 210 ic_fp_label_off[ic_fp_n] = ic_fp_used 211 rm_catn(ic_fp_arena, ic_fp_used, (b as i64 + p2 + 1) as *u8, llen) 212 ic_fp_used = ic_fp_used + llen 213 ic_fp_arena[ic_fp_used] = 0 as u8; ic_fp_used = ic_fp_used + 1 214 ic_fp_n = ic_fp_n + 1 215 } } } 216 } } } 217 } } } 218 i = e + 1 219 } 220 return ic_fp_n 221} 222func ic_fp_count() -> i64 { return ic_fp_n } 223func ic_fp_reset_found() -> i64 { var i: i64 = 0; while i < ic_fp_n { ic_fp_found[i] = 0; i = i + 1 } return 0 } 224// does the (case-folded) name contain pattern row r? 225func ic_name_matches(name: *u8, r: i64) -> i64 { 226 let nl: i64 = rm_slen(name) 227 let pl: i64 = ic_fp_pat_len[r] 228 let pat: *u8 = (ic_fp_arena as i64 + ic_fp_pat_off[r]) as *u8 229 if pl > nl { return 0 } 230 var i: i64 = 0 231 while i + pl <= nl { 232 var same: i64 = 1 233 var j: i64 = 0 234 while j < pl { if ic_lc(name[i + j] as i64) != (pat[j] as i64) { same = 0; j = pl } j = j + 1 } 235 if same == 1 { return 1 } 236 i = i + 1 237 } 238 return 0 239} 240func ic_match_name(name: *u8) -> i64 { 241 var hits: i64 = 0 242 var r: i64 = 0 243 while r < ic_fp_n { 244 if ic_fp_found[r] == 0 { if ic_name_matches(name, r) == 1 { ic_fp_found[r] = 1; hits = hits + 1 } } 245 r = r + 1 246 } 247 return hits 248} 249// labels seen for a class, distinct, joined by ';' into out; returns the count of distinct labels ('-' written when none) 250func ic_class_labels(c: i64, out: *u8, cap: i64) -> i64 { 251 var o: i64 = 0 252 var cnt: i64 = 0 253 var r: i64 = 0 254 while r < ic_fp_n { 255 if ic_fp_class[r] == c { if ic_fp_found[r] == 1 { 256 let lab: *u8 = (ic_fp_arena as i64 + ic_fp_label_off[r]) as *u8 257 // dedupe against labels already written (scan out) 258 var dup: i64 = 0 259 var s: i64 = 0 260 while s < o { 261 var e: i64 = s 262 while e < o { if (out[e] as i64) == IC_CH_SEMI { e = o + 1 } else { e = e + 1 } } 263 var segend: i64 = e 264 if e == o + 1 { segend = e - 1 } 265 // segment [s, segend) -- compare to lab 266 var same: i64 = 1 267 var k: i64 = 0 268 while k < segend - s { if lab[k] == (0 as u8) { same = 0 } else { if lab[k] != out[s + k] { same = 0 } } k = k + 1 } 269 if same == 1 { if lab[segend - s] != (0 as u8) { same = 0 } } 270 if same == 1 { dup = 1 } 271 s = segend + 1 272 } 273 if dup == 0 { 274 let ll: i64 = rm_slen(lab) 275 if o + ll + IC_SEP_AND_NUL < cap { 276 if o > 0 { out[o] = IC_CH_SEMI as u8; o = o + 1 } 277 o = rm_cat(out, o, lab) 278 cnt = cnt + 1 279 } 280 } 281 } } 282 r = r + 1 283 } 284 if cnt == 0 { out[0] = RM_MINUS as u8; o = 1 } 285 out[o] = 0 as u8 286 return cnt 287} 288// ---- extension table ---- 289func ic_ext_reset() -> i64 { ic_ext_n = 0; ic_ext_overflow = 0; ic_walk_bytes = 0; return 0 } 290// lowercase extension of a file name into ic_ext_scratch; 0 when none, too long, or a dot-file 291func ic_ext_of(name: *u8) -> i64 { 292 let n: i64 = rm_slen(name) 293 var i: i64 = n - 1 294 var dot: i64 = 0 - 1 295 while i > 0 { if (name[i] as i64) == IC_CH_DOT { dot = i; i = 0 } else { i = i - 1 } } 296 if dot <= 0 { return 0 } 297 let el: i64 = n - dot - 1 298 if el <= 0 { return 0 } 299 if el > IC_EXT_CAP { return 0 } 300 var k: i64 = 0 301 while k < el { ic_ext_scratch[k] = ic_lc(name[dot + 1 + k] as i64) as u8; k = k + 1 } 302 ic_ext_scratch[el] = 0 as u8 303 return el 304} 305func ic_ext_bump(ext: *u8, bytes: i64) -> i64 { 306 var i: i64 = 0 307 while i < ic_ext_n { 308 if rm_streq((ic_ext_names as i64 + i * IC_EXT_STRIDE) as *u8, ext) == 1 { ic_ext_count[i] = ic_ext_count[i] + 1; ic_ext_bytes[i] = ic_ext_bytes[i] + bytes; return i } 309 i = i + 1 310 } 311 if ic_ext_n >= IC_EXT_MAX { ic_ext_overflow = 1; return 0 - 1 } 312 let d: *u8 = (ic_ext_names as i64 + ic_ext_n * IC_EXT_STRIDE) as *u8 313 let o: i64 = rm_cat(d, 0, ext) 314 d[o] = 0 as u8 315 ic_ext_count[ic_ext_n] = 1 316 ic_ext_bytes[ic_ext_n] = bytes 317 ic_ext_n = ic_ext_n + 1 318 return ic_ext_n - 1 319} 320func ic_ext_count_get() -> i64 { return ic_ext_n } 321func ic_ext_overflowed() -> i64 { return ic_ext_overflow } 322func ic_walk_bytes_get() -> i64 { return ic_walk_bytes } 323func ic_ext_name_at(i: i64) -> *u8 { return (ic_ext_names as i64 + i * IC_EXT_STRIDE) as *u8 } 324func ic_ext_files_at(i: i64) -> i64 { return ic_ext_count[i] } 325func ic_ext_bytes_at(i: i64) -> i64 { return ic_ext_bytes[i] } 326func ic_file_size(path: *u8) -> i64 { 327 let fd: i64 = sys_openat_rd(path) 328 if fd < 0 { return 0 } 329 let sz: i64 = sys_lseek(fd, 0, IC_SEEK_END) 330 sys_close(fd) 331 if sz < 0 { return 0 } 332 return sz 333} 334// top-N extensions by bytes as ext:files:bytes;... ('-' when none). Selection over at most IC_EXT_MAX entries. 335func ic_assets_summary(out: *u8, cap: i64, topn: i64) -> i64 { 336 var o: i64 = 0 337 var written: i64 = 0 338 let taken: *i64 = sys_mmap(IC_EXT_MAX * RM_I64_BYTES) as *i64 339 var r: i64 = 0 340 while r < topn { 341 var best: i64 = 0 - 1 342 var bestb: i64 = 0 - 1 343 var i: i64 = 0 344 while i < ic_ext_n { if taken[i] == 0 { if ic_ext_bytes[i] > bestb { best = i; bestb = ic_ext_bytes[i] } } i = i + 1 } 345 if best < 0 { r = topn } 346 else { 347 taken[best] = 1 348 if o + IC_EXT_STRIDE + IC_EXT_ROW_NUMS < cap { 349 if written > 0 { out[o] = IC_CH_SEMI as u8; o = o + 1 } 350 o = rm_cat(out, o, ic_ext_name_at(best)) 351 out[o] = IC_CH_COLON as u8; o = o + 1 352 o = rm_cati(out, o, ic_ext_count[best]) 353 out[o] = IC_CH_COLON as u8; o = o + 1 354 o = rm_cati(out, o, ic_ext_bytes[best]) 355 written = written + 1 356 } 357 r = r + 1 358 } 359 } 360 if written == 0 { out[0] = RM_MINUS as u8; o = 1 } 361 out[o] = 0 as u8 362 return written 363} 364// ---- the bounded walk ---- 365func ic_walk(dir: *u8, depth: i64) -> i64 { 366 if depth > ic_maxdepth { return 0 } 367 let fd: i64 = sys_openat_rd(dir) 368 if fd < 0 { return 0 - 1 } 369 let dbuf: *u8 = (ic_dbuf as i64 + depth * IC_DIRBUF) as *u8 370 let child: *u8 = (ic_pbuf as i64 + depth * IC_PATH_CAP) as *u8 371 let dl: i64 = rm_slen(dir) 372 var go: i64 = 1 373 while go == 1 { 374 let nread: i64 = sys_getdents64(fd, dbuf, IC_DIRBUF) 375 if nread <= 0 { go = 0 } 376 else { 377 var off: i64 = 0 378 while off < nread { 379 let rec: *u8 = (dbuf as i64 + off) as *u8 380 let reclen: i64 = dirent_reclen(rec) 381 let dtype: i64 = dirent_type(rec) 382 let name: *u8 = dirent_name(rec) 383 var skip: i64 = 0 384 if (name[0] as i64) == IC_CH_DOT { if name[1] == (0 as u8) { skip = 1 } if (name[1] as i64) == IC_CH_DOT { if name[IC_DOTDOT_LEN] == (0 as u8) { skip = 1 } } } 385 if skip == 0 { 386 if ic_walk_entries >= IC_MAX_ENTRIES { ic_walk_capped = 1 } 387 else { 388 ic_walk_entries = ic_walk_entries + 1 389 ic_match_name(name) 390 if dtype == IC_DT_DIR { 391 ic_walk_dirs = ic_walk_dirs + 1 392 if depth < ic_maxdepth { 393 let nl: i64 = rm_slen(name) 394 if dl + 1 + nl + 1 < IC_PATH_CAP { 395 var o: i64 = rm_cat(child, 0, dir) 396 child[o] = RM_CH_SLASH as u8; o = o + 1 397 o = rm_cat(child, o, name) 398 child[o] = 0 as u8 399 ic_walk(child, depth + 1) 400 } 401 } 402 } else { 403 ic_walk_files = ic_walk_files + 1 404 let nl2: i64 = rm_slen(name) 405 if dl + 1 + nl2 + 1 < IC_PATH_CAP { 406 var o2: i64 = rm_cat(child, 0, dir) 407 child[o2] = RM_CH_SLASH as u8; o2 = o2 + 1 408 o2 = rm_cat(child, o2, name) 409 child[o2] = 0 as u8 410 let fsz: i64 = ic_file_size(child) 411 ic_walk_bytes = ic_walk_bytes + fsz 412 if ic_ext_of(name) > 0 { ic_ext_bump(ic_ext_scratch, fsz) } 413 } 414 } 415 } 416 } 417 if reclen <= 0 { off = nread } else { off = off + reclen } 418 } 419 } 420 } 421 sys_close(fd) 422 return 0 423} 424func ic_walk_reset() -> i64 { ic_walk_entries = 0; ic_walk_files = 0; ic_walk_dirs = 0; ic_walk_capped = 0; ic_fp_reset_found(); ic_ext_reset(); return 0 } 425func ic_walk_files_seen() -> i64 { return ic_walk_files } 426func ic_walk_dirs_seen() -> i64 { return ic_walk_dirs } 427func ic_walk_was_capped() -> i64 { return ic_walk_capped } 428func ic_set_maxdepth(d: i64) -> i64 { if d < 0 { return 0 } if d > IC_MAXDEPTH_MAX { ic_maxdepth = IC_MAXDEPTH_MAX } else { ic_maxdepth = d } return 0 } 429 430// ---- Valve KeyValues: "key"<ws>"value" -- the first occurrence of the quoted key within [from,to) ---- 431func ic_kv(buf: *u8, n: i64, key: *u8, from: i64, to: i64, out: *u8, cap: i64) -> i64 { 432 let kl: i64 = rm_slen(key) 433 var lim: i64 = to 434 if lim > n { lim = n } 435 var i: i64 = from 436 while i + kl + RM_KEY_QUOTES < lim { 437 if (buf[i] as i64) == RM_Q { if (buf[i + 1 + kl] as i64) == RM_Q { 438 var same: i64 = 1 439 var j: i64 = 0 440 while j < kl { if buf[i + 1 + j] != key[j] { same = 0 } j = j + 1 } 441 if same == 1 { 442 var p: i64 = i + kl + RM_KEY_QUOTES 443 var ws: i64 = 1 444 while ws == 1 { if p >= lim { ws = 0 } else { let c: i64 = buf[p] as i64; if c == RM_SP { p = p + 1 } else { if c == RM_TAB { p = p + 1 } else { ws = 0 } } } } 445 if p < lim { if (buf[p] as i64) == RM_Q { 446 p = p + 1 447 var o: i64 = 0 448 var go: i64 = 1 449 while go == 1 { 450 if p >= lim { go = 0 } 451 else { 452 let c2: i64 = buf[p] as i64 453 if c2 == RM_Q { go = 0 } 454 else { 455 if c2 == IC_CH_BSLASH { if p + 1 < lim { if (buf[p + 1] as i64) == IC_CH_BSLASH { p = p + 1 } } } 456 if o + 1 < cap { out[o] = buf[p] as u8; o = o + 1 } 457 p = p + 1 458 } 459 } 460 } 461 out[o] = 0 as u8 462 return o 463 } } 464 return 0 - 1 465 } 466 } } 467 i = i + 1 468 } 469 out[0] = 0 as u8 470 return 0 - 1 471} 472// Windows drive path -> WSL mount path; a POSIX path passes through. Backslashes become slashes either way. 473func ic_map_path(src: *u8, dst: *u8, cap: i64) -> i64 { 474 var o: i64 = 0 475 var i: i64 = 0 476 if src[0] != (0 as u8) { if (src[1] as i64) == IC_CH_COLON { 477 o = rm_cat(dst, 0, "/mnt/" as *u8) 478 dst[o] = ic_lc(src[0] as i64) as u8; o = o + 1 479 i = IC_DRIVE_PREFIX_LEN 480 } } 481 while src[i] != (0 as u8) { 482 var c: i64 = src[i] as i64 483 if c == IC_CH_BSLASH { c = RM_CH_SLASH } 484 if o + 1 < cap { dst[o] = c as u8; o = o + 1 } 485 i = i + 1 486 } 487 // drop a trailing slash 488 if o > 1 { if (dst[o - 1] as i64) == RM_CH_SLASH { o = o - 1 } } 489 dst[o] = 0 as u8 490 return o 491} 492func ic_lib_at(i: i64) -> *u8 { return (ic_libs as i64 + i * IC_PATH_CAP) as *u8 } 493func ic_lib_count() -> i64 { return ic_lib_n } 494func ic_lib_add(p: *u8) -> i64 { 495 var i: i64 = 0 496 while i < ic_lib_n { if rm_streq(ic_lib_at(i), p) == 1 { return 0 } i = i + 1 } 497 if ic_lib_n >= IC_LIB_MAX { return 0 - 1 } 498 let d: *u8 = ic_lib_at(ic_lib_n) 499 let o: i64 = rm_cat(d, 0, p) 500 d[o] = 0 as u8 501 ic_lib_n = ic_lib_n + 1 502 return 1 503} 504// the quoted key "path" as bytes, assembled at runtime so no source literal carries an escaped quote 505static ic_needle_g: *u8 506func ic_needle_path() -> *u8 { 507 if (ic_needle_g as i64) == 0 { 508 ic_needle_g = sys_mmap(IC_NEEDLE_CAP) 509 ic_needle_g[0] = RM_Q as u8 510 rm_cat(ic_needle_g, 1, "path" as *u8) 511 ic_needle_g[IC_NEEDLE_PATH_LEN - 1] = RM_Q as u8 512 ic_needle_g[IC_NEEDLE_PATH_LEN] = 0 as u8 513 } 514 return ic_needle_g 515} 516// libraries: the root itself plus every "path" in steamapps/libraryfolders.vdf (mapped). Returns the count. 517func ic_libraries(steamroot: *u8) -> i64 { 518 ic_init() 519 ic_lib_n = 0 520 let mapped: *u8 = sys_mmap(IC_PATH_CAP) 521 ic_map_path(steamroot, mapped, IC_PATH_CAP) 522 ic_lib_add(mapped) 523 let vdf: *u8 = sys_mmap(IC_PATH_CAP) 524 var o: i64 = rm_cat(vdf, 0, mapped) 525 o = rm_cat(vdf, o, "/steamapps/libraryfolders.vdf" as *u8) 526 vdf[o] = 0 as u8 527 let lp: *i64 = sys_mmap(RM_I64_PAIR) as *i64 528 lp[0] = 0 529 let b: *u8 = sys_read_file(vdf, lp) 530 if (b as i64) == 0 { return ic_lib_n } 531 let n: i64 = lp[0] 532 let raw: *u8 = sys_mmap(IC_PATH_CAP) 533 var from: i64 = 0 534 var go: i64 = 1 535 while go == 1 { 536 let p: i64 = rm_find(b, n, ic_needle_path(), from) 537 if p < 0 { go = 0 } 538 else { 539 let l: i64 = ic_kv(b, n, "path" as *u8, p, n, raw, IC_PATH_CAP) 540 if l > 0 { ic_map_path(raw, mapped, IC_PATH_CAP); ic_lib_add(mapped) } 541 from = p + IC_NEEDLE_PATH_LEN 542 } 543 } 544 return ic_lib_n 545} 546// parse one manifest into ic_rec / ic_name / ic_installdir / ic_lang; returns 1 ok 547func ic_manifest(path: *u8) -> i64 { 548 var k: i64 = 0 549 while k < IC_R_N { ic_rec[k] = 0; k = k + 1 } 550 ic_name[0] = 0 as u8; ic_installdir[0] = 0 as u8; ic_lang[0] = 0 as u8 551 let lp: *i64 = sys_mmap(RM_I64_PAIR) as *i64 552 lp[0] = 0 553 let b: *u8 = sys_read_file(path, lp) 554 if (b as i64) == 0 { return 0 } 555 let n: i64 = lp[0] 556 let tmp: *u8 = sys_mmap(IC_NAME_CAP) 557 if ic_kv(b, n, "appid" as *u8, 0, n, tmp, IC_NAME_CAP) <= 0 { return 0 } 558 ic_rec[IC_R_APPID] = rm_atoi(tmp) 559 ic_kv(b, n, "name" as *u8, 0, n, ic_name, IC_NAME_CAP) 560 ic_kv(b, n, "installdir" as *u8, 0, n, ic_installdir, IC_NAME_CAP) 561 if ic_kv(b, n, "SizeOnDisk" as *u8, 0, n, tmp, IC_NAME_CAP) > 0 { ic_rec[IC_R_SIZE] = rm_atoi(tmp) } 562 if ic_kv(b, n, "buildid" as *u8, 0, n, tmp, IC_NAME_CAP) > 0 { ic_rec[IC_R_BUILDID] = rm_atoi(tmp) } 563 if ic_kv(b, n, "LastUpdated" as *u8, 0, n, tmp, IC_NAME_CAP) > 0 { ic_rec[IC_R_UPDATED] = rm_atoi(tmp) } 564 if ic_kv(b, n, "LastPlayed" as *u8, 0, n, tmp, IC_NAME_CAP) > 0 { ic_rec[IC_R_PLAYED] = rm_atoi(tmp) } 565 ic_kv(b, n, "language" as *u8, 0, n, ic_lang, IC_NAME_CAP) 566 if ic_lang[0] == (0 as u8) { ic_lang[0] = RM_MINUS as u8; ic_lang[1] = 0 as u8 } 567 if ic_rec[IC_R_APPID] <= 0 { return 0 } 568 return 1 569} 570func ic_rec_get(slot: i64) -> i64 { return ic_rec[slot] } 571func ic_name_get() -> *u8 { return ic_name } 572func ic_installdir_get() -> *u8 { return ic_installdir } 573func ic_lang_get() -> *u8 { return ic_lang } 574// tie-out: <reviewdir>/<appid>.summary (appid|total|pos|neg|score|epoch|rows) and <appid>.seen (8 bytes per journaled review) 575func ic_tieout(reviewdir: *u8, appid: i64) -> i64 { 576 ic_rec[IC_R_REVIEWS] = 0; ic_rec[IC_R_SCORE] = 0; ic_rec[IC_R_POS] = 0; ic_rec[IC_R_NEG] = 0; ic_rec[IC_R_TOTAL] = 0 577 let p: *u8 = sys_mmap(IC_PATH_CAP) 578 var o: i64 = rm_cat(p, 0, reviewdir) 579 o = rm_cati(p, o, appid) 580 o = rm_cat(p, o, ".seen" as *u8) 581 p[o] = 0 as u8 582 let lp: *i64 = sys_mmap(RM_I64_PAIR) as *i64 583 lp[0] = 0 584 let sb: *u8 = sys_read_file(p, lp) 585 if (sb as i64) != 0 { ic_rec[IC_R_REVIEWS] = lp[0] / RM_I64_BYTES } 586 o = rm_cat(p, 0, reviewdir) 587 o = rm_cati(p, o, appid) 588 o = rm_cat(p, o, ".summary" as *u8) 589 p[o] = 0 as u8 590 lp[0] = 0 591 let b: *u8 = sys_read_file(p, lp) 592 if (b as i64) == 0 { return 0 } 593 let n: i64 = lp[0] 594 // fields split on pipes: 0 appid 1 total 2 pos 3 neg 4 score 595 var f: i64 = 0 596 var s: i64 = 0 597 var i: i64 = 0 598 while i <= n { 599 var c: i64 = RM_PIPE 600 if i < n { c = b[i] as i64 } 601 if c == RM_PIPE { let v: i64 = rm_int(b, n, s); if f == IC_SUM_F_TOTAL { ic_rec[IC_R_TOTAL] = v } if f == IC_SUM_F_POS { ic_rec[IC_R_POS] = v } if f == IC_SUM_F_NEG { ic_rec[IC_R_NEG] = v } if f == IC_SUM_F_SCORE { ic_rec[IC_R_SCORE] = v } f = f + 1; s = i + 1 } 602 else { if c == RM_LF { i = n } } 603 i = i + 1 604 } 605 return 1 606} 607// append one census row for the current app to out (pipe-separated; '|' in a name becomes \p). Returns bytes. 608func ic_row_emit(libidx: i64, out: *u8, cap: i64) -> i64 { 609 var o: i64 = 0 610 out[o] = IC_ROW_MARK as u8; o = o + 1 611 out[o] = RM_PIPE as u8; o = o + 1; o = rm_cati(out, o, ic_rec[IC_R_APPID]) 612 out[o] = RM_PIPE as u8; o = o + 1 613 var i: i64 = 0 614 while ic_name[i] != (0 as u8) { if (ic_name[i] as i64) == RM_PIPE { out[o] = RM_BS as u8; out[o + 1] = RM_CH_p as u8; o = o + RM_ESC_LEN } else { out[o] = ic_name[i]; o = o + 1 } i = i + 1; if o + IC_LABELS_CAP >= cap { i = i + IC_NAME_CAP } } 615 out[o] = RM_PIPE as u8; o = o + 1; o = rm_cat(out, o, ic_installdir) 616 out[o] = RM_PIPE as u8; o = o + 1; o = rm_cati(out, o, ic_rec[IC_R_SIZE]) 617 out[o] = RM_PIPE as u8; o = o + 1; o = rm_cati(out, o, ic_rec[IC_R_BUILDID]) 618 out[o] = RM_PIPE as u8; o = o + 1; o = rm_cati(out, o, ic_rec[IC_R_UPDATED]) 619 out[o] = RM_PIPE as u8; o = o + 1; o = rm_cati(out, o, ic_rec[IC_R_PLAYED]) 620 out[o] = RM_PIPE as u8; o = o + 1; o = rm_cat(out, o, ic_lang) 621 out[o] = RM_PIPE as u8; o = o + 1; o = rm_cati(out, o, libidx) 622 out[o] = RM_PIPE as u8; o = o + 1; o = rm_cati(out, o, ic_rec[IC_R_ON_DISK]) 623 var c: i64 = 0 624 while c < IC_CLASS_N { ic_class_labels(c, ic_labels, IC_LABELS_CAP); out[o] = RM_PIPE as u8; o = o + 1; o = rm_cat(out, o, ic_labels); c = c + 1 } 625 out[o] = RM_PIPE as u8; o = o + 1; o = rm_cati(out, o, ic_rec[IC_R_FILES]) 626 out[o] = RM_PIPE as u8; o = o + 1; o = rm_cati(out, o, ic_rec[IC_R_DIRS]) 627 out[o] = RM_PIPE as u8; o = o + 1; o = rm_cati(out, o, ic_rec[IC_R_CAPPED]) 628 out[o] = RM_PIPE as u8; o = o + 1; o = rm_cati(out, o, ic_rec[IC_R_REVIEWS]) 629 out[o] = RM_PIPE as u8; o = o + 1; o = rm_cati(out, o, ic_rec[IC_R_SCORE]) 630 out[o] = RM_PIPE as u8; o = o + 1; o = rm_cati(out, o, ic_rec[IC_R_POS]) 631 out[o] = RM_PIPE as u8; o = o + 1; o = rm_cati(out, o, ic_rec[IC_R_NEG]) 632 out[o] = RM_PIPE as u8; o = o + 1; o = rm_cati(out, o, ic_rec[IC_R_BYTES]) 633 ic_assets_summary(ic_labels, IC_LABELS_CAP, IC_ASSETS_TOP) 634 out[o] = RM_PIPE as u8; o = o + 1; o = rm_cat(out, o, ic_labels) 635 out[o] = RM_LF as u8; o = o + 1 636 return o 637} 638// census one manifest: parse, locate the install tree, walk, fingerprint, tie out. Returns 1 row ready (in ic_rec etc.), 0 unreadable. 639func ic_census_app(lib: *u8, manifest_path: *u8, reviewdir: *u8) -> i64 { 640 if ic_manifest(manifest_path) == 0 { return 0 } 641 ic_walk_reset() 642 let tree: *u8 = (ic_pbuf as i64 + (IC_MAXDEPTH_MAX + 1) * IC_PATH_CAP) as *u8 643 var o: i64 = rm_cat(tree, 0, lib) 644 o = rm_cat(tree, o, "/steamapps/common/" as *u8) 645 o = rm_cat(tree, o, ic_installdir) 646 tree[o] = 0 as u8 647 var on_disk: i64 = 0 648 if ic_installdir[0] != (0 as u8) { if ic_walk(tree, 0) == 0 { on_disk = 1 } } 649 ic_rec[IC_R_ON_DISK] = on_disk 650 ic_rec[IC_R_FILES] = ic_walk_files 651 ic_rec[IC_R_DIRS] = ic_walk_dirs 652 ic_rec[IC_R_CAPPED] = ic_walk_capped 653 ic_rec[IC_R_BYTES] = ic_walk_bytes 654 ic_tieout(reviewdir, ic_rec[IC_R_APPID]) 655 return 1 656} 657// is this directory entry an app manifest? appmanifest_<digits>.acf 658func ic_is_manifest(name: *u8) -> i64 { 659 let pl: i64 = rm_slen(IC_MANIFEST_PREFIX) 660 let nl: i64 = rm_slen(name) 661 let sl: i64 = rm_slen(IC_MANIFEST_SUFFIX) 662 if nl <= pl + sl { return 0 } 663 var i: i64 = 0 664 while i < pl { if name[i] != IC_MANIFEST_PREFIX[i] { return 0 } i = i + 1 } 665 var j: i64 = 0 666 while j < sl { if name[nl - sl + j] != IC_MANIFEST_SUFFIX[j] { return 0 } j = j + 1 } 667 return 1 668}