nx_compare_gw_lib.nx source
↩ module page · 541 lines · 26076 B
1// nx_compare_gw_lib.nx -- THE PURE DECISION CORE of the /compare gateway: who may see which board.
2//
3// WHY A GATEWAY AT ALL (operator 2026-08-27: "/compare against the login walls so its behind the same thing
4// as opaque and has the same access structure -- i can see them as the owner but then others see different
5// levels of /compare so i can provide a client a /compare"). Today /compare is plain static docroot bytes:
6// no route row, no session, every domain public. This lib decides, per request, from THREE inputs --
7// the viewer's LEVEL (resolved by the estate's one access spine, nx_hr_access: OPAQUE session -> HR level,
8// deny-by-default), the DOMAIN the path names, and the ACCESS the domain declares as DATA in
9// knowledge/compare/access.conf -- whether the bytes are served, and what an index at that level lists.
10//
11// ACCESS IS DATA, NEVER CODE (rule 11). access.conf rows:
12// default|<access> the access every domain gets unless a row names it (ABSENT = operator: fail-closed)
13// domain|<dom>|<access> one domain's access
14// access vocabulary, identical to nx_maturity_registry's mr_level_of so the hub and the compare wall cannot
15// disagree on a word: public=0 viewer=1 member=2 operator=3 ; an UNKNOWN word reads operator (fail-closed:
16// a typo locks a board to the owner, it never exposes one). HR enrols owner=3 and family/client=1, so a
17// client handed a /compare sees the public boards plus every board declared viewer; the owner sees all.
18//
19// NO-LEAK AT THE RENDER LAYER: a viewer's index bytes never contain the name of a board above their level
20// (absent from the bytes, not hidden by style), and a request for such a board answers exactly like a
21// request for a board that does not exist. Not-even-listed is the property, and it is measured by the gate.
22// license_tier: ORIGINAL No hw writes (Rule 26).
23import "nx_syscalls.nx"
24
25const CG_LVL_PUBLIC: i64 = 0
26const CG_LVL_VIEWER: i64 = 1
27const CG_LVL_MEMBER: i64 = 2
28const CG_LVL_OPERATOR: i64 = 3
29
30const CG_ROW_DEFAULT: *u8 = "default" as *u8
31const CG_ROW_DOMAIN: *u8 = "domain" as *u8
32const CG_PREFIX: *u8 = "/compare" as *u8
33// EC47 (2026-09-16): the feed link the served root page advertises (declared here, above its reader in cg_index_open: a module
34// const read above its declaration folds to 0, and the builder refuses that)
35const CG_FEED_LINK_TAG: *u8 = "<link rel='alternate' type='application/rss+xml' title='Nishi /compare daily positions and reviews' href='/compare/feed.xml'>" as *u8
36
37// decisions -- one per request, named
38const CG_D_SERVE: i64 = 1
39const CG_D_DENY: i64 = 0
40const CG_D_NOTFOUND: i64 = 0 - 1
41const CG_D_BADPATH: i64 = 0 - 2
42
43const CG_CH_NL: i64 = 10
44const CG_CH_CR: i64 = 13
45const CG_CH_SPACE: i64 = 32
46const CG_CH_HASH: i64 = 35
47const CG_CH_QUOTE: i64 = 34
48const CG_CH_BSLASH: i64 = 92
49const CG_CH_DOT: i64 = 46
50const CG_CH_SLASH: i64 = 47
51const CG_CH_PIPE: i64 = 124
52const CG_CH_QMARK: i64 = 63
53const CG_CH_COLON: i64 = 58
54const CG_MAXF: i64 = 4
55const CG_I64_BYTES: i64 = 8
56const CG_DOM_CAP: i64 = 128
57
58func cg_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
59func cg_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; var p: i64 = o; while s[i] != (0 as u8) { d[p] = s[i]; p = p + 1; i = i + 1 } return p }
60func cg_catn(d: *u8, o: i64, src: *u8, n: i64) -> i64 { var i: i64 = 0; var p: i64 = o; while i < n { d[p] = src[i]; p = p + 1; i = i + 1 } return p }
61func cg_catnum(d: *u8, o: i64, v: i64) -> i64 {
62 let t: *u8 = sys_mmap(28)
63 var m: i64 = v
64 var p: i64 = o
65 if m < 0 { d[p] = 45 as u8; p = p + 1; m = 0 - m }
66 var k: i64 = 0
67 if m == 0 { t[0] = 48 as u8; k = 1 }
68 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
69 var i: i64 = 0
70 while i < k { d[p] = t[k-1-i]; p = p + 1; i = i + 1 }
71 sys_munmap(t, 28)
72 return p
73}
74// slice[a..b) == NUL-terminated literal
75func cg_sl_eq(buf: *u8, a: i64, b: i64, lit: *u8) -> i64 {
76 let l: i64 = cg_slen(lit)
77 if b - a != l { return 0 }
78 var i: i64 = 0
79 while i < l { if buf[a+i] != lit[i] { return 0 } i = i + 1 }
80 return 1
81}
82func cg_streq(a: *u8, b: *u8) -> i64 {
83 let n: i64 = cg_slen(a)
84 if n != cg_slen(b) { return 0 }
85 var i: i64 = 0
86 while i < n { if a[i] != b[i] { return 0 } i = i + 1 }
87 return 1
88}
89
90// ---- the access vocabulary: the SAME four words and the SAME fail-closed default as mr_level_of ----
91func cg_access_level(buf: *u8, a: i64, b: i64) -> i64 {
92 if cg_sl_eq(buf, a, b, "public" as *u8) == 1 { return CG_LVL_PUBLIC }
93 if cg_sl_eq(buf, a, b, "viewer" as *u8) == 1 { return CG_LVL_VIEWER }
94 if cg_sl_eq(buf, a, b, "member" as *u8) == 1 { return CG_LVL_MEMBER }
95 if cg_sl_eq(buf, a, b, "operator" as *u8) == 1 { return CG_LVL_OPERATOR }
96 return CG_LVL_OPERATOR
97}
98func cg_level_name(l: i64) -> *u8 {
99 if l == CG_LVL_PUBLIC { return "public" as *u8 }
100 if l == CG_LVL_VIEWER { return "viewer" as *u8 }
101 if l == CG_LVL_MEMBER { return "member" as *u8 }
102 return "operator" as *u8
103}
104
105// ---- the conf: rows over a COUNTED buffer, comment lines skipped, fields split on the pipe ----
106// Walk every row; call once per row via the caller's loop. Returns the field count for the row at [s,e)
107// into fa/fb (field start/end offsets), at most CG_MAXF fields.
108func cg_row_fields(buf: *u8, s: i64, e: i64, fa: *i64, fb: *i64) -> i64 {
109 var nf: i64 = 0
110 var start: i64 = s
111 var i: i64 = s
112 while i <= e {
113 var atend: i64 = 0
114 if i == e { atend = 1 } else { if buf[i] == (CG_CH_PIPE as u8) { atend = 1 } }
115 if atend == 1 {
116 if nf < CG_MAXF { fa[nf] = start; fb[nf] = i; nf = nf + 1 }
117 start = i + 1
118 }
119 i = i + 1
120 }
121 return nf
122}
123func cg_eol(buf: *u8, n: i64, i: i64) -> i64 {
124 var e: i64 = i
125 while e < n { if buf[e] == (CG_CH_NL as u8) { return e } e = e + 1 }
126 return n
127}
128
129// THE REQUIRED LEVEL FOR A DOMAIN. A `domain|<dom>|<access>` row wins; else the `default|<access>` row;
130// else OPERATOR -- a conf that names nothing exposes nothing. Later rows win over earlier ones for the same
131// key, so an append is an override (rule 13: history stays in the file).
132func cg_required(conf: *u8, n: i64, dom: *u8) -> i64 {
133 let fa: *i64 = sys_mmap(CG_MAXF * CG_I64_BYTES) as *i64
134 let fb: *i64 = sys_mmap(CG_MAXF * CG_I64_BYTES) as *i64
135 var dflt: i64 = CG_LVL_OPERATOR
136 var found: i64 = 0 - 1
137 var i: i64 = 0
138 while i < n {
139 let e: i64 = cg_eol(conf, n, i)
140 if e > i { if conf[i] != (CG_CH_HASH as u8) {
141 let nf: i64 = cg_row_fields(conf, i, e, fa, fb)
142 if nf == 2 { if cg_sl_eq(conf, fa[0], fb[0], CG_ROW_DEFAULT) == 1 { dflt = cg_access_level(conf, fa[1], fb[1]) } }
143 if nf == 3 { if cg_sl_eq(conf, fa[0], fb[0], CG_ROW_DOMAIN) == 1 {
144 if cg_sl_eq(conf, fa[1], fb[1], dom) == 1 { found = cg_access_level(conf, fa[2], fb[2]) }
145 } }
146 } }
147 i = e + 1
148 }
149 sys_munmap(fa as *u8, CG_MAXF * CG_I64_BYTES)
150 sys_munmap(fb as *u8, CG_MAXF * CG_I64_BYTES)
151 if found >= 0 { return found }
152 return dflt
153}
154
155// Enumerate the DECLARED domains of the conf into a caller buffer as NUL-separated names; returns the count.
156// Only `domain|` rows are declared -- a board that is not named is governed by the default and is listed by
157// the daemon from the docroot, never from here.
158func cg_declared(conf: *u8, n: i64, out: *u8, cap: i64, count_out: *i64) -> i64 {
159 let fa: *i64 = sys_mmap(CG_MAXF * CG_I64_BYTES) as *i64
160 let fb: *i64 = sys_mmap(CG_MAXF * CG_I64_BYTES) as *i64
161 var o: i64 = 0
162 var c: i64 = 0
163 var i: i64 = 0
164 while i < n {
165 let e: i64 = cg_eol(conf, n, i)
166 if e > i { if conf[i] != (CG_CH_HASH as u8) {
167 let nf: i64 = cg_row_fields(conf, i, e, fa, fb)
168 if nf == 3 { if cg_sl_eq(conf, fa[0], fb[0], CG_ROW_DOMAIN) == 1 {
169 let dl: i64 = fb[1] - fa[1]
170 if o + dl + 1 < cap { o = cg_catn(out, o, ((conf as i64) + fa[1]) as *u8, dl); out[o] = 0 as u8; o = o + 1; c = c + 1 }
171 } }
172 } }
173 i = e + 1
174 }
175 count_out[0] = c
176 return o
177}
178
179// ---- the request: path from the request line, one header value, the domain under /compare ----
180// "METHOD <target> HTTP/x" -> the target up to but excluding any query string. Returns its length, 0 if none.
181func cg_req_path(req: *u8, n: i64, out: *u8, cap: i64) -> i64 {
182 var sp1: i64 = 0 - 1
183 var i: i64 = 0
184 while i < n { if sp1 < 0 { if req[i] == (CG_CH_SPACE as u8) { sp1 = i } } i = i + 1 }
185 if sp1 < 0 { return 0 }
186 var o: i64 = 0
187 var k: i64 = sp1 + 1
188 var go: i64 = 1
189 while go == 1 {
190 if k >= n { go = 0 } else {
191 let c: i64 = req[k] as i64
192 if c == CG_CH_SPACE { go = 0 } else { if c == CG_CH_QMARK { go = 0 } else { if c == CG_CH_NL { go = 0 } else { if c == CG_CH_CR { go = 0 } else {
193 if o < cap - 1 { out[o] = req[k]; o = o + 1 }
194 k = k + 1
195 } } } }
196 }
197 }
198 out[o] = 0 as u8
199 return o
200}
201// header value by name (name includes the colon), line-anchored; tolerant of CRLF and LF. 0 if absent.
202func cg_req_header(req: *u8, n: i64, name: *u8, out: *u8, cap: i64) -> i64 {
203 let nl: i64 = cg_slen(name)
204 var ls: i64 = 0
205 while ls < n {
206 let le: i64 = cg_eol(req, n, ls)
207 if le - ls >= nl {
208 var m: i64 = 1
209 var c: i64 = 0
210 while c < nl { if req[ls + c] != name[c] { m = 0; c = nl } else { c = c + 1 } }
211 if m == 1 {
212 var vs: i64 = ls + nl
213 while vs < le { if req[vs] == (CG_CH_SPACE as u8) { vs = vs + 1 } else { break } }
214 var ve: i64 = le
215 if ve > vs { if req[ve - 1] == (CG_CH_CR as u8) { ve = ve - 1 } }
216 var o: i64 = 0
217 var k: i64 = vs
218 while k < ve { if o < cap - 1 { out[o] = req[k]; o = o + 1 } k = k + 1 }
219 out[o] = 0 as u8
220 return o
221 }
222 }
223 ls = le + 1
224 }
225 out[0] = 0 as u8
226 return 0
227}
228
229// Boundary defence (rule 12): a docroot-relative path may not climb, may not be absolute, and may not carry
230// an empty segment. Returns 1 when safe.
231func cg_path_safe(p: *u8, n: i64) -> i64 {
232 if n <= 0 { return 0 }
233 if p[0] == (CG_CH_SLASH as u8) { return 0 }
234 var i: i64 = 0
235 while i + 1 < n { if p[i] == (CG_CH_DOT as u8) { if p[i + 1] == (CG_CH_DOT as u8) { return 0 } } i = i + 1 }
236 i = 0
237 while i + 1 < n { if p[i] == (CG_CH_SLASH as u8) { if p[i + 1] == (CG_CH_SLASH as u8) { return 0 } } i = i + 1 }
238 return 1
239}
240
241// Split a request path under the prefix: "/compare" or "/compare/" -> is_index=1, dom empty, rest empty.
242// "/compare/<dom>" -> dom, rest empty. "/compare/<dom>/<rest>" -> dom + rest. A path that does not start
243// with the prefix returns -1 (this daemon is only ever reached through the /compare route, so that is a
244// misroute and it is refused rather than guessed). Returns is_index (0/1) or -1.
245func cg_split(path: *u8, n: i64, dom: *u8, dcap: i64, rest: *u8, rcap: i64) -> i64 {
246 let pl: i64 = cg_slen(CG_PREFIX)
247 dom[0] = 0 as u8
248 rest[0] = 0 as u8
249 if n < pl { return 0 - 1 }
250 var i: i64 = 0
251 while i < pl { if path[i] != CG_PREFIX[i] { return 0 - 1 } i = i + 1 }
252 if n == pl { return 1 }
253 if path[pl] != (CG_CH_SLASH as u8) { return 0 - 1 }
254 if n == pl + 1 { return 1 }
255 var p: i64 = pl + 1
256 var o: i64 = 0
257 while p < n { if path[p] == (CG_CH_SLASH as u8) { break } if o < dcap - 1 { dom[o] = path[p]; o = o + 1 } p = p + 1 }
258 dom[o] = 0 as u8
259 var r: i64 = 0
260 if p < n {
261 p = p + 1
262 while p < n { if r < rcap - 1 { rest[r] = path[p]; r = r + 1 } p = p + 1 }
263 }
264 rest[r] = 0 as u8
265 return 0
266}
267
268// THE DECISION, pure and total. exists = the docroot holds the domain (the daemon stats it).
269// NOT-EVEN-LISTED IS TOTAL: a board the viewer may not see answers NOTFOUND -- the SAME answer a board that
270// does not exist gives -- for EVERY viewer below its level, not only for anonymous ones. That is the whole
271// point of "provide a client a /compare": a client handed a subset must not be able to discover that boards
272// exist above their tier, and a DENY (403) would leak exactly that existence. So below the bar is always
273// NOTFOUND. CG_D_DENY stays in the vocabulary for a caller that deliberately wants a visible 403, but
274// cg_decide never returns it -- the no-leak property is not left to the daemon to remember.
275func cg_decide(level: i64, required: i64, exists: i64) -> i64 {
276 if exists == 0 { return CG_D_NOTFOUND }
277 if level >= required { return CG_D_SERVE }
278 return CG_D_NOTFOUND
279}
280func cg_decision_name(d: i64) -> *u8 {
281 if d == CG_D_SERVE { return "SERVE" as *u8 }
282 if d == CG_D_DENY { return "DENY-LEVEL" as *u8 }
283 if d == CG_D_NOTFOUND { return "NOTFOUND" as *u8 }
284 if d == CG_D_BADPATH { return "REFUSED-BAD-PATH" as *u8 }
285 return "UNCLASSIFIED" as *u8
286}
287
288// "title":"..." out of a JSON buffer (the domain api.json), backslash-escapes copied through, bounded.
289func cg_json_str(buf: *u8, n: i64, key: *u8, out: *u8, cap: i64) -> i64 {
290 let kl: i64 = cg_slen(key)
291 var i: i64 = 0
292 while i + kl + 3 < n {
293 var m: i64 = 1
294 var c: i64 = 0
295 if buf[i] != (CG_CH_QUOTE as u8) { m = 0 }
296 while m == 1 { if c < kl { if buf[i + 1 + c] != key[c] { m = 0 } c = c + 1 } else { break } }
297 if m == 1 { if buf[i + 1 + kl] != (CG_CH_QUOTE as u8) { m = 0 } }
298 if m == 1 { if buf[i + 2 + kl] != (CG_CH_COLON as u8) { m = 0 } }
299 if m == 1 { if buf[i + 3 + kl] != (CG_CH_QUOTE as u8) { m = 0 } }
300 if m == 1 {
301 var p: i64 = i + 4 + kl
302 var o: i64 = 0
303 var go: i64 = 1
304 while go == 1 {
305 if p >= n { go = 0 } else {
306 let ch: i64 = buf[p] as i64
307 if ch == CG_CH_QUOTE { go = 0 } else {
308 if ch == CG_CH_BSLASH { if p + 1 < n { if o < cap - 1 { out[o] = buf[p + 1]; o = o + 1 } p = p + 2 } else { p = n } }
309 else { if o < cap - 1 { out[o] = buf[p]; o = o + 1 } p = p + 1 }
310 }
311 }
312 }
313 out[o] = 0 as u8
314 return o
315 }
316 i = i + 1
317 }
318 out[0] = 0 as u8
319 return 0
320}
321
322// ---- INDEX: one row per VISIBLE domain, and nothing else. The daemon supplies the candidate list (the
323// docroot's directories, NUL-separated) and a title per domain; this emits only what `level` may see.
324// The bytes for a hidden board are never written -- not-even-listed is a property of the output, and the
325// gate measures it by searching the bytes.
326func cg_index_open(out: *u8, level: i64, with_upgrade: i64) -> i64 {
327 var o: i64 = 0
328 o = cg_cat(out, o, "<!DOCTYPE html><html lang='en'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width, initial-scale=1'><title>Nishi Compare</title>" as *u8)
329 // EC47 (2026-09-16): the served root page advertises the daily feed, as the regen hub does in the docroot file this page replaces
330 o = cg_cat(out, o, CG_FEED_LINK_TAG)
331 o = cg_cat(out, o, "<style>body{font-family:-apple-system,Segoe UI,Roboto,sans-serif;max-width:900px;margin:0 auto;padding:24px;line-height:1.5}h1{font-size:1.6rem}ul{list-style:none;padding:0}li{padding:10px 0;border-bottom:1px solid rgb(220,220,220)}a{text-decoration:none}small{color:rgb(110,110,110);display:block}.lv{font-size:.8rem;color:rgb(110,110,110)}</style></head><body>" as *u8)
332 if with_upgrade == 1 {
333 // NO-COOKIE UPGRADE SHIM (the hub's OLGD_SPA pattern): a plain navigation carries no header, so a
334 // signed-in browser re-fetches the same path WITH its X-Nishi-Session and swaps in the leveled page.
335 o = cg_cat(out, o, "<script>(function(){var t=sessionStorage.getItem('nsess');if(!t){return}fetch(location.pathname,{headers:{'X-Nishi-Session':t}}).then(function(r){return r.ok?r.text():null}).then(function(x){if(x){document.open();document.write(x);document.close()}})})();</script>" as *u8)
336 }
337 // <main> LANDMARK (2026-08-27): nx_page_verify a11y-lite flagged the generated index as the ONE gateway
338 // page without a <main> landmark (every emitted board page has it). Opened here, closed in cg_index_close.
339 o = cg_cat(out, o, "<main><h1>Nishi Compare</h1><p class='lv'>viewing as " as *u8)
340 o = cg_cat(out, o, cg_level_name(level))
341 o = cg_cat(out, o, " · every board is measured against source at emit time · <a href='/compare/feed.xml'>daily positions feed (RSS)</a></p><ul>" as *u8)
342 return o
343}
344func cg_index_row(out: *u8, o: i64, dom: *u8, title: *u8) -> i64 {
345 var p: i64 = o
346 p = cg_cat(out, p, "<li><a href='/compare/" as *u8)
347 p = cg_cat(out, p, dom)
348 p = cg_cat(out, p, "'>" as *u8)
349 if title[0] != (0 as u8) { p = cg_cat(out, p, title) } else { p = cg_cat(out, p, dom) }
350 p = cg_cat(out, p, "</a><small>/compare/" as *u8)
351 p = cg_cat(out, p, dom)
352 p = cg_cat(out, p, "</small></li>" as *u8)
353 return p
354}
355func cg_index_close(out: *u8, o: i64, shown: i64) -> i64 {
356 var p: i64 = o
357 p = cg_cat(out, p, "</ul><p class='lv'>" as *u8)
358 p = cg_catnum(out, p, shown)
359 p = cg_cat(out, p, " boards at this level · generated by nx_compare_gw</p></main></body></html>" as *u8)
360 out[p] = 0 as u8
361 return p
362}
363
364// the filtered hub api.json: only the visible domains, in the same top-level shape consumers expect
365func cg_api_open(out: *u8, now: i64) -> i64 {
366 var o: i64 = cg_cat(out, 0, "{\"v\":1,\"api\":\"nishi-compare\",\"resource\":\"index\",\"generated_unix\":" as *u8)
367 o = cg_catnum(out, o, now)
368 o = cg_cat(out, o, ",\"gated\":1,\"comparisons\":[" as *u8)
369 return o
370}
371func cg_api_row(out: *u8, o: i64, first: i64, dom: *u8, title: *u8) -> i64 {
372 var p: i64 = o
373 if first == 0 { out[p] = 44 as u8; p = p + 1 }
374 p = cg_cat(out, p, "{\"domain\":\"" as *u8)
375 p = cg_cat(out, p, dom)
376 p = cg_cat(out, p, "\",\"title\":\"" as *u8)
377 // the title came out of a JSON string already; a quote inside it is re-escaped so the row stays valid
378 var i: i64 = 0
379 while title[i] != (0 as u8) {
380 if title[i] == (CG_CH_QUOTE as u8) { out[p] = CG_CH_BSLASH as u8; p = p + 1 }
381 if title[i] == (CG_CH_BSLASH as u8) { out[p] = CG_CH_BSLASH as u8; p = p + 1 }
382 out[p] = title[i]; p = p + 1
383 i = i + 1
384 }
385 p = cg_cat(out, p, "\",\"page\":\"/compare/" as *u8)
386 p = cg_cat(out, p, dom)
387 p = cg_cat(out, p, "\",\"data\":\"/compare/" as *u8)
388 p = cg_cat(out, p, dom)
389 p = cg_cat(out, p, "/api.json\"}" as *u8)
390 return p
391}
392func cg_api_close(out: *u8, o: i64, shown: i64) -> i64 {
393 var p: i64 = cg_cat(out, o, "],\"count\":" as *u8)
394 p = cg_catnum(out, p, shown)
395 p = cg_cat(out, p, "}\n" as *u8)
396 out[p] = 0 as u8
397 return p
398}
399
400// content type by extension -- the file kinds the compare docroot actually holds
401func cg_ctype(file: *u8) -> *u8 {
402 let n: i64 = cg_slen(file)
403 var dot: i64 = 0 - 1
404 var i: i64 = 0
405 while i < n { if file[i] == (CG_CH_DOT as u8) { dot = i } i = i + 1 }
406 if dot < 0 { return "application/octet-stream" as *u8 }
407 let ext: *u8 = ((file as i64) + dot) as *u8
408 if cg_streq(ext, ".html" as *u8) == 1 { return "text/html; charset=utf-8" as *u8 }
409 if cg_streq(ext, ".json" as *u8) == 1 { return "application/json" as *u8 }
410 if cg_streq(ext, ".css" as *u8) == 1 { return "text/css" as *u8 }
411 if cg_streq(ext, ".js" as *u8) == 1 { return "application/javascript" as *u8 }
412 if cg_streq(ext, ".png" as *u8) == 1 { return "image/png" as *u8 }
413 if cg_streq(ext, ".svg" as *u8) == 1 { return "image/svg+xml" as *u8 }
414 if cg_streq(ext, ".pdf" as *u8) == 1 { return "application/pdf" as *u8 }
415 if cg_streq(ext, ".md" as *u8) == 1 { return "text/markdown; charset=utf-8" as *u8 }
416 if cg_streq(ext, ".txt" as *u8) == 1 { return "text/plain; charset=utf-8" as *u8 }
417 if cg_streq(ext, ".xml" as *u8) == 1 { return "application/xml" as *u8 }
418 return "application/octet-stream" as *u8
419}
420
421// ---- EC47 (2026-09-16): the /compare feed, projected per level ----
422// /compare/feed.xml is ONE file carrying an item per board per day, so the gateway projects it the way it projects the
423// index: an <item> whose <link> names a board the level may not open is ABSENT from the bytes (never hidden), and an
424// item this projector cannot attribute to a board is absent at EVERY level (an unattributable row is served to nobody).
425// The board is the segment after "/compare/" in the item's <link>. Returns the projected length; -1 when the source is
426// not a feed it understands (an <item> without its close) or the projection would not fit -- the daemon then answers
427// 503, never a half feed. kept/dropped are written so the daemon can announce the projection it served.
428const CG_FEED_ITEM_OPEN: *u8 = "<item>" as *u8
429const CG_FEED_ITEM_CLOSE: *u8 = "</item>" as *u8
430const CG_FEED_LINK_OPEN: *u8 = "<link>" as *u8
431const CG_FEED_BOARD_MARK: *u8 = "/compare/" as *u8
432const CG_CH_LT: i64 = 60
433// bounded substring search: the offset of needle in buf[from..n), or -1
434func cg_find(buf: *u8, n: i64, from: i64, needle: *u8) -> i64 {
435 let m: i64 = cg_slen(needle)
436 if m == 0 { return from }
437 var i: i64 = from
438 while i + m <= n {
439 var j: i64 = 0
440 var ok: i64 = 1
441 while j < m { if buf[i + j] != needle[j] { ok = 0; j = m } else { j = j + 1 } }
442 if ok == 1 { return i }
443 i = i + 1
444 }
445 return 0 - 1
446}
447// the board an item's link names: the bytes after "/compare/" up to the next '/' or '<', NUL-terminated into dom;
448// returns the board length, 0 when the item carries no attributable link
449func cg_feed_item_board(src: *u8, ib: i64, ie: i64, dom: *u8, dcap: i64) -> i64 {
450 let l: i64 = cg_find(src, ie, ib, CG_FEED_LINK_OPEN)
451 if l < 0 { return 0 }
452 let b: i64 = cg_find(src, ie, l, CG_FEED_BOARD_MARK)
453 if b < 0 { return 0 }
454 var p: i64 = b + cg_slen(CG_FEED_BOARD_MARK)
455 var o: i64 = 0
456 while p < ie {
457 let c: i64 = src[p] as i64
458 if c == CG_CH_SLASH { p = ie } else { if c == CG_CH_LT { p = ie } else {
459 if o + 1 >= dcap { return 0 }
460 dom[o] = c as u8; o = o + 1; p = p + 1
461 } }
462 }
463 dom[o] = 0 as u8
464 return o
465}
466func cg_feed_project(src: *u8, n: i64, level: i64, conf: *u8, cn: i64, out: *u8, cap: i64, dom: *u8, dcap: i64, kept: *i64, dropped: *i64) -> i64 {
467 kept[0] = 0
468 dropped[0] = 0
469 var p: i64 = cg_find(src, n, 0, CG_FEED_ITEM_OPEN)
470 if p < 0 {
471 // no items at all: the feed is its channel metadata and is served whole
472 if n > cap { return 0 - 1 }
473 var z: i64 = 0
474 while z < n { out[z] = src[z]; z = z + 1 }
475 return n
476 }
477 // the head: channel metadata before the first item
478 if p > cap { return 0 - 1 }
479 var o: i64 = 0
480 while o < p { out[o] = src[o]; o = o + 1 }
481 var cursor: i64 = p
482 while p >= 0 {
483 let c: i64 = cg_find(src, n, p, CG_FEED_ITEM_CLOSE)
484 if c < 0 { return 0 - 1 }
485 var e: i64 = c + cg_slen(CG_FEED_ITEM_CLOSE)
486 if e < n { if src[e] == (CG_CH_NL as u8) { e = e + 1 } }
487 let dl: i64 = cg_feed_item_board(src, p, e, dom, dcap)
488 var serve: i64 = 0
489 if dl > 0 { if cg_decide(level, cg_required(conf, cn, dom), 1) == CG_D_SERVE { serve = 1 } }
490 if serve == 1 {
491 if o + (e - p) > cap { return 0 - 1 }
492 var k: i64 = p
493 while k < e { out[o] = src[k]; o = o + 1; k = k + 1 }
494 kept[0] = kept[0] + 1
495 } else { dropped[0] = dropped[0] + 1 }
496 cursor = e
497 p = cg_find(src, n, e, CG_FEED_ITEM_OPEN)
498 }
499 // the tail: everything after the last item (the channel and rss closes)
500 if o + (n - cursor) > cap { return 0 - 1 }
501 var t: i64 = cursor
502 while t < n { out[o] = src[t]; o = o + 1; t = t + 1 }
503 return o
504}
505
506// ---- THE FILE SERVED UNDER A BOARD (2026-09-16) ----
507// THE DEFECT, MEASURED THROUGH THE EDGE: every board's frontier radar lives at <board>/frontier/index.html and the hub
508// links it as <board>/frontier, but the daemon took the rest of the path as a FILE name and read the DIRECTORY as a
509// file: the read returned no bytes and the gateway answered 200 OK, application/octet-stream, Content-Length 0 --
510// a false success on /compare/lang/frontier, /compare/stem/frontier and every other radar, while the page itself
511// served fine at .../frontier/index.html. The decision now lives here, pure, so the gate reaches it: a rest that
512// names a directory is served as its index.html, and an empty rest is the board's own index.html.
513// cg_board_file writes the served path (relative to the board) into out and returns its length; out must hold
514// rest plus CG_INDEX_SUFFIX_BYTES.
515const CG_INDEX_FILE: *u8 = "index.html"
516const CG_INDEX_SUFFIX_BYTES: i64 = 16
517const CG_CH_SLASH: i64 = 47
518func cg_board_file(rest: *u8, rest_is_dir: i64, out: *u8) -> i64 {
519 var o: i64 = 0
520 var n: i64 = 0
521 while rest[n] != (0 as u8) { n = n + 1 }
522 if n == 0 { o = cg_cat(out, 0, CG_INDEX_FILE); out[o] = 0 as u8; return o }
523 while o < n { out[o] = rest[o]; o = o + 1 }
524 if rest_is_dir == 1 {
525 if out[o - 1] != (CG_CH_SLASH as u8) { out[o] = CG_CH_SLASH as u8; o = o + 1 }
526 o = cg_cat(out, o, CG_INDEX_FILE)
527 }
528 out[o] = 0 as u8
529 return o
530}
531// A READ THAT FILLED THE BODY RESERVE IS NOT A PAGE. The reader stops at the reserve, so a page larger than it would
532// have been served truncated under 200 OK; the verdict refuses that instead. CG_BODY_SERVE . CG_BODY_TOO_LARGE
533// (n reached the reserve) . CG_BODY_MISSING (unreadable).
534const CG_BODY_SERVE: i64 = 1
535const CG_BODY_TOO_LARGE: i64 = 2
536const CG_BODY_MISSING: i64 = 0 - 1
537func cg_body_verdict(n: i64, cap: i64) -> i64 {
538 if n < 0 { return CG_BODY_MISSING }
539 if n >= cap { return CG_BODY_TOO_LARGE }
540 return CG_BODY_SERVE
541}