code wiki / (root) / nx_unidoc_lib.nx

nx_unidoc_lib.nx source

↩ module page · 518 lines · 28920 B

1// nx_unidoc_lib.nx -- THE UNIFIED DOCUMENT PAGE (rung UD1 of /compare/unidoc, 2026-09-03). 2// 3// Operator (2026-09-03): "we want nishi browser to handle documents like webpages securely but with full 4// interactivity so we dont have to have a file browser and a web browser etc but a unified portal." 5// 6// ONE page per document, whatever it was born as. A .docx, .odt, .pdf, .md, .csv or .txt becomes the SAME 7// zero-script HTML page: the office's own document model (the H/P/T/B/I spec lines that nx_office_serve 8// edits, versions and signs) rendered as a page, with a Content-Security-Policy that forbids every script 9// and every foreign fetch, every byte of document text escaped BY CONSTRUCTION, and the portal's actions 10// (original bytes, versions, annotate, sign) as plain HTML forms -- no client script anywhere, so the page 11// is native in the Nishi browser and NishiOS first and degrades to nothing in a third-party browser. 12// 13// A web page is NOT a document: .html and .htm are REFUSED here by name (UD_ERR_REFUSED_HTML). The browser 14// already serves a page as itself; wrapping one as a document would be the exact confusion this organ ends. 15// 16// Composition, never re-implementation: .docx and .odt go through the promoted format organs' `spec` verbs 17// (_offc/nx_docx.elf, _offc/nx_odt.elf -- the same verbs the office import uses), .pdf through the proven 18// text extractor _offc/nx_pdf_text.elf, all captured with nx_tool_run. Markdown, CSV and text are folded 19// to the spec here because their grammar IS the spec's grammar. 20// 21// Every refusal is NAMED (ud_err_name) and the input ceiling is ANNOUNCED, never a silent truncation. 22// license_tier: ORIGINAL 23import "nx_syscalls.nx" 24import "nx_itoa_lib.nx" 25import "nx_tool_run.nx" 26 27// document kinds (the value the registry dispatch and the page label both key on) 28const UD_KIND_UNKNOWN: i64 = 0 29const UD_KIND_DOCX: i64 = 1 30const UD_KIND_ODT: i64 = 2 31const UD_KIND_PDF: i64 = 3 32const UD_KIND_MD: i64 = 4 33const UD_KIND_CSV: i64 = 5 34const UD_KIND_TXT: i64 = 6 35const UD_KIND_HTML: i64 = 7 // recognised so it can be REFUSED by name, never rendered as a document 36 37// named refusals (negative, distinct, each with a name in ud_err_name) 38const UD_ERR_UNKNOWN_KIND: i64 = 0 - 1 39const UD_ERR_REFUSED_HTML: i64 = 0 - 2 40const UD_ERR_RENDERER: i64 = 0 - 3 // the format organ failed or handed back no spec 41const UD_ERR_TOO_BIG: i64 = 0 - 4 // above UD_IN_CAP: refused by name, never cut 42const UD_ERR_READ: i64 = 0 - 5 // the input could not be read 43const UD_ERR_OUT_CAP: i64 = 0 - 6 // the page would not fit the caller's buffer: refused, never truncated 44 45// ceilings -- each one NAMED and ANNOUNCED on refusal (rule 11: a ceiling that truncates in silence is a defect) 46const UD_IN_CAP: i64 = 8388608 // 8 MiB of document bytes per page 47const UD_SPEC_CAP: i64 = 4194304 // the folded spec (H/P/T lines) 48const UD_CAPTURE_CAP: i64 = 4194320 // a format organ's captured stdout (spec + its BEGIN/END markers) 49const UD_PATH_CAP: i64 = 4096 50const UD_EXT_MAX: i64 = 12 // the browser's own br_doc_ext bound 51const UD_ARGV_SLOTS: i64 = 6 52const UD_PTR_BYTES: i64 = 8 53const UD_SLACK: i64 = 4096 // headroom the page emitter keeps before refusing UD_ERR_OUT_CAP 54 55const UD_DOCX_ELF: *u8 = "_offc/nx_docx.elf" 56const UD_ODT_ELF: *u8 = "_offc/nx_odt.elf" 57const UD_PDFTEXT_ELF: *u8 = "_offc/nx_pdf_text.elf" 58const UD_PDF_TXT_SCRATCH: *u8 = "/tmp/nx_unidoc_pdf.txt" 59 60// ASCII the folders key on 61const UD_NL: i64 = 10 62const UD_CR: i64 = 13 63const UD_TAB: i64 = 9 64const UD_SP: i64 = 32 65const UD_HASH: i64 = 35 66const UD_DASH: i64 = 45 67const UD_STAR: i64 = 42 68const UD_PLUS: i64 = 43 69const UD_PIPE: i64 = 124 70const UD_SLASH: i64 = 47 71const UD_COMMA: i64 = 44 72const UD_QUOTE: i64 = 34 73const UD_DOT: i64 = 46 74const UD_LT: i64 = 60 75const UD_GT: i64 = 62 76const UD_AMP: i64 = 38 77const UD_APOS: i64 = 39 78const UD_BACKTICK: i64 = 96 79const UD_H: i64 = 72 80const UD_P: i64 = 80 81const UD_T: i64 = 84 82const UD_B: i64 = 66 83const UD_I: i64 = 73 84const UD_MD_HEADING_MAX: i64 = 6 85 86func ud_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 87func ud_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64 = 0; var o: i64 = off; while s[i] != (0 as u8) { dst[o] = s[i]; o = o + 1; i = i + 1 } return o } 88func ud_catn(dst: *u8, off: i64, v: i64) -> i64 { return nxi_buf(dst, off, v) } 89func ud_lower(c: i64) -> i64 { if c >= 65 { if c <= 90 { return c + 32 } } return c } 90func ud_streq(a: *u8, b: *u8) -> i64 { 91 var i: i64 = 0 92 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 93 if b[i] != (0 as u8) { return 0 } 94 return 1 95} 96// does hay[0..n) contain needle (NUL-terminated)? -1 if not, else the offset 97func ud_find(hay: *u8, n: i64, needle: *u8) -> i64 { 98 let nl: i64 = ud_slen(needle) 99 if nl == 0 { return 0 } 100 var i: i64 = 0 101 while i + nl <= n { 102 var j: i64 = 0 103 var m: i64 = 1 104 while j < nl { if hay[i + j] != needle[j] { m = 0; j = nl } else { j = j + 1 } } 105 if m == 1 { return i } 106 i = i + 1 107 } 108 return 0 - 1 109} 110func ud_count(hay: *u8, n: i64, needle: *u8) -> i64 { 111 let nl: i64 = ud_slen(needle) 112 if nl == 0 { return 0 } 113 var c: i64 = 0 114 var i: i64 = 0 115 while i + nl <= n { 116 var j: i64 = 0 117 var m: i64 = 1 118 while j < nl { if hay[i + j] != needle[j] { m = 0; j = nl } else { j = j + 1 } } 119 if m == 1 { c = c + 1; i = i + nl } else { i = i + 1 } 120 } 121 return c 122} 123 124// ---- THE SECURITY PRIMITIVE: every byte of document text passes through here before it is HTML ------------- 125// Five characters can change the meaning of a page; all five are turned into references. Everything else is 126// copied as-is (UTF-8 passes untouched). A document that CONTAINS the text "<script>" renders as that text. 127func ud_esc_n(out: *u8, off: i64, src: *u8, a: i64, b: i64) -> i64 { 128 var o: i64 = off 129 var i: i64 = a 130 while i < b { 131 let c: i64 = src[i] as i64 132 if c == UD_AMP { o = ud_cat(out, o, "&amp;" as *u8) } else { 133 if c == UD_LT { o = ud_cat(out, o, "&lt;" as *u8) } else { 134 if c == UD_GT { o = ud_cat(out, o, "&gt;" as *u8) } else { 135 if c == UD_QUOTE { o = ud_cat(out, o, "&quot;" as *u8) } else { 136 if c == UD_APOS { o = ud_cat(out, o, "&#39;" as *u8) } else { out[o] = src[i]; o = o + 1 } } } } } 137 i = i + 1 138 } 139 return o 140} 141func ud_esc(out: *u8, off: i64, s: *u8) -> i64 { return ud_esc_n(out, off, s, 0, ud_slen(s)) } 142 143// ---- kind detection: the lowercased extension of the last path segment ------------------------------------ 144func ud_ext(path: *u8, ext: *u8) -> i64 { 145 let n: i64 = ud_slen(path) 146 var dot: i64 = 0 - 1 147 var slash: i64 = 0 - 1 148 var i: i64 = 0 149 while i < n { 150 let c: i64 = path[i] as i64 151 if c == UD_SLASH { slash = i; dot = 0 - 1 } else { if c == UD_DOT { dot = i } } 152 i = i + 1 153 } 154 ext[0] = 0 as u8 155 if dot < 0 { return 0 } 156 if dot <= slash { return 0 } 157 var k: i64 = 0 158 i = dot + 1 159 while i < n { 160 if k >= UD_EXT_MAX { ext[0] = 0 as u8; return 0 } 161 ext[k] = ud_lower(path[i] as i64) as u8 162 k = k + 1 163 i = i + 1 164 } 165 ext[k] = 0 as u8 166 return k 167} 168func ud_kind_of_ext(ext: *u8) -> i64 { 169 if ud_streq(ext, "docx" as *u8) == 1 { return UD_KIND_DOCX } 170 if ud_streq(ext, "odt" as *u8) == 1 { return UD_KIND_ODT } 171 if ud_streq(ext, "pdf" as *u8) == 1 { return UD_KIND_PDF } 172 if ud_streq(ext, "md" as *u8) == 1 { return UD_KIND_MD } 173 if ud_streq(ext, "markdown" as *u8) == 1 { return UD_KIND_MD } 174 if ud_streq(ext, "csv" as *u8) == 1 { return UD_KIND_CSV } 175 if ud_streq(ext, "txt" as *u8) == 1 { return UD_KIND_TXT } 176 if ud_streq(ext, "text" as *u8) == 1 { return UD_KIND_TXT } 177 if ud_streq(ext, "html" as *u8) == 1 { return UD_KIND_HTML } 178 if ud_streq(ext, "htm" as *u8) == 1 { return UD_KIND_HTML } 179 return UD_KIND_UNKNOWN 180} 181func ud_kind_of_path(path: *u8) -> i64 { 182 let ext: *u8 = sys_mmap(UD_EXT_MAX + 4) 183 let k: i64 = ud_ext(path, ext) 184 if k == 0 { return UD_KIND_UNKNOWN } 185 return ud_kind_of_ext(ext) 186} 187func ud_kind_label(kind: i64) -> *u8 { 188 if kind == UD_KIND_DOCX { return "Word document" as *u8 } 189 if kind == UD_KIND_ODT { return "OpenDocument text" as *u8 } 190 if kind == UD_KIND_PDF { return "PDF document (text layer)" as *u8 } 191 if kind == UD_KIND_MD { return "Markdown" as *u8 } 192 if kind == UD_KIND_CSV { return "CSV table" as *u8 } 193 if kind == UD_KIND_TXT { return "Plain text" as *u8 } 194 if kind == UD_KIND_HTML { return "web page (refused as a document)" as *u8 } 195 return "unknown" as *u8 196} 197func ud_kind_name(kind: i64) -> *u8 { 198 if kind == UD_KIND_DOCX { return "docx" as *u8 } 199 if kind == UD_KIND_ODT { return "odt" as *u8 } 200 if kind == UD_KIND_PDF { return "pdf" as *u8 } 201 if kind == UD_KIND_MD { return "md" as *u8 } 202 if kind == UD_KIND_CSV { return "csv" as *u8 } 203 if kind == UD_KIND_TXT { return "txt" as *u8 } 204 if kind == UD_KIND_HTML { return "html" as *u8 } 205 return "unknown" as *u8 206} 207func ud_err_name(code: i64) -> *u8 { 208 if code == UD_ERR_UNKNOWN_KIND { return "UNKNOWN-KIND (no renderer for this extension; add a kind, never guess)" as *u8 } 209 if code == UD_ERR_REFUSED_HTML { return "REFUSED-HTML (a web page is served as itself, never wrapped as a document)" as *u8 } 210 if code == UD_ERR_RENDERER { return "RENDERER-FAILED (the format organ exited non-zero or handed back no spec)" as *u8 } 211 if code == UD_ERR_TOO_BIG { return "TOO-BIG (above the announced UD_IN_CAP of 8388608 bytes; refused, never cut)" as *u8 } 212 if code == UD_ERR_READ { return "READ-FAILED (the input path could not be read)" as *u8 } 213 if code == UD_ERR_OUT_CAP { return "OUT-CAP (the page would not fit the caller's buffer; refused, never truncated)" as *u8 } 214 return "OK" as *u8 215} 216// the input ceiling as a decidable predicate (0 = admitted, UD_ERR_TOO_BIG = refused) 217func ud_check_size(n: i64) -> i64 { if n > UD_IN_CAP { return UD_ERR_TOO_BIG } return 0 } 218 219// ---- spec folders: every kind becomes H/P/T lines (the office's document model) ------------------------------ 220// write one spec line: <tag> SP <text a..b with NL/TAB folded to SP and, in a table, PIPE folded to SLASH> NL 221func ud_spec_line(out: *u8, o0: i64, cap: i64, tag: i64, src: *u8, a: i64, b: i64, in_tbl: i64) -> i64 { 222 if o0 + (b - a) + 3 >= cap { return 0 - 1 } 223 var o: i64 = o0 224 out[o] = tag as u8; out[o + 1] = UD_SP as u8; o = o + 2 225 var k: i64 = a 226 while k < b { 227 var c: i64 = src[k] as i64 228 if c == UD_NL { c = UD_SP } 229 if c == UD_CR { c = UD_SP } 230 if c == UD_TAB { c = UD_SP } 231 if in_tbl == 1 { if c == UD_PIPE { c = UD_SLASH } } 232 out[o] = c as u8; o = o + 1; k = k + 1 233 } 234 out[o] = UD_NL as u8 235 return o + 1 236} 237// plain text: paragraphs separated by one or more blank lines 238func ud_spec_from_text(src: *u8, n: i64, out: *u8, cap: i64) -> i64 { 239 var o: i64 = 0 240 var ps: i64 = 0 - 1 // paragraph start, -1 = none open 241 var i: i64 = 0 242 while i <= n { 243 var e: i64 = i 244 while e < n { if src[e] == (UD_NL as u8) { break } e = e + 1 } 245 var end: i64 = e 246 if end > i { if src[end - 1] == (UD_CR as u8) { end = end - 1 } } 247 var blank: i64 = 1 248 var q: i64 = i 249 while q < end { let c: i64 = src[q] as i64; if c != UD_SP { if c != UD_TAB { blank = 0; q = end } } q = q + 1 } 250 if blank == 1 { 251 if ps >= 0 { let no: i64 = ud_spec_line(out, o, cap, UD_P, src, ps, i - 1, 0); if no < 0 { return UD_ERR_OUT_CAP } o = no; ps = 0 - 1 } 252 } else { if ps < 0 { ps = i } } 253 i = e + 1 254 } 255 if ps >= 0 { let no2: i64 = ud_spec_line(out, o, cap, UD_P, src, ps, n, 0); if no2 < 0 { return UD_ERR_OUT_CAP } o = no2 } 256 out[o] = 0 as u8 257 return o 258} 259// markdown: '#' headings -> H, list items -> P prefixed "- ", fenced code lines -> P, blank line ends a paragraph 260func ud_spec_from_md(src: *u8, n: i64, out: *u8, cap: i64) -> i64 { 261 var o: i64 = 0 262 var ps: i64 = 0 - 1 263 var infence: i64 = 0 264 var i: i64 = 0 265 while i < n { 266 var e: i64 = i 267 while e < n { if src[e] == (UD_NL as u8) { break } e = e + 1 } 268 var end: i64 = e 269 if end > i { if src[end - 1] == (UD_CR as u8) { end = end - 1 } } 270 var fence: i64 = 0 271 if end >= i + 3 { if src[i] == (UD_BACKTICK as u8) { if src[i + 1] == (UD_BACKTICK as u8) { if src[i + 2] == (UD_BACKTICK as u8) { fence = 1 } } } } 272 if fence == 1 { 273 if ps >= 0 { let f0: i64 = ud_spec_line(out, o, cap, UD_P, src, ps, i - 1, 0); if f0 < 0 { return UD_ERR_OUT_CAP } o = f0; ps = 0 - 1 } 274 if infence == 1 { infence = 0 } else { infence = 1 } 275 } else { if infence == 1 { 276 let f1: i64 = ud_spec_line(out, o, cap, UD_P, src, i, end, 0); if f1 < 0 { return UD_ERR_OUT_CAP } o = f1 277 } else { 278 var hn: i64 = 0 279 while i + hn < end { if src[i + hn] == (UD_HASH as u8) { hn = hn + 1 } else { break } } 280 var heading: i64 = 0 281 if hn >= 1 { if hn <= UD_MD_HEADING_MAX { if i + hn < end { if src[i + hn] == (UD_SP as u8) { heading = 1 } } } } 282 var list: i64 = 0 283 if end >= i + 2 { if src[i + 1] == (UD_SP as u8) { let c0: i64 = src[i] as i64; if c0 == UD_DASH { list = 1 } else { if c0 == UD_STAR { list = 1 } else { if c0 == UD_PLUS { list = 1 } } } } } 284 var blank: i64 = 1 285 var q: i64 = i 286 while q < end { let c: i64 = src[q] as i64; if c != UD_SP { if c != UD_TAB { blank = 0; q = end } } q = q + 1 } 287 if heading == 1 { 288 if ps >= 0 { let h0: i64 = ud_spec_line(out, o, cap, UD_P, src, ps, i - 1, 0); if h0 < 0 { return UD_ERR_OUT_CAP } o = h0; ps = 0 - 1 } 289 let h1: i64 = ud_spec_line(out, o, cap, UD_H, src, i + hn + 1, end, 0); if h1 < 0 { return UD_ERR_OUT_CAP } o = h1 290 } else { if list == 1 { 291 if ps >= 0 { let l0: i64 = ud_spec_line(out, o, cap, UD_P, src, ps, i - 1, 0); if l0 < 0 { return UD_ERR_OUT_CAP } o = l0; ps = 0 - 1 } 292 // "- " kept as the visible marker: the office model has no list block yet (the list-block watch on the unidoc board, rung UD5) 293 let l1: i64 = ud_spec_line(out, o, cap, UD_P, src, i, end, 0); if l1 < 0 { return UD_ERR_OUT_CAP } o = l1 294 } else { if blank == 1 { 295 if ps >= 0 { let b0: i64 = ud_spec_line(out, o, cap, UD_P, src, ps, i - 1, 0); if b0 < 0 { return UD_ERR_OUT_CAP } o = b0; ps = 0 - 1 } 296 } else { if ps < 0 { ps = i } } } } 297 } } 298 i = e + 1 299 } 300 if ps >= 0 { let z: i64 = ud_spec_line(out, o, cap, UD_P, src, ps, n, 0); if z < 0 { return UD_ERR_OUT_CAP } o = z } 301 out[o] = 0 as u8 302 return o 303} 304// csv: one T row per line, cells joined by PIPE; a quoted cell keeps its commas, "" inside quotes is one quote 305func ud_spec_from_csv(src: *u8, n: i64, out: *u8, cap: i64) -> i64 { 306 var o: i64 = 0 307 var i: i64 = 0 308 while i < n { 309 var e: i64 = i 310 while e < n { if src[e] == (UD_NL as u8) { break } e = e + 1 } 311 var end: i64 = e 312 if end > i { if src[end - 1] == (UD_CR as u8) { end = end - 1 } } 313 if end > i { 314 if o + (end - i) * 2 + 3 >= cap { return UD_ERR_OUT_CAP } 315 out[o] = UD_T as u8; out[o + 1] = UD_SP as u8; o = o + 2 316 var inq: i64 = 0 317 var k: i64 = i 318 while k < end { 319 let c: i64 = src[k] as i64 320 if c == UD_QUOTE { 321 if inq == 1 { if k + 1 < end { if src[k + 1] == (UD_QUOTE as u8) { out[o] = UD_QUOTE as u8; o = o + 1; k = k + 1 } else { inq = 0 } } else { inq = 0 } } else { inq = 1 } 322 } else { if c == UD_COMMA { if inq == 0 { out[o] = UD_PIPE as u8; o = o + 1 } else { out[o] = UD_COMMA as u8; o = o + 1 } } 323 else { if c == UD_PIPE { out[o] = UD_SLASH as u8; o = o + 1 } else { if c == UD_TAB { out[o] = UD_SP as u8; o = o + 1 } else { out[o] = c as u8; o = o + 1 } } } } 324 k = k + 1 325 } 326 out[o] = UD_NL as u8; o = o + 1 327 } 328 i = e + 1 329 } 330 out[o] = 0 as u8 331 return o 332} 333// the promoted format organs' `spec` verb, captured: everything between "-SPEC-BEGIN\n" and the "-SPEC-END len=" line 334func ud_spec_from_fork(elf: *u8, path: *u8, out: *u8, cap: i64) -> i64 { 335 let argv: *i64 = sys_mmap(UD_PTR_BYTES * UD_ARGV_SLOTS) as *i64 336 argv[0] = elf as i64 337 argv[1] = ("spec" as *u8) as i64 338 argv[2] = path as i64 339 argv[3] = 0 340 let capbuf: *u8 = sys_mmap(UD_CAPTURE_CAP) 341 let lenp: *i64 = sys_mmap(UD_PTR_BYTES * 2) as *i64 342 lenp[0] = 0 343 let rc: i64 = tr_run_capture(elf, argv, capbuf, UD_CAPTURE_CAP - 16, lenp) 344 let n: i64 = lenp[0] 345 if rc != 0 { return UD_ERR_RENDERER } 346 let b: i64 = ud_find(capbuf, n, "-SPEC-BEGIN\n" as *u8) 347 if b < 0 { return UD_ERR_RENDERER } 348 let s: i64 = b + 12 349 var e: i64 = ud_find(capbuf, n, "-SPEC-END len=" as *u8) 350 if e < 0 { return UD_ERR_RENDERER } 351 // the END marker starts its own line ("DOCX-SPEC-END" / "ODT-SPEC-END"): back up to the previous NL 352 while e > s { if capbuf[e - 1] == (UD_NL as u8) { break } e = e - 1 } 353 if e < s { return UD_ERR_RENDERER } 354 if e - s >= cap { return UD_ERR_OUT_CAP } 355 var o: i64 = 0 356 var i: i64 = s 357 while i < e { out[o] = capbuf[i]; o = o + 1; i = i + 1 } 358 out[o] = 0 as u8 359 return o 360} 361// pdf: the proven extractor writes a text file; control bytes other than NL/TAB/CR mark a non-text line, dropped 362func ud_spec_from_pdf(path: *u8, out: *u8, cap: i64) -> i64 { 363 let argv: *i64 = sys_mmap(UD_PTR_BYTES * UD_ARGV_SLOTS) as *i64 364 argv[0] = UD_PDFTEXT_ELF as i64 365 argv[1] = path as i64 366 argv[2] = UD_PDF_TXT_SCRATCH as i64 367 argv[3] = 0 368 let capbuf: *u8 = sys_mmap(UD_CAPTURE_CAP) 369 let lenp: *i64 = sys_mmap(UD_PTR_BYTES * 2) as *i64 370 lenp[0] = 0 371 let rc: i64 = tr_run_capture(UD_PDFTEXT_ELF, argv, capbuf, UD_CAPTURE_CAP - 16, lenp) 372 if rc != 0 { return UD_ERR_RENDERER } 373 let tl: *i64 = sys_mmap(UD_PTR_BYTES * 2) as *i64 374 tl[0] = 0 375 let txt: *u8 = sys_read_file(UD_PDF_TXT_SCRATCH, tl) 376 if (txt as i64) == 0 { return UD_ERR_RENDERER } 377 let tn: i64 = tl[0] 378 if tn <= 0 { return UD_ERR_RENDERER } 379 // fold: keep only lines free of control bytes, then paragraph on blank lines 380 let clean: *u8 = sys_mmap(tn + 16) 381 var co: i64 = 0 382 var i: i64 = 0 383 while i < tn { 384 var e: i64 = i 385 while e < tn { if txt[e] == (UD_NL as u8) { break } e = e + 1 } 386 var ok: i64 = 1 387 var q: i64 = i 388 while q < e { let c: i64 = txt[q] as i64; if c < UD_SP { if c != UD_TAB { if c != UD_CR { ok = 0; q = e } } } q = q + 1 } 389 if ok == 1 { var k: i64 = i; while k < e { clean[co] = txt[k]; co = co + 1; k = k + 1 } } 390 clean[co] = UD_NL as u8; co = co + 1 391 i = e + 1 392 } 393 return ud_spec_from_text(clean, co, out, cap) 394} 395// dispatch by kind; the caller has already read the bytes (so the size ceiling is decided once, here) 396func ud_spec_of(kind: i64, path: *u8, bytes: *u8, n: i64, out: *u8, cap: i64) -> i64 { 397 if kind == UD_KIND_DOCX { return ud_spec_from_fork(UD_DOCX_ELF, path, out, cap) } 398 if kind == UD_KIND_ODT { return ud_spec_from_fork(UD_ODT_ELF, path, out, cap) } 399 if kind == UD_KIND_PDF { return ud_spec_from_pdf(path, out, cap) } 400 if kind == UD_KIND_MD { return ud_spec_from_md(bytes, n, out, cap) } 401 if kind == UD_KIND_CSV { return ud_spec_from_csv(bytes, n, out, cap) } 402 if kind == UD_KIND_TXT { return ud_spec_from_text(bytes, n, out, cap) } 403 if kind == UD_KIND_HTML { return UD_ERR_REFUSED_HTML } 404 return UD_ERR_UNKNOWN_KIND 405} 406 407// ---- the page: spec -> zero-script HTML with a fail-closed CSP and the portal's actions as plain forms ---------- 408func ud_spec_to_html(out: *u8, off: i64, spec: *u8, sl: i64) -> i64 { 409 var o: i64 = off 410 var intable: i64 = 0 411 var i: i64 = 0 412 while i < sl { 413 var e: i64 = i 414 while e < sl { if spec[e] == (UD_NL as u8) { break } e = e + 1 } 415 let c0: i64 = spec[i] as i64 416 var cs: i64 = i 417 if e >= i + 2 { if spec[i + 1] == (UD_SP as u8) { cs = i + 2 } } 418 if c0 == UD_T { 419 if intable == 0 { o = ud_cat(out, o, "<div class=tbl><table><tbody>" as *u8); intable = 1 } 420 o = ud_cat(out, o, "<tr>" as *u8) 421 var cstart: i64 = cs 422 var k: i64 = cs 423 while k <= e { 424 if k == e { o = ud_cat(out, o, "<td>" as *u8); o = ud_esc_n(out, o, spec, cstart, k); o = ud_cat(out, o, "</td>" as *u8); k = k + 1 } 425 else { if spec[k] == (UD_PIPE as u8) { o = ud_cat(out, o, "<td>" as *u8); o = ud_esc_n(out, o, spec, cstart, k); o = ud_cat(out, o, "</td>" as *u8); cstart = k + 1; k = k + 1 } else { k = k + 1 } } 426 } 427 o = ud_cat(out, o, "</tr>\n" as *u8) 428 } else { 429 if intable == 1 { o = ud_cat(out, o, "</tbody></table></div>\n" as *u8); intable = 0 } 430 if c0 == UD_H { o = ud_cat(out, o, "<h2>" as *u8); o = ud_esc_n(out, o, spec, cs, e); o = ud_cat(out, o, "</h2>\n" as *u8) } 431 else { if c0 == UD_B { o = ud_cat(out, o, "<p><b>" as *u8); o = ud_esc_n(out, o, spec, cs, e); o = ud_cat(out, o, "</b></p>\n" as *u8) } 432 else { if c0 == UD_I { o = ud_cat(out, o, "<p><i>" as *u8); o = ud_esc_n(out, o, spec, cs, e); o = ud_cat(out, o, "</i></p>\n" as *u8) } 433 else { o = ud_cat(out, o, "<p>" as *u8); o = ud_esc_n(out, o, spec, cs, e); o = ud_cat(out, o, "</p>\n" as *u8) } } } 434 } 435 i = e + 1 436 } 437 if intable == 1 { o = ud_cat(out, o, "</tbody></table></div>\n" as *u8) } 438 return o 439} 440// house tokens on :root, referenced (never merely defined) below; mobile-first: one column, 24px targets, 441// wide tables scroll in their OWN container so the page never scrolls sideways 442func ud_css(out: *u8, off: i64) -> i64 { 443 var o: i64 = off 444 o = ud_cat(out, o, ":root{--nx-ink:#1a1d21;--nx-paper:#ffffff;--nx-ground:#eef1f5;--nx-band:#0f172a;--nx-band-ink:#ffffff;--nx-line:#cbd5e1;--nx-accent:#2563eb;--nx-muted:#64748b;--nx-wrap:46rem;--nx-target:44px}\n" as *u8) 445 o = ud_cat(out, o, "body{margin:0;background:var(--nx-ground);color:var(--nx-ink);font:16px/1.6 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}\n" as *u8) 446 o = ud_cat(out, o, "header{background:var(--nx-band);color:var(--nx-band-ink);padding:14px 18px}header .eyebrow{font-size:12px;letter-spacing:.08em;text-transform:uppercase;opacity:.8}header h1{margin:4px 0 0;font-size:19px;font-weight:600;word-break:break-word}\n" as *u8) 447 o = ud_cat(out, o, ".meta{max-width:var(--nx-wrap);margin:10px auto 0;padding:0 14px;color:var(--nx-muted);font-size:13px;word-break:break-all}\n" as *u8) 448 o = ud_cat(out, o, ".actions{max-width:var(--nx-wrap);margin:10px auto;padding:0 14px;display:flex;flex-wrap:wrap;gap:8px}.actions a,.actions button{display:inline-flex;align-items:center;min-height:var(--nx-target);padding:0 16px;border-radius:8px;border:1px solid var(--nx-line);background:var(--nx-paper);color:var(--nx-ink);text-decoration:none;font-size:15px}.actions button.primary{background:var(--nx-accent);color:var(--nx-band-ink);border-color:var(--nx-accent)}\n" as *u8) 449 o = ud_cat(out, o, ".actions form{display:flex;flex-wrap:wrap;gap:8px;align-items:center}.actions textarea{min-height:var(--nx-target);width:100%;max-width:var(--nx-wrap);border:1px solid var(--nx-line);border-radius:8px;padding:8px;font:inherit}.actions label{display:inline-flex;align-items:center;gap:8px;min-height:var(--nx-target)}.actions input[type=checkbox]{width:24px;height:24px}\n" as *u8) 450 o = ud_cat(out, o, "main.doc{max-width:var(--nx-wrap);margin:16px auto 40px;background:var(--nx-paper);border:1px solid var(--nx-line);border-radius:12px;padding:22px 18px}main.doc h2{font-size:20px;margin:18px 0 8px;border-bottom:1px solid var(--nx-line);padding-bottom:5px}main.doc h2:first-child{margin-top:0}main.doc p{margin:9px 0;overflow-wrap:anywhere}\n" as *u8) 451 o = ud_cat(out, o, ".tbl{overflow-x:auto;margin:14px 0}.tbl table{border-collapse:collapse;min-width:100%}.tbl td{border:1px solid var(--nx-line);padding:7px 10px;vertical-align:top;font-size:15px}\n" as *u8) 452 o = ud_cat(out, o, "footer{max-width:var(--nx-wrap);margin:0 auto 40px;padding:0 14px;color:var(--nx-muted);font-size:13px;text-align:center}\n" as *u8) 453 return o 454} 455// the page. title/kind/dlurl/base/sha are NUL-terminated; base "" = no portal chrome (pure viewer); sha "" = omitted. 456// Refuses UD_ERR_OUT_CAP rather than emitting a cut page. 457func ud_page(out: *u8, cap: i64, title: *u8, kind: i64, nbytes: i64, spec: *u8, sl: i64, dlurl: *u8, base: *u8, sha: *u8) -> i64 { 458 // a spec line can grow ~6x under escaping (every char a reference) plus tags; bound before writing, never after 459 if sl * 8 + UD_SLACK * 4 >= cap { return UD_ERR_OUT_CAP } 460 var o: i64 = 0 461 o = ud_cat(out, o, "<!DOCTYPE html>\n<html lang=en><head><meta charset=utf-8>\n<meta name=viewport content=\"width=device-width,initial-scale=1\">\n" as *u8) 462 o = ud_cat(out, o, "<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; form-action 'self'; base-uri 'none'\">\n" as *u8) 463 o = ud_cat(out, o, "<title>" as *u8); o = ud_esc(out, o, title); o = ud_cat(out, o, " - Nishi Docs</title>\n<style>\n" as *u8) 464 o = ud_css(out, o) 465 o = ud_cat(out, o, "</style></head>\n<body>\n<header><div class=eyebrow>Nishi Docs</div><h1>" as *u8) 466 o = ud_esc(out, o, title) 467 o = ud_cat(out, o, "</h1></header>\n<div class=meta>" as *u8) 468 o = ud_esc(out, o, ud_kind_label(kind)) 469 o = ud_cat(out, o, " &middot; " as *u8); o = ud_catn(out, o, nbytes); o = ud_cat(out, o, " bytes" as *u8) 470 if sha[0] != (0 as u8) { o = ud_cat(out, o, " &middot; sha256 " as *u8); o = ud_esc(out, o, sha) } 471 o = ud_cat(out, o, " &middot; rendered by nx_unidoc, no script</div>\n<div class=actions>\n" as *u8) 472 if dlurl[0] != (0 as u8) { o = ud_cat(out, o, "<a href=\"" as *u8); o = ud_esc(out, o, dlurl); o = ud_cat(out, o, "\">Original bytes</a>\n" as *u8) } 473 if base[0] != (0 as u8) { 474 o = ud_cat(out, o, "<a href=\"" as *u8); o = ud_esc(out, o, base); o = ud_cat(out, o, "/versions\">Versions</a>\n" as *u8) 475 o = ud_cat(out, o, "<form method=post action=\"" as *u8); o = ud_esc(out, o, base); o = ud_cat(out, o, "/annotate\"><textarea name=note placeholder=\"Add a note for the firm\" aria-label=\"Note\"></textarea><button type=submit>Add note</button></form>\n" as *u8) 476 o = ud_cat(out, o, "<form method=post action=\"" as *u8); o = ud_esc(out, o, base); o = ud_cat(out, o, "/sign\"><label><input type=checkbox name=consent value=yes> I intend to sign and consent to sign electronically</label><button type=submit class=primary>Sign this version</button></form>\n" as *u8) 477 } 478 o = ud_cat(out, o, "</div>\n<main class=doc>\n" as *u8) 479 o = ud_spec_to_html(out, o, spec, sl) 480 o = ud_cat(out, o, "</main>\n<footer>The document is shown as a page: no script runs, nothing is fetched from elsewhere, and the original bytes are always one link away.</footer>\n</body></html>\n" as *u8) 481 if o + UD_SLACK >= cap { return UD_ERR_OUT_CAP } 482 out[o] = 0 as u8 483 return o 484} 485// title = the last path segment 486func ud_title_of(path: *u8, out: *u8) -> i64 { 487 let n: i64 = ud_slen(path) 488 var s: i64 = 0 489 var i: i64 = 0 490 while i < n { if path[i] == (UD_SLASH as u8) { s = i + 1 } i = i + 1 } 491 var o: i64 = 0 492 i = s 493 while i < n { out[o] = path[i]; o = o + 1; i = i + 1 } 494 out[o] = 0 as u8 495 return o 496} 497// THE WHOLE PIPELINE: path -> kind -> bytes (ceiling decided once) -> spec -> page. Returns the page length, 498// or a NAMED negative code. kind_out[0] receives the detected kind even on refusal (so a caller can say why). 499func ud_render_path(path: *u8, dlurl: *u8, base: *u8, out: *u8, cap: i64, kind_out: *i64) -> i64 { 500 let kind: i64 = ud_kind_of_path(path) 501 kind_out[0] = kind 502 if kind == UD_KIND_UNKNOWN { return UD_ERR_UNKNOWN_KIND } 503 if kind == UD_KIND_HTML { return UD_ERR_REFUSED_HTML } 504 let lenp: *i64 = sys_mmap(UD_PTR_BYTES * 2) as *i64 505 lenp[0] = 0 506 let bytes: *u8 = sys_read_file(path, lenp) 507 if (bytes as i64) == 0 { return UD_ERR_READ } 508 let n: i64 = lenp[0] 509 if n < 0 { return UD_ERR_READ } 510 let sz: i64 = ud_check_size(n) 511 if sz != 0 { return sz } 512 let spec: *u8 = sys_mmap(UD_SPEC_CAP + 16) 513 let sl: i64 = ud_spec_of(kind, path, bytes, n, spec, UD_SPEC_CAP) 514 if sl < 0 { return sl } 515 let title: *u8 = sys_mmap(UD_PATH_CAP) 516 ud_title_of(path, title) 517 return ud_page(out, cap, title, kind, n, spec, sl, dlurl, base, "" as *u8) 518}