code wiki / (root) / nx_synthroom_lib.nx

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}