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}