nx_synthroom_lib.nx source
↩ module page · 443 lines · 20445 B
1// nx_synthroom_lib.nx -- ONE CONTRACT for the synth-human room protocol (/compare/synthroom rung SR1).
2//
3// THE WIRE, THE TABLE AND THE DERIVATION live here exactly once, imported by the writer
4// (nx_synthroom append), the reader (nx_synthroom replay) and the gate. A room protocol is a producer and
5// a consumer of one wire, and this estate has measured what it costs when each is written separately:
6// a producer and a consumer each correct in isolation can still disagree on the wire.
7//
8// WHAT A ROOM IS. Chaturbate's own architecture is an event bus -- tip, chatMessage, room status, user
9// interactions -- with tip menus mapping token amounts to actions and tip-activated actuators behind
10// them. This lib is that shape with the human replaced by a director: every interaction is ONE row in an
11// APPEND-ONLY log, a tip menu is DATA, and the intent stream is DERIVED from the log so the same log
12// always yields the same intents (bit-identical replay is the SR1 done rule).
13//
14// THREE RULES THE WIRE ENFORCES BY CONSTRUCTION:
15// 1. A caller-supplied NONCE per event. A retry after a dropped response is the estate's most-measured
16// hazard (a 503 often LANDED); an append is not idempotent, so the nonce is what makes a blind retry
17// safe: a duplicate nonce is REFUSED by name, never double-applied.
18// 2. UNKNOWN IS ITS OWN BUCKET. An unknown verb is refused on the way in, and an unparseable row is
19// refused on the way out with its row index -- a replay never skips a row it cannot read, because the
20// count it publishes would then be a count of what it understood, not of what happened.
21// 3. A tip below every menu threshold maps to the DECLARED fallback intent, never to nothing. A menu
22// with no fallback is refused at parse time so the hole is found before the first tip, not after.
23//
24// license_tier: ORIGINAL No hw writes (Rule 26).
25import "nx_syscalls.nx"
26
27// ---------------------------------------------------------------------------------------------
28// THE WIRE. Row kinds and the verb vocabulary. Every literal lives here once; the writer composes
29// rows from these and the reader matches against the same bytes.
30const SR_ROW_EV: *u8 = "ev" as *u8
31const SR_ROW_IN: *u8 = "in" as *u8
32const SR_ROW_MENU: *u8 = "menu" as *u8
33const SR_ROW_FALLBACK: *u8 = "fallback" as *u8
34const SR_EMPTY: *u8 = "-" as *u8
35
36const SR_V_TIP: i64 = 0
37const SR_V_SAY: i64 = 1
38const SR_V_ENTER: i64 = 2
39const SR_V_LEAVE: i64 = 3
40const SR_V_GESTURE: i64 = 4
41const SR_V_COMMAND: i64 = 5
42const SR_V_TOY: i64 = 6
43const SR_V_N: i64 = 7
44const SR_V_NONE: i64 = 0 - 1
45
46// THE DECISIONS. Exactly one per call; every refusal names its own rule.
47const SR_OK: i64 = 0
48const SR_D_BADVERB: i64 = 1
49const SR_D_BADNONCE: i64 = 2
50const SR_D_BADACTOR: i64 = 3
51const SR_D_BADARG: i64 = 4
52const SR_D_DUPNONCE: i64 = 5
53const SR_D_WRITEFAIL: i64 = 6
54const SR_D_UNPARSED: i64 = 7
55const SR_D_OVERFLOW: i64 = 8
56const SR_D_NOMENU: i64 = 9
57const SR_D_NOFALLBACK: i64 = 10
58const SR_D_BADMENU: i64 = 11
59const SR_D_NOLOG: i64 = 12
60
61// Event row: ev|nonce|ts|verb|actor|a1|a2 (7 fields). Intent row: in|seq|intent|actor|amount|nonce (6).
62const SR_EV_FIELDS: i64 = 7
63const SR_IN_FIELDS: i64 = 6
64const SR_EV_F_KIND: i64 = 0
65const SR_EV_F_NONCE: i64 = 1
66const SR_EV_F_TS: i64 = 2
67const SR_EV_F_VERB: i64 = 3
68const SR_EV_F_ACTOR: i64 = 4
69const SR_EV_F_A1: i64 = 5
70const SR_EV_F_A2: i64 = 6
71// Menu row: menu|item|min_tokens|intent (4). Fallback row: fallback|intent (2).
72const SR_MENU_FIELDS: i64 = 4
73const SR_FALLBACK_FIELDS: i64 = 2
74const SR_MENU_F_ITEM: i64 = 1
75const SR_MENU_F_MIN: i64 = 2
76const SR_MENU_F_INTENT: i64 = 3
77// A parsed menu row is three i64 handles: item ptr, min tokens, intent ptr.
78const SR_M_ITEM: i64 = 0
79const SR_M_MIN: i64 = 1
80const SR_M_INTENT: i64 = 2
81const SR_M_STRIDE: i64 = 3
82const SR_I64_BYTES: i64 = 8
83// sr_split hands back at most this many fields; a row with more is over-count and refused.
84const SR_MAXF: i64 = 8
85
86const SR_CH_NL: i64 = 10
87const SR_CH_HASH: i64 = 35
88const SR_CH_MINUS: i64 = 45
89const SR_CH_ZERO: i64 = 48
90const SR_CH_NINE: i64 = 57
91const SR_CH_PIPE: i64 = 124
92const SR_B10: i64 = 10
93const SR_I64_DIGITS: i64 = 20
94// One event row is bounded and the bound is DERIVED: 7 fields, 6 pipes, one newline, and the two
95// numeric fields at most 20 characters each. Callers hand in SR_LINE and the composer refuses a buffer
96// that cannot hold the row rather than truncating it into a different row.
97const SR_SEPS: i64 = 7
98const SR_LINE: i64 = 1024
99const SR_SCRATCH: i64 = 64
100
101func sr_slen(p: *u8) -> i64 { var n: i64 = 0; while p[n] != (0 as u8) { n = n + 1 } return n }
102
103func sr_streq(a: *u8, b: *u8) -> i64 {
104 let n: i64 = sr_slen(a)
105 if n != sr_slen(b) { return 0 }
106 var i: i64 = 0
107 while i < n { if a[i] != b[i] { return 0 } i = i + 1 }
108 return 1
109}
110
111func sr_cat(d: *u8, o: i64, s: *u8) -> i64 {
112 var i: i64 = 0
113 var p: i64 = o
114 while s[i] != (0 as u8) { d[p] = s[i]; p = p + 1; i = i + 1 }
115 return p
116}
117
118func sr_catnum(d: *u8, o: i64, v: i64) -> i64 {
119 var p: i64 = o
120 var x: i64 = v
121 if x < 0 { d[p] = SR_CH_MINUS as u8; p = p + 1; x = 0 - x }
122 var div: i64 = 1
123 var t: i64 = x
124 while t >= SR_B10 { div = div * SR_B10; t = t / SR_B10 }
125 while div > 0 { d[p] = ((x / div) % SR_B10 + SR_CH_ZERO) as u8; p = p + 1; div = div / SR_B10 }
126 return p
127}
128
129// "12345" -> 12345, or -1 when the text is not a non-negative decimal. STRICT: any non-digit rejects,
130// because a lenient parse turns a typo into a real amount.
131func sr_atoi_strict(s: *u8) -> i64 {
132 let n: i64 = sr_slen(s)
133 if n == 0 { return 0 - 1 }
134 if n > SR_I64_DIGITS { return 0 - 1 }
135 var v: i64 = 0
136 var i: i64 = 0
137 while i < n {
138 let c: i64 = s[i] as i64
139 if c < SR_CH_ZERO { return 0 - 1 }
140 if c > SR_CH_NINE { return 0 - 1 }
141 v = v * SR_B10 + (c - SR_CH_ZERO)
142 i = i + 1
143 }
144 return v
145}
146
147// A field may carry neither the separator nor a row terminator, or the row it lands in is a different row.
148func sr_field_clean(s: *u8) -> i64 {
149 var i: i64 = 0
150 while s[i] != (0 as u8) {
151 let c: i64 = s[i] as i64
152 if c == SR_CH_PIPE { return 0 }
153 if c == SR_CH_NL { return 0 }
154 i = i + 1
155 }
156 if i == 0 { return 0 }
157 return 1
158}
159
160// Literal substring search over a COUNTED buffer (a log is read whole and may contain a NUL).
161func sr_has(buf: *u8, n: i64, pat: *u8) -> i64 {
162 let m: i64 = sr_slen(pat)
163 if m == 0 { return 1 }
164 var i: i64 = 0
165 while i + m <= n {
166 var j: i64 = 0
167 var ok: i64 = 1
168 while j < m { if buf[i+j] != pat[j] { ok = 0; j = m } else { j = j + 1 } }
169 if ok == 1 { return 1 }
170 i = i + 1
171 }
172 return 0
173}
174
175// Split ONE row in place on the pipe: writes NUL over each pipe, fills fld[] with field pointers, and
176// returns the field count. Refuses (-1) a row with more fields than maxf rather than folding the surplus
177// into the last field -- an over-count row is a lie wearing the shape of a note. The caller guarantees
178// line[len] is writable: a row from sys_read_file always is (its mapping is NUL-terminated and padded).
179func sr_split(line: *u8, len: i64, fld: *i64, maxf: i64) -> i64 {
180 var nf: i64 = 0
181 var start: i64 = 0
182 var i: i64 = 0
183 while i <= len {
184 var atend: i64 = 0
185 if i == len { atend = 1 } else { if line[i] == (SR_CH_PIPE as u8) { atend = 1 } }
186 if atend == 1 {
187 if nf >= maxf { return 0 - 1 }
188 fld[nf] = (line as i64) + start
189 nf = nf + 1
190 if i < len { line[i] = 0 as u8 }
191 start = i + 1
192 }
193 i = i + 1
194 }
195 line[len] = 0 as u8
196 return nf
197}
198
199func sr_verb_name(v: i64) -> *u8 {
200 if v == SR_V_TIP { return "tip" as *u8 }
201 if v == SR_V_SAY { return "say" as *u8 }
202 if v == SR_V_ENTER { return "enter" as *u8 }
203 if v == SR_V_LEAVE { return "leave" as *u8 }
204 if v == SR_V_GESTURE { return "gesture" as *u8 }
205 if v == SR_V_COMMAND { return "command" as *u8 }
206 if v == SR_V_TOY { return "toy" as *u8 }
207 return "unknown" as *u8
208}
209
210func sr_verb_of(s: *u8) -> i64 {
211 var v: i64 = 0
212 while v < SR_V_N {
213 if sr_streq(s, sr_verb_name(v)) == 1 { return v }
214 v = v + 1
215 }
216 return SR_V_NONE
217}
218
219func sr_decision_name(d: i64) -> *u8 {
220 if d == SR_OK { return "OK" as *u8 }
221 if d == SR_D_BADVERB { return "REFUSED-BAD-VERB" as *u8 }
222 if d == SR_D_BADNONCE { return "REFUSED-BAD-NONCE" as *u8 }
223 if d == SR_D_BADACTOR { return "REFUSED-BAD-ACTOR" as *u8 }
224 if d == SR_D_BADARG { return "REFUSED-BAD-ARG" as *u8 }
225 if d == SR_D_DUPNONCE { return "REFUSED-DUPLICATE-NONCE" as *u8 }
226 if d == SR_D_WRITEFAIL { return "WRITE-FAILED" as *u8 }
227 if d == SR_D_UNPARSED { return "REFUSED-UNPARSED-ROW" as *u8 }
228 if d == SR_D_OVERFLOW { return "REFUSED-OUTPUT-OVERFLOW" as *u8 }
229 if d == SR_D_NOMENU { return "REFUSED-NO-MENU" as *u8 }
230 if d == SR_D_NOFALLBACK { return "REFUSED-MENU-WITHOUT-FALLBACK" as *u8 }
231 if d == SR_D_BADMENU { return "REFUSED-BAD-MENU-ROW" as *u8 }
232 if d == SR_D_NOLOG { return "REFUSED-NO-LOG" as *u8 }
233 return "REFUSED-UNCLASSIFIED" as *u8
234}
235
236func sr_decision_why(d: i64) -> *u8 {
237 if d == SR_OK { return "appended, or replayed, exactly as asked" as *u8 }
238 if d == SR_D_BADVERB { return "the verb is not one of tip say enter leave gesture command toy -- unknown is its own bucket, never a default" as *u8 }
239 if d == SR_D_BADNONCE { return "the nonce is empty or carries a separator -- a nonce is the caller's identity for this event and must be a clean token" as *u8 }
240 if d == SR_D_BADACTOR { return "the actor is empty or carries a separator" as *u8 }
241 if d == SR_D_BADARG { return "an argument carries a separator -- it would land in a different field of a different row" as *u8 }
242 if d == SR_D_DUPNONCE { return "this nonce is already in the log -- a retry after a dropped response landed; nothing appended, nothing double-applied" as *u8 }
243 if d == SR_D_WRITEFAIL { return "the row could not be appended -- the log is UNCHANGED" as *u8 }
244 if d == SR_D_UNPARSED { return "a log row is not a well-formed event row -- replay refuses rather than skipping, so its count is a count of what happened" as *u8 }
245 if d == SR_D_OVERFLOW { return "the intent stream does not fit the buffer offered -- refused rather than truncated" as *u8 }
246 if d == SR_D_NOMENU { return "the menu conf could not be read" as *u8 }
247 if d == SR_D_NOFALLBACK { return "the menu declares no fallback intent -- a tip below every threshold would map to nothing" as *u8 }
248 if d == SR_D_BADMENU { return "a menu row is malformed: unknown kind, wrong field count, or a non-numeric threshold" as *u8 }
249 if d == SR_D_NOLOG { return "the log could not be read" as *u8 }
250 return "unclassified" as *u8
251}
252
253// ---------------------------------------------------------------------------------------------
254// THE TABLE. Parse a menu conf IN PLACE (the buffer is mutated: pipes become NULs) into rows of
255// SR_M_STRIDE handles. Returns the row count, or -1 with the reason in why[0]. A menu without a fallback
256// is refused HERE, at parse time, so the hole is found before the first tip.
257func sr_menu_parse(buf: *u8, n: i64, tab: *i64, cap_rows: i64, fallback_out: *i64, why: *i64) -> i64 {
258 why[0] = SR_OK
259 fallback_out[0] = 0
260 let fld: *i64 = sys_mmap(SR_MAXF * SR_I64_BYTES) as *i64
261 var rows: i64 = 0
262 var p: i64 = 0
263 while p < n {
264 var e: i64 = p
265 while e < n { if buf[e] == (SR_CH_NL as u8) { break } e = e + 1 }
266 let line: *u8 = (buf as i64 + p) as *u8
267 let len: i64 = e - p
268 p = e + 1
269 var skip: i64 = 0
270 if len == 0 { skip = 1 } else { if line[0] == (SR_CH_HASH as u8) { skip = 1 } }
271 if skip == 0 {
272 let nf: i64 = sr_split(line, len, fld, SR_MAXF)
273 if nf < 0 { why[0] = SR_D_BADMENU; return 0 - 1 }
274 let kind: *u8 = fld[0] as *u8
275 if sr_streq(kind, SR_ROW_MENU) == 1 {
276 if nf != SR_MENU_FIELDS { why[0] = SR_D_BADMENU; return 0 - 1 }
277 let mn: i64 = sr_atoi_strict(fld[SR_MENU_F_MIN] as *u8)
278 if mn < 0 { why[0] = SR_D_BADMENU; return 0 - 1 }
279 if sr_field_clean(fld[SR_MENU_F_ITEM] as *u8) == 0 { why[0] = SR_D_BADMENU; return 0 - 1 }
280 if sr_field_clean(fld[SR_MENU_F_INTENT] as *u8) == 0 { why[0] = SR_D_BADMENU; return 0 - 1 }
281 if rows >= cap_rows { why[0] = SR_D_OVERFLOW; return 0 - 1 }
282 tab[rows * SR_M_STRIDE + SR_M_ITEM] = fld[SR_MENU_F_ITEM]
283 tab[rows * SR_M_STRIDE + SR_M_MIN] = mn
284 tab[rows * SR_M_STRIDE + SR_M_INTENT] = fld[SR_MENU_F_INTENT]
285 rows = rows + 1
286 } else {
287 if sr_streq(kind, SR_ROW_FALLBACK) == 1 {
288 if nf != SR_FALLBACK_FIELDS { why[0] = SR_D_BADMENU; return 0 - 1 }
289 if sr_field_clean(fld[1] as *u8) == 0 { why[0] = SR_D_BADMENU; return 0 - 1 }
290 fallback_out[0] = fld[1]
291 } else {
292 why[0] = SR_D_BADMENU
293 return 0 - 1
294 }
295 }
296 }
297 }
298 if fallback_out[0] == 0 { why[0] = SR_D_NOFALLBACK; return 0 - 1 }
299 return rows
300}
301
302// THE DERIVATION for one tip. With a named item: the row whose item matches and whose threshold the
303// amount reaches. Without one (item is "-"): the row with the HIGHEST threshold the amount reaches, first
304// declared wins a tie. Below every threshold: the fallback. Pure and total: every input maps to exactly
305// one intent pointer, never to 0, because the parser already refused a menu without a fallback.
306func sr_menu_intent(tab: *i64, rows: i64, fallback: *u8, amount: i64, item: *u8) -> *u8 {
307 var named: i64 = 0
308 if sr_streq(item, SR_EMPTY) == 0 { if sr_slen(item) > 0 { named = 1 } }
309 var best: i64 = 0 - 1
310 var bestmin: i64 = 0 - 1
311 var r: i64 = 0
312 while r < rows {
313 let mn: i64 = tab[r * SR_M_STRIDE + SR_M_MIN]
314 if mn <= amount {
315 var eligible: i64 = 1
316 if named == 1 { if sr_streq(tab[r * SR_M_STRIDE + SR_M_ITEM] as *u8, item) == 0 { eligible = 0 } }
317 if eligible == 1 { if mn > bestmin { bestmin = mn; best = r } }
318 }
319 r = r + 1
320 }
321 if best < 0 { return fallback }
322 return tab[best * SR_M_STRIDE + SR_M_INTENT] as *u8
323}
324
325// ---------------------------------------------------------------------------------------------
326// THE WRITER. Compose ev|nonce|ts|verb|actor|a1|a2 and APPEND it. Never overwrite (rule 13). Returns a
327// decision code; the log is UNCHANGED on every refusal. `scratch` is SR_SCRATCH bytes for the nonce
328// pattern; `line` is SR_LINE bytes for the row.
329func sr_event_append(logpath: *u8, nonce: *u8, ts: i64, verb: *u8, actor: *u8, a1: *u8, a2: *u8,
330 line: *u8, lcap: i64, scratch: *u8, lp: *i64) -> i64 {
331 let v: i64 = sr_verb_of(verb)
332 if v == SR_V_NONE { return SR_D_BADVERB }
333 if sr_field_clean(nonce) == 0 { return SR_D_BADNONCE }
334 if sr_field_clean(actor) == 0 { return SR_D_BADACTOR }
335 if sr_field_clean(a1) == 0 { return SR_D_BADARG }
336 if sr_field_clean(a2) == 0 { return SR_D_BADARG }
337 // capacity, DERIVED from the fields, checked before a byte is written
338 let need: i64 = sr_slen(SR_ROW_EV) + sr_slen(nonce) + SR_I64_DIGITS + sr_slen(verb) + sr_slen(actor) + sr_slen(a1) + sr_slen(a2) + SR_SEPS + 1
339 if need > lcap { return SR_D_OVERFLOW }
340 if sr_slen(nonce) + 2 + 1 > SR_SCRATCH { return SR_D_BADNONCE }
341 // idempotence: a nonce already in the log means the earlier attempt landed
342 var so: i64 = 0
343 scratch[so] = SR_CH_PIPE as u8; so = so + 1
344 so = sr_cat(scratch, so, nonce)
345 scratch[so] = SR_CH_PIPE as u8; so = so + 1
346 scratch[so] = 0 as u8
347 let old: *u8 = sys_read_file(logpath, lp)
348 if (old as i64) != 0 {
349 let on: i64 = lp[0]
350 let dup: i64 = sr_has(old, on, scratch)
351 sys_free_file(old, on)
352 if dup == 1 { return SR_D_DUPNONCE }
353 }
354 var p: i64 = 0
355 p = sr_cat(line, p, SR_ROW_EV)
356 line[p] = SR_CH_PIPE as u8; p = p + 1
357 p = sr_cat(line, p, nonce)
358 line[p] = SR_CH_PIPE as u8; p = p + 1
359 p = sr_catnum(line, p, ts)
360 line[p] = SR_CH_PIPE as u8; p = p + 1
361 p = sr_cat(line, p, sr_verb_name(v))
362 line[p] = SR_CH_PIPE as u8; p = p + 1
363 p = sr_cat(line, p, actor)
364 line[p] = SR_CH_PIPE as u8; p = p + 1
365 p = sr_cat(line, p, a1)
366 line[p] = SR_CH_PIPE as u8; p = p + 1
367 p = sr_cat(line, p, a2)
368 line[p] = SR_CH_NL as u8; p = p + 1
369 line[p] = 0 as u8
370 let fd: i64 = sys_openat_append(logpath, MODE_0644)
371 if fd < 0 { return SR_D_WRITEFAIL }
372 let w: i64 = sys_write(fd, line, p)
373 sys_fsync(fd)
374 sys_close(fd)
375 if w != p { return SR_D_WRITEFAIL }
376 return SR_OK
377}
378
379// ---------------------------------------------------------------------------------------------
380// THE READER. Derive the intent stream from a log buffer (mutated in place) and a parsed menu.
381// EVERY event yields exactly ONE intent row -- tip through the menu, every other verb as its own name
382// with a1 as the argument -- so the published count is a partition of the log, not a filter over it.
383// Returns a decision code; out holds `in|seq|intent|actor|amount|nonce` rows; count_out[0] the rows
384// written; bad_row[0] the 0-based index of the row that refused, or -1.
385func sr_replay(log: *u8, n: i64, tab: *i64, rows: i64, fallback: *u8,
386 out: *u8, ocap: i64, count_out: *i64, bad_row: *i64) -> i64 {
387 count_out[0] = 0
388 bad_row[0] = 0 - 1
389 let fld: *i64 = sys_mmap(SR_MAXF * SR_I64_BYTES) as *i64
390 var o: i64 = 0
391 var idx: i64 = 0
392 var p: i64 = 0
393 while p < n {
394 var e: i64 = p
395 while e < n { if log[e] == (SR_CH_NL as u8) { break } e = e + 1 }
396 let line: *u8 = (log as i64 + p) as *u8
397 let len: i64 = e - p
398 p = e + 1
399 if len > 0 {
400 let nf: i64 = sr_split(line, len, fld, SR_MAXF)
401 if nf != SR_EV_FIELDS { bad_row[0] = idx; return SR_D_UNPARSED }
402 if sr_streq(fld[SR_EV_F_KIND] as *u8, SR_ROW_EV) == 0 { bad_row[0] = idx; return SR_D_UNPARSED }
403 let v: i64 = sr_verb_of(fld[SR_EV_F_VERB] as *u8)
404 if v == SR_V_NONE { bad_row[0] = idx; return SR_D_UNPARSED }
405 let a1: *u8 = fld[SR_EV_F_A1] as *u8
406 let a2: *u8 = fld[SR_EV_F_A2] as *u8
407 var amount: i64 = 0
408 var intent: *u8 = sr_verb_name(v)
409 if v == SR_V_TIP {
410 amount = sr_atoi_strict(a1)
411 if amount < 0 { bad_row[0] = idx; return SR_D_UNPARSED }
412 intent = sr_menu_intent(tab, rows, fallback, amount, a2)
413 } else {
414 if sr_streq(a1, SR_EMPTY) == 0 { intent = a1 }
415 }
416 let need: i64 = sr_slen(SR_ROW_IN) + SR_I64_DIGITS + sr_slen(intent) + sr_slen(fld[SR_EV_F_ACTOR] as *u8) + SR_I64_DIGITS + sr_slen(fld[SR_EV_F_NONCE] as *u8) + SR_SEPS
417 if o + need > ocap { return SR_D_OVERFLOW }
418 o = sr_cat(out, o, SR_ROW_IN)
419 out[o] = SR_CH_PIPE as u8; o = o + 1
420 o = sr_catnum(out, o, idx)
421 out[o] = SR_CH_PIPE as u8; o = o + 1
422 o = sr_cat(out, o, intent)
423 out[o] = SR_CH_PIPE as u8; o = o + 1
424 o = sr_cat(out, o, fld[SR_EV_F_ACTOR] as *u8)
425 out[o] = SR_CH_PIPE as u8; o = o + 1
426 o = sr_catnum(out, o, amount)
427 out[o] = SR_CH_PIPE as u8; o = o + 1
428 o = sr_cat(out, o, fld[SR_EV_F_NONCE] as *u8)
429 out[o] = SR_CH_NL as u8; o = o + 1
430 idx = idx + 1
431 }
432 }
433 out[o] = 0 as u8
434 count_out[0] = idx
435 return SR_OK
436}
437
438// Convenience: read a menu conf from disk and parse it. Returns rows or -1 with why[0] set.
439func sr_menu_load(path: *u8, tab: *i64, cap_rows: i64, fallback_out: *i64, why: *i64, lp: *i64) -> i64 {
440 let buf: *u8 = sys_read_file(path, lp)
441 if (buf as i64) == 0 { why[0] = SR_D_NOMENU; fallback_out[0] = 0; return 0 - 1 }
442 return sr_menu_parse(buf, lp[0], tab, cap_rows, fallback_out, why)
443}