nx_mvault_fetch.nx source
↩ module page · 501 lines · 22746 B
1// nx_mvault_fetch.nx -- album/gallery ingest for the vault (a BunkrDownloader-class
2// capability, sovereign and data-driven).
3//
4// OPERATOR DECISION 2026-07-31: adapters are a DATA TABLE, not code. A new site is
5// an INSERT, not a rebuild. That is rule 11 (policy in data, never buried in code)
6// and it is also what keeps this from becoming the patch cascade that per-site
7// scraper code always becomes -- every host that needs a `if host == "..."` branch
8// is a redeploy, and the branch list is never complete.
9//
10// WHAT AN ADAPTER SAYS: album pages differ only in WHERE the links live. So a row
11// describes that shape --
12// link_tag/link_attr/link_pfx where the per-item links are on an ALBUM page
13// item_tag/item_attr where the DIRECT media url is on an ITEM page
14// Two-hop by design: album -> item page -> CDN url is the near-universal shape,
15// and a one-hop site is expressed by leaving the item rule empty.
16//
17// ★★★ ORDERING: this writes into the COLLECTION AXIS (nx_mvault_coll) so every
18// fetched file records WHICH ALBUM and WHICH SITE it came from AT INGEST TIME.
19// Ingesting first and grouping later is what produced the 07-30 backfill of
20// 35,615 mis-typed records; provenance captured at ingest is free, recovered
21// later it is a full-corpus pass.
22//
23// This module is the OFFLINE half -- adapter storage + extraction. It is pure and
24// fully gateable with no network, which is deliberate: the parsing rules are where
25// the bugs live, and they should be provable without a live site.
26// license_tier: ORIGINAL
27
28import "nx_syscalls.nx"
29import "nx_canon_cid.nx"
30import "nx_registry.nx"
31import "nx_mvault_record.nx" // mv_rec_field -- read adapter record fields
32import "nx_mvault_tag.nx" // mv_u_dec -- decimal into the JSON report
33import "nx_mvault_coll.nx" // the collection axis this ingest writes into
34import "nx_https_fetch_lib.nx" // hf_fetch (pages) / hf_fetch_to_file (media)
35
36const MVF_PFX: *u8 = "knowledge/store/mvfetch-\x00"
37const MVF_KP: *u8 = "ad:\x00"
38const MVF_IDS: *u8 = "ad:ids\x00"
39
40const MVF_REC_CAP: i64 = 4096
41const MVF_URL_CAP: i64 = 2048
42
43func mvf_strlen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
44func mvf_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){dst[off+i]=s[i]; i=i+1} return off+i }
45
46func mvf_lower(c: i64) -> i64 { if c >= 65 { if c <= 90 { return c + 32 } } return c }
47
48// case-insensitive: does buf[at..] begin with s?
49func mvf_at_ci(buf: *u8, n: i64, at: i64, s: *u8) -> i64 {
50 var i: i64 = 0
51 while s[i] != (0 as u8) {
52 if at + i >= n { return 0 }
53 if mvf_lower(buf[at + i] as i64) != mvf_lower(s[i] as i64) { return 0 }
54 i = i + 1
55 }
56 return 1
57}
58
59// does buf[at..at+len) begin with the NUL-terminated prefix?
60func mvf_has_pfx(buf: *u8, at: i64, len: i64, pfx: *u8) -> i64 {
61 var i: i64 = 0
62 while pfx[i] != (0 as u8) {
63 if i >= len { return 0 }
64 if buf[at + i] != pfx[i] { return 0 }
65 i = i + 1
66 }
67 return 1
68}
69
70// ---- URL HELPERS ----------------------------------------------------------
71// host of "scheme://host/path" -> out (NUL-terminated); returns length.
72func mvf_host_of(url: *u8, out: *u8) -> i64 {
73 let n: i64 = mvf_strlen(url)
74 var i: i64 = 0
75 var start: i64 = 0
76 while i + 2 < n {
77 if url[i] == (58 as u8) { if url[i+1] == (47 as u8) { if url[i+2] == (47 as u8) { start = i + 3; i = n } } }
78 i = i + 1
79 }
80 var o: i64 = 0
81 var j: i64 = start
82 while j < n {
83 let c: u8 = url[j]
84 if c == (47 as u8) { j = n } else {
85 if c == (58 as u8) { j = n } else { out[o] = c; o = o + 1; j = j + 1 }
86 }
87 }
88 out[o] = 0 as u8
89 return o
90}
91
92// scheme://host of a url -> out; returns length.
93func mvf_origin_of(url: *u8, out: *u8) -> i64 {
94 let n: i64 = mvf_strlen(url)
95 var start: i64 = 0
96 var i: i64 = 0
97 while i + 2 < n {
98 if url[i] == (58 as u8) { if url[i+1] == (47 as u8) { if url[i+2] == (47 as u8) { start = i + 3; i = n } } }
99 i = i + 1
100 }
101 var o: i64 = 0
102 var j: i64 = 0
103 while j < start { out[o] = url[j]; o = o + 1; j = j + 1 }
104 j = start
105 while j < n { if url[j] == (47 as u8) { j = n } else { out[o] = url[j]; o = o + 1; j = j + 1 } }
106 out[o] = 0 as u8
107 return o
108}
109
110// Resolve href against a base url. Absolute (has "://") passes through;
111// root-relative ("/f/x") gets scheme://host; anything else is refused (0) rather
112// than guessed -- a mis-resolved url silently downloads the WRONG file, so a
113// shape we do not understand must not be turned into a plausible-looking one.
114func mvf_resolve(base: *u8, href: *u8, out: *u8, cap: i64) -> i64 {
115 let hn: i64 = mvf_strlen(href)
116 if hn == 0 { return 0 }
117 var i: i64 = 0
118 var abs: i64 = 0
119 while i + 2 < hn { if href[i]==(58 as u8) { if href[i+1]==(47 as u8) { if href[i+2]==(47 as u8) { abs=1; i=hn } } } i = i + 1 }
120 if abs == 1 {
121 if hn + 1 > cap { return 0 - 1 }
122 var k: i64 = 0
123 while k < hn { out[k] = href[k]; k = k + 1 }
124 out[hn] = 0 as u8
125 return hn
126 }
127 if href[0] != (47 as u8) { return 0 }
128 let org: *u8 = sys_mmap(MVF_URL_CAP)
129 let ol: i64 = mvf_origin_of(base, org)
130 if ol + hn + 1 > cap { sys_munmap(org, MVF_URL_CAP); return 0 - 1 }
131 var o: i64 = 0
132 while o < ol { out[o] = org[o]; o = o + 1 }
133 var q: i64 = 0
134 while q < hn { out[o] = href[q]; o = o + 1; q = q + 1 }
135 out[o] = 0 as u8
136 sys_munmap(org, MVF_URL_CAP)
137 return o
138}
139
140// ---- EXTRACTION -----------------------------------------------------------
141// Pull every <tag ... attr="value"> whose value starts with pfx (pfx "" = any),
142// newline-separated, into out. Returns bytes written, or -1 if it does not fit.
143//
144// REFUSES rather than truncating, same contract as reg_index after the
145// 2026-07-31 root fix: a half-read album that looks complete is how a downloader
146// silently skips files and reports success.
147//
148// Deliberately a TAG+ATTRIBUTE scanner, not a regex over the whole document:
149// the adapter names the tag and attribute, so the rule stays legible as DATA and
150// cannot accidentally match a url mentioned in prose or a script blob.
151func mvf_extract(html: *u8, n: i64, tag: *u8, attr: *u8, pfx: *u8, out: *u8, cap: i64) -> i64 {
152 var o: i64 = 0
153 var i: i64 = 0
154 while i < n {
155 if html[i] == (60 as u8) {
156 if mvf_at_ci(html, n, i + 1, tag) == 1 {
157 let tl: i64 = mvf_strlen(tag)
158 var p: i64 = i + 1 + tl
159 var okb: i64 = 0
160 if p < n { if html[p]==(32 as u8) { okb=1 } if html[p]==(9 as u8) { okb=1 } if html[p]==(10 as u8) { okb=1 } if html[p]==(13 as u8) { okb=1 } }
161 if okb == 1 {
162 // scan attributes until the tag closes
163 var g: i64 = 1
164 while g == 1 {
165 if p >= n { g = 0 } else {
166 if html[p] == (62 as u8) { g = 0 } else {
167 if mvf_at_ci(html, n, p, attr) == 1 {
168 // NOTE: NishiLang has no `break`, and the first cut of this used
169 // `q = n + 1` as a loop-exit sentinel -- which exits the loop but
170 // DESTROYS the very index the next step needs, so every extraction
171 // returned 0. Caught by the gate before this shipped. Skip whitespace
172 // with an explicit flag and leave the cursor where it landed.
173 var q: i64 = p + mvf_strlen(attr)
174 var gq: i64 = 1
175 while gq == 1 { if q >= n { gq = 0 } else { if html[q]==(32 as u8) { q = q + 1 } else { gq = 0 } } }
176 if q <= n { if q < n { if html[q] == (61 as u8) {
177 var v: i64 = q + 1
178 var gv: i64 = 1
179 while gv == 1 { if v >= n { gv = 0 } else { if html[v]==(32 as u8) { v = v + 1 } else { gv = 0 } } }
180 if v <= n {
181 var quote: i64 = 0
182 if v < n { if html[v]==(34 as u8) { quote=34; v=v+1 } else { if html[v]==(39 as u8) { quote=39; v=v+1 } } }
183 var e: i64 = v
184 var g2: i64 = 1
185 while g2 == 1 {
186 if e >= n { g2 = 0 } else {
187 if quote != 0 { if (html[e] as i64) == quote { g2 = 0 } else { e = e + 1 } }
188 else { if html[e]==(32 as u8) { g2=0 } else { if html[e]==(62 as u8) { g2=0 } else { e=e+1 } } }
189 }
190 }
191 let vlen: i64 = e - v
192 if vlen > 0 { if mvf_has_pfx(html, v, vlen, pfx) == 1 {
193 if o + vlen + 1 > cap { return 0 - 1 }
194 var w: i64 = 0
195 while w < vlen { out[o] = html[v + w]; o = o + 1; w = w + 1 }
196 out[o] = 10 as u8; o = o + 1
197 } }
198 p = e
199 }
200 } } }
201 }
202 p = p + 1
203 }
204 }
205 }
206 i = p
207 }
208 }
209 }
210 i = i + 1
211 }
212 return o
213}
214
215// ---- ADAPTER TABLE (the data) ---------------------------------------------
216func mvf_adapter_rec(out: *u8, host: *u8, link_tag: *u8, link_attr: *u8, link_pfx: *u8,
217 item_tag: *u8, item_attr: *u8, ua: *u8) -> i64 {
218 let keys: *i64 = sys_mmap(8 * 7) as *i64
219 let vals: *i64 = sys_mmap(8 * 7) as *i64
220 let k_host: *u8 = "host" as *u8
221 let k_lt: *u8 = "link_tag" as *u8
222 let k_la: *u8 = "link_attr" as *u8
223 let k_lp: *u8 = "link_pfx" as *u8
224 let k_it: *u8 = "item_tag" as *u8
225 let k_ia: *u8 = "item_attr" as *u8
226 let k_ua: *u8 = "ua" as *u8
227 keys[0]=k_host as i64; vals[0]=host as i64
228 keys[1]=k_lt as i64; vals[1]=link_tag as i64
229 keys[2]=k_la as i64; vals[2]=link_attr as i64
230 keys[3]=k_lp as i64; vals[3]=link_pfx as i64
231 keys[4]=k_it as i64; vals[4]=item_tag as i64
232 keys[5]=k_ia as i64; vals[5]=item_attr as i64
233 keys[6]=k_ua as i64; vals[6]=ua as i64
234 let rl: i64 = canon_encode(keys, vals, 7, out)
235 sys_munmap(keys as *u8, 8 * 7)
236 sys_munmap(vals as *u8, 8 * 7)
237 return rl
238}
239
240func mvf_adapter_put_pfx(prefix: *u8, host: *u8, link_tag: *u8, link_attr: *u8, link_pfx: *u8,
241 item_tag: *u8, item_attr: *u8, ua: *u8) -> i64 {
242 let rec: *u8 = sys_mmap(MVF_REC_CAP)
243 let rl: i64 = mvf_adapter_rec(rec, host, link_tag, link_attr, link_pfx, item_tag, item_attr, ua)
244 let rc: i64 = reg_put(prefix, MVF_KP, MVF_IDS, host, rec, rl)
245 sys_munmap(rec, MVF_REC_CAP)
246 return rc
247}
248
249// Find the adapter for a host, trying the most specific first and then dropping
250// leading labels: img.bunkr.si -> bunkr.si -> si. So one row covers a whole
251// family of mirror domains, which is exactly how these hosts multiply, WITHOUT
252// the row having to enumerate them.
253func mvf_adapter_find_pfx(prefix: *u8, host: *u8, ptrout: *i64, lenout: *i64) -> i64 {
254 let n: i64 = mvf_strlen(host)
255 var start: i64 = 0
256 while start < n {
257 let cand: *u8 = ((host as i64) + start) as *u8
258 if reg_get(prefix, MVF_KP, cand, ptrout, lenout) == 1 { return 1 }
259 // drop to just past the next dot
260 var d: i64 = start
261 var moved: i64 = 0
262 while d < n { if host[d] == (46 as u8) { start = d + 1; moved = 1; d = n } else { d = d + 1 } }
263 if moved == 0 { start = n }
264 }
265 return 0
266}
267
268func mvf_adapter_list_pfx(prefix: *u8, out: *u8, cap: i64) -> i64 {
269 return reg_index(prefix, MVF_IDS, out, cap)
270}
271
272// ---- ONLINE HALF: album -> vault ------------------------------------------
273
274// Last path segment of a url, SANITISED, into out. Refuses (0) a name that is
275// empty or carries a path separator or ".." -- a remote server chooses this
276// string, so it is untrusted input at a boundary (rule 12) and a crafted one
277// would otherwise write outside the destination directory.
278func mvf_basename(url: *u8, out: *u8, cap: i64) -> i64 {
279 let n: i64 = mvf_strlen(url)
280 var start: i64 = 0
281 var i: i64 = 0
282 while i < n { if url[i] == (47 as u8) { start = i + 1 } i = i + 1 }
283 var e: i64 = start
284 while e < n { if url[e] == (63 as u8) { e = n } else { e = e + 1 } }
285 var end: i64 = start
286 var j: i64 = start
287 while j < n { if url[j] == (63 as u8) { j = n } else { end = j + 1; j = j + 1 } }
288 let ln: i64 = end - start
289 if ln <= 0 { return 0 }
290 if ln + 1 > cap { return 0 }
291 var o: i64 = 0
292 while o < ln {
293 let c: u8 = url[start + o]
294 if c == (47 as u8) { return 0 }
295 if c == (92 as u8) { return 0 }
296 out[o] = c
297 o = o + 1
298 }
299 out[ln] = 0 as u8
300 if ln >= 2 { if out[0] == (46 as u8) { if out[1] == (46 as u8) { return 0 } } }
301 return ln
302}
303
304// Read a NUL-terminated field out of an adapter record.
305func mvf_field(rec: *u8, rl: i64, key: *u8, out: *u8, cap: i64) -> i64 {
306 let po: *i64 = sys_mmap(16) as *i64
307 let lo: *i64 = sys_mmap(16) as *i64
308 if mv_rec_field(rec, rl, key, mvf_strlen(key), po, lo) != 1 { out[0] = 0 as u8; return 0 }
309 let n: i64 = lo[0]
310 if n + 1 > cap { out[0] = 0 as u8; return 0 }
311 let src: *u8 = po[0] as *u8
312 var i: i64 = 0
313 while i < n { out[i] = src[i]; i = i + 1 }
314 out[n] = 0 as u8
315 return n
316}
317
318const MVF_PAGE_CAP: i64 = 8388608
319const MVF_LINKS_CAP: i64 = 1048576
320const MVF_ITEMS_CAP: i64 = 1048576
321const MVF_MAX_ITEMS: i64 = 4096
322
323// Ingest one album URL into the vault, recording its SITE and ALBUM provenance
324// at ingest time.
325//
326// ★ PROVENANCE IS CAPTURED HERE, NOT LATER. Recovering "which album was this"
327// after the fact means a full-corpus pass -- that is exactly what the 07-30
328// classifier backfill cost (35,615 records). Captured at ingest it is free.
329//
330// Returns items ingested, or negative: -1 no adapter, -2 album fetch failed,
331// -3 no links found. Writes a JSON summary into report.
332func mvf_fetch_album_pfx(cpfx: *u8, store: i64, album_url: *u8, dest_dir: *u8,
333 report: *u8, rcap: i64) -> i64 {
334 let host: *u8 = sys_mmap(512)
335 mvf_host_of(album_url, host)
336
337 let po: *i64 = sys_mmap(16) as *i64
338 let lo: *i64 = sys_mmap(16) as *i64
339 if mvf_adapter_find(host, po, lo) != 1 { return 0 - 1 }
340 let rec: *u8 = po[0] as *u8
341 let rl: i64 = lo[0]
342
343 let l_tag: *u8 = sys_mmap(128)
344 let l_attr: *u8 = sys_mmap(128)
345 let l_pfx: *u8 = sys_mmap(256)
346 let i_tag: *u8 = sys_mmap(128)
347 let i_attr: *u8 = sys_mmap(128)
348 mvf_field(rec, rl, "link_tag" as *u8, l_tag, 128)
349 mvf_field(rec, rl, "link_attr" as *u8, l_attr, 128)
350 mvf_field(rec, rl, "link_pfx" as *u8, l_pfx, 256)
351 mvf_field(rec, rl, "item_tag" as *u8, i_tag, 128)
352 mvf_field(rec, rl, "item_attr" as *u8, i_attr, 128)
353
354 // ---- album page ----
355 let page: *u8 = sys_mmap(MVF_PAGE_CAP)
356 let pn: i64 = hf_fetch(store, album_url, 0, 0, page, MVF_PAGE_CAP)
357 if pn <= 0 { return 0 - 2 }
358 var boff: i64 = hf_body_off(page, pn)
359 if boff < 0 { boff = 0 }
360 let body: *u8 = ((page as i64) + boff) as *u8
361
362 let links: *u8 = sys_mmap(MVF_LINKS_CAP)
363 let ln: i64 = mvf_extract(body, pn - boff, l_tag, l_attr, l_pfx, links, MVF_LINKS_CAP)
364 if ln <= 0 { return 0 - 3 }
365
366 // ---- declare the two collections this album's files belong to ----
367 let site_col: *u8 = sys_mmap(256)
368 let alb_col: *u8 = sys_mmap(256)
369 mvc_declare_pfx(cpfx, "site" as *u8, host, host, host, album_url, "-" as *u8, site_col)
370 let akey: *u8 = sys_mmap(1024)
371 if mvf_basename(album_url, akey, 1024) == 0 { mvf_cat(akey, 0, "album" as *u8); akey[5] = 0 as u8 }
372 mvc_declare_pfx(cpfx, "album" as *u8, host, akey, akey, album_url, site_col, alb_col)
373
374 let items: *u8 = sys_mmap(MVF_ITEMS_CAP)
375 var io: i64 = 0
376 var got: i64 = 0
377 var failed: i64 = 0
378 var truncated: i64 = 0
379
380 let one: *u8 = sys_mmap(MVF_URL_CAP)
381 let direct: *u8 = sys_mmap(MVF_URL_CAP)
382 let ipage: *u8 = sys_mmap(MVF_PAGE_CAP)
383 let dbuf: *u8 = sys_mmap(MVF_LINKS_CAP)
384 let path: *u8 = sys_mmap(2048)
385 let base: *u8 = sys_mmap(1024)
386 let cid: *u8 = sys_mmap(256)
387 let szbox: *i64 = sys_mmap(16) as *i64
388 let stbox: *i64 = sys_mmap(16) as *i64
389
390 var ls: i64 = 0
391 var i: i64 = 0
392 while i <= ln {
393 var eol: i64 = 0
394 if i == ln { eol = 1 } else { if links[i] == (10 as u8) { eol = 1 } }
395 if eol == 1 {
396 let hl: i64 = i - ls
397 if hl > 0 {
398 if got >= MVF_MAX_ITEMS { truncated = 1 } else {
399 let href: *u8 = sys_mmap(MVF_URL_CAP)
400 var q: i64 = 0
401 while q < hl { href[q] = links[ls + q]; q = q + 1 }
402 href[hl] = 0 as u8
403
404 var ok: i64 = 1
405 if mvf_resolve(album_url, href, one, MVF_URL_CAP) <= 0 { ok = 0 }
406
407 // second hop: the item PAGE carries the direct CDN url
408 if ok == 1 { if i_tag[0] != (0 as u8) {
409 let inn: i64 = hf_fetch(store, one, 0, 0, ipage, MVF_PAGE_CAP)
410 if inn <= 0 { ok = 0 } else {
411 var ibo: i64 = hf_body_off(ipage, inn)
412 if ibo < 0 { ibo = 0 }
413 let dn: i64 = mvf_extract(((ipage as i64) + ibo) as *u8, inn - ibo, i_tag, i_attr, "" as *u8, dbuf, MVF_LINKS_CAP)
414 if dn <= 0 { ok = 0 } else {
415 // take the FIRST extracted direct url
416 var cut: i64 = 0
417 var z: i64 = 0
418 while z < dn { if dbuf[z] == (10 as u8) { cut = z; z = dn } else { z = z + 1 } }
419 if cut == 0 { cut = dn }
420 let tmp: *u8 = sys_mmap(MVF_URL_CAP)
421 var w: i64 = 0
422 while w < cut { tmp[w] = dbuf[w]; w = w + 1 }
423 tmp[cut] = 0 as u8
424 if mvf_resolve(one, tmp, direct, MVF_URL_CAP) <= 0 { ok = 0 }
425 }
426 }
427 } else {
428 var c2: i64 = 0
429 while one[c2] != (0 as u8) { direct[c2] = one[c2]; c2 = c2 + 1 }
430 direct[c2] = 0 as u8
431 } }
432
433 // ---- stream to disk, then CID the FILE (never the buffer) ----
434 if ok == 1 {
435 if mvf_basename(direct, base, 1024) == 0 { ok = 0 } else {
436 var pO: i64 = mvf_cat(path, 0, dest_dir)
437 path[pO] = 47 as u8; pO = pO + 1
438 pO = mvf_cat(path, pO, base)
439 path[pO] = 0 as u8
440 let wfd: i64 = sys_openat_wr(path, 420)
441 if wfd < 0 { ok = 0 } else {
442 let wn: i64 = hf_fetch_to_file(store, direct, 0, 0, wfd, 0, stbox)
443 sys_close(wfd)
444 if wn <= 0 { ok = 0; sys_unlinkat(path) }
445 }
446 }
447 }
448 if ok == 1 {
449 if cid_of_file_chunk(path, 4194304, szbox, cid) <= 0 { ok = 0 }
450 }
451 if ok == 1 {
452 let cl: i64 = mvf_strlen(cid)
453 if io + cl + 2 < MVF_ITEMS_CAP {
454 var y: i64 = 0
455 while y < cl { items[io] = cid[y]; io = io + 1; y = y + 1 }
456 items[io] = 10 as u8; io = io + 1
457 got = got + 1
458 }
459 } else { failed = failed + 1 }
460 }
461 }
462 ls = i + 1
463 }
464 i = i + 1
465 }
466
467 // ---- one segment per collection, both directions of provenance ----
468 var linked: i64 = 0
469 if io > 0 {
470 linked = mvc_batch_add_pfx(cpfx, alb_col, items, io)
471 mvc_batch_add_pfx(cpfx, site_col, items, io)
472 }
473
474 var ro: i64 = mvf_cat(report, 0, "{\"album\":\"" as *u8)
475 ro = mvf_cat(report, ro, alb_col)
476 ro = mvf_cat(report, ro, "\",\"site\":\"" as *u8)
477 ro = mvf_cat(report, ro, site_col)
478 ro = mvf_cat(report, ro, "\",\"found\":" as *u8)
479 ro = mv_u_dec(report, ro, mvc_count_lines(links, ln))
480 ro = mvf_cat(report, ro, ",\"ingested\":" as *u8)
481 ro = mv_u_dec(report, ro, got)
482 ro = mvf_cat(report, ro, ",\"linked\":" as *u8)
483 ro = mv_u_dec(report, ro, linked)
484 ro = mvf_cat(report, ro, ",\"failed\":" as *u8)
485 ro = mv_u_dec(report, ro, failed)
486 // NEVER a silent cap: if the envelope truncated the album, SAY SO in the
487 // report, or a partial ingest reads as a complete one.
488 ro = mvf_cat(report, ro, ",\"truncated\":" as *u8)
489 ro = mv_u_dec(report, ro, truncated)
490 ro = mvf_cat(report, ro, "}\n" as *u8)
491 report[ro] = 0 as u8
492 return got
493}
494
495// ---- production-prefix wrappers -------------------------------------------
496func mvf_adapter_put(host: *u8, link_tag: *u8, link_attr: *u8, link_pfx: *u8, item_tag: *u8, item_attr: *u8, ua: *u8) -> i64 {
497 return mvf_adapter_put_pfx(MVF_PFX, host, link_tag, link_attr, link_pfx, item_tag, item_attr, ua)
498}
499func mvf_adapter_find(host: *u8, ptrout: *i64, lenout: *i64) -> i64 { return mvf_adapter_find_pfx(MVF_PFX, host, ptrout, lenout) }
500func mvf_adapter_list(out: *u8, cap: i64) -> i64 { return mvf_adapter_list_pfx(MVF_PFX, out, cap) }
501func mvf_fetch_album(store: i64, album_url: *u8, dest_dir: *u8, report: *u8, rcap: i64) -> i64 { return mvf_fetch_album_pfx(MVC_PFX, store, album_url, dest_dir, report, rcap) }