1 From 22659315652abb18b9442b0d8062efbd07e88f9b Mon Sep 17 00:00:00 20012 From: gl0w <gl0w@world>3 Date: Thu, 10 Sep 2026 04:58:46 +01004 Subject: [PATCH] R1: deps decision (pulldown-cmark .13.4, yaml_serde,5 hand-rolled dates, std serve)6 7 ---8 docs/deps-decision.md | 109 ++++++++++++++++++++++++++++++++++++++++++9 1 file changed, 109 insertions(+)10 create mode 100644 docs/deps-decision.md11 12 diff --git a/docs/deps-decision.md b/docs/deps-decision.md13 new file mode 10064414 index 0000000..1bf93b315 --- /dev/null16 +++ b/docs/deps-decision.md17 @@ -0,0 +1,109 @@18 +# hu13 deps decision (M1/R1 spike, f0rge issue #1, read-only)19 +20 +Spiked 2026-09-10 in scratch cargo project under `/tmp` (not in repo)21 +against real f0rge data at `~/src/code/gl0w-w0rld/f0rge/.forge`.22 +Total direct deps: **4** (limit ≤ 5). All pins exact (`=`), f0rge-style.23 +24 +## Pinned set25 +26 +```toml27 +[dependencies]28 +pulldown-cmark = "=0.13.4"29 +serde = { version = "=1.0.229", features = ["derive"] }30 +serde_json = "=1.0.151"31 +yaml_serde = "=0.10.7"32 +# dates: hand-rolled, zero deps (copy f0rge-core is_rfc3339_utc shape)33 +# serve: std only (TcpListener + thread-per-connection), no tokio/hyper34 +```35 +36 +`serde`/`serde_json` pins match f0rge workspace pins on purpose37 +(`f0rge/Cargo.toml`: `serde =1.0.229`, `serde_json =1.0.151`).38 +`yaml_serde =0.10.7` is f0rge's YAML pin (the maintained39 +`serde_yaml` rename); spike ran on `serde_yaml =0.9.34` then40 +verified `yaml_serde =0.10.7` drop-in (see §2).41 +42 +## 1. Markdown → HTML: pulldown-cmark =0.13.443 +44 +Why: CommonMark-faithful, zero-JS output, no network/build deps,45 +`Options::empty()` is enough for f0rge bodies + wiki; already proven46 +at this version via `cargo fetch` + `cargo run`.47 +48 +Spike: rendered real `issues/<uuid>/issue.md` body (164 B md →49 +171 B html), real `prs/<uuid>/pr.md` body (202 B → 210 B), and full50 +`wiki/docs/PLAN.md` (21598 B → 25377 B). All produced `<p>`/`<h1>`.51 +Code spans escape correctly (`` `<script>` `` → `<code><script></code>`).52 +53 +XSS finding (honest): **raw `<script>` is NOT escaped by default.**54 +Probe `# hi / <script>alert(1)</script> / <img src=x onerror=…>` →55 +`"<h1>hi</h1>\n<script>alert(1)</script>\n<img …>\n"`56 +(raw present, escaped absent). This is correct CommonMark57 +passthrough, but wrong for hu13's untrusted inbox-adjacent bodies.58 +Fix (spiked, zero extra dep): map59 +`Event::Html(h)/InlineHtml(h) → Event::Html(escape_html(&h))`60 +with a 5-line hand-rolled `escape_html` (`&<>"'`), since mapping to61 +`Text` double-escapes. Verified safe output:62 +`"<script>alert(1)</script>"`, raw absent, legit63 +`**bold**`/lists intact. Diffs/logs never go through markdown —64 +always `<pre>` + same `escape_html`.65 +66 +Fallback: `pulldown-cmark =0.12.1` (same `Parser/html::push_html`67 +API for our use). Never comrak (bigger deps, server-side novelty68 +we don't need).69 +70 +## 2. Frontmatter: split `---` + yaml_serde =0.10.771 +72 +Why: f0rge schema is `---` fences + YAML body; `yaml_serde` is the73 +maintained `serde_yaml` (0.9.x is `+deprecated`), same API, and the74 +exact crate f0rge-core pins, so hu13 parses byte-identically.75 +76 +Spike (`serde_yaml =0.9.34`, `serde =1.0.229`, `serde_json =1.0.151`):77 +25/25 real files parsed — 12× `issues/*/issue.md` + 5× `prs/*/pr.md`78 ++ 6× `tasks/*.md` + `labels.md` (4 entries, YAML list, no fences) +79 +`milestones.md` (2 entries). Typed structs with `#[serde(default)]`80 +for optional `labels/assignees/milestone/updated`.81 +Equivalence probe on `yaml_serde =0.10.7`: unknown field tolerated,82 +`missing field \`id\`` error identical — drop-in confirmed.83 +84 +- Unknown fields tolerated: `zzz_future: 1` → `Ok` (true).85 +- Missing required field names file+field: hu13 formats as86 + `{basename}: {serde msg}` → e.g. `missing-title.md: missing field \`title\``87 + (`msg.contains("title")` true), matching f0rge `MissingField{file,field}`.88 +- Zero panics: 10 hostile inputs via `catch_unwind`, 0 panics —89 + `spec/fixtures/invalid/{bad-ts,bad-uuid,missing-title}.md`,90 + `spec/fixtures/sec/alias-bomb.md`, golden `issue.md` (control ok),91 + plus `""`, `---/---`, unclosed fence (→ `nofront`), NUL bytes92 + (→ clean `Err`, no panic), 200 KB single line (→ `Ok`, cap handles93 + before parse per schema SEC-01 256 KiB).94 +95 +Fallback: `serde_yaml =0.9.34` (identical API, deprecated warning96 +only). Keep f0rge's pre-parse guards (size cap, indent/flow depth,97 +alias count) in hu13 build — YAML lib alone is not the guard.98 +99 +## 3. Dates: hand-rolled RFC3339-UTC (no chrono/jiff) + serde_json =1.0.151100 +101 +Why hand-rolled: schema needs only validation + `(created,id)` sort,102 +and lexicographic == chronological for fixed-width UTC output.103 +f0rge-core already proves it (`is_rfc3339_utc`, `unix_to_rfc3339`,104 +`now_rfc3339`, `forbid(unsafe_code)`); adding chrono/jiff buys105 +arithmetic/timezones hu13 never uses, costs a 5th dep + binary size.106 +Spike validator (f0rge shape: `YYYY-MM-DDTHH:MM:SS[.1-9frac]Z`):107 +23/23 real `created` fields ok, 0 bad; `08/09/2025 02:00` correctly108 +rejected. `serde_json` roundtrip (`json!` → `to_string` →109 +`from_str`) ok.110 +111 +Fallback: `chrono =0.4.x` with `features=["serde"]` + `DateTime<Utc>`112 +if arithmetic ever needed; prefer jiff over chrono only if a date113 +dep becomes mandatory (smaller, safer API). Not needed for v1.114 +115 +## 4. Serve: std TcpListener + thread-per-connection, no tokio/hyper116 +117 +No code in this spike, decision only: the only dynamic surface is118 +2 write-only text POST routes (issue/PR inbox → `/data/inbox`,119 +never read back over HTTP, never rendered). Traffic is human +120 +occasional agent curl — single-digit req/s. A std `TcpListener`121 +accept loop + `thread::spawn` per connection + ~50-line HTTP/1.1122 +parser (`Content-Length` only, urlencoded form, 256 KiB cap, no123 +keep-alive tricks) is enough; static site is prebuilt files.124 +tokio/hyper add async runtime + dep tree for zero benefit here.125 +Fallback: `tiny_http` single-file crate if std parser ever feels126 +hand-rolled-critical — still no async.127 -- 128 2.55.0129