content: read a site root into bundles
Bundle loading with no HTTP: walk content/, split YAML frontmatter, derive an NFC-normalised key and a language from the filename, and lift only title out of frontmatter so every other key stays readable through Extra (ADR-0002). Path safety is os.Root rather than a hand-rolled cleaner (ADR-0031). os.DirFS documents that it does not prevent symlink escape; os.Root refuses any name resolving outside the root, so the guard is a property of the type instead of a check to remember at each call site. Test: a symlink to a file above the root cannot be read. This clears the traversal item off the latent list. A bundle that will not parse is logged and skipped, never fatal (ADR-0029), as is a key claimed by two spellings of one variant (ADR-0021). Bundle carries only Key, Lang, Path, Title, Body and Extra; Date, Slug, Draft and Aliases arrive with the features that read them.
This commit is contained in:
@@ -354,3 +354,16 @@ Consequence: cheap now, and permanent-ish once the module path is fetched by any
|
||||
a module rename after publication needs a redirect or a major-version bump, and a deployed binary name
|
||||
appears in service files and container tags.
|
||||
Revisit if: never. Renaming again costs strictly more than this did.
|
||||
|
||||
## ADR-0031 — Path safety is `os.Root`, not a hand-rolled check
|
||||
Date: 2026-07-30 · Status: accepted
|
||||
Decision: every read of the site root goes through an `*os.Root` obtained by `os.OpenRoot` (Go 1.24+).
|
||||
No code cleans, joins or validates a request path itself, and `os.DirFS` is not used for the site root.
|
||||
Why: `os.Root` refuses any name resolving outside the root, including through a symlink; `os.DirFS`
|
||||
documents that it does *not* prevent symlink escape. So the traversal guard that has sat on the latent
|
||||
list becomes a property of the type rather than a check somebody has to remember at every call site —
|
||||
which is the only version that survives twenty features.
|
||||
Consequence: cheap — the guard cannot be forgotten, and the test is one symlink. Expensive — reads must
|
||||
go through the root handle, so no helper may take a `string` path and open it directly, and the root is
|
||||
held for the life of the process.
|
||||
Revisit if: never. A hand-rolled cleaner is strictly worse.
|
||||
|
||||
+35
-5
@@ -7,9 +7,11 @@ If this file disagrees with the code, the code is right and this file is a bug.
|
||||
|
||||
| File | Purpose | LOC |
|
||||
|---|---|---|
|
||||
| `go.mod` | module `khosra`; `x/text` and `yaml.v3` required, not yet imported | 8 |
|
||||
| `go.mod` | module `khosra`; `x/text`, `yaml.v3` direct | 8 |
|
||||
| `internal/content/content.go` | site root → bundles: `os.Root` open, walk, frontmatter split, key/lang derivation, NFC, collision drop | 217 |
|
||||
| `internal/content/content_test.go` | table-driven; symlink-escape evidence for the path guard | 155 |
|
||||
|
||||
No Go source yet. This repo holds engine source only — the site root is external and passed with `-site`
|
||||
No HTTP yet. This repo holds engine source only — the site root is external and passed with `-site`
|
||||
(ADR-0011).
|
||||
|
||||
Dependencies: none.
|
||||
@@ -28,9 +30,9 @@ this change*.
|
||||
| Effects | 0 | **2** | Effect runner + trigger wiring (change / schedule / demand) |
|
||||
| Extensions | 0 | **3** | Extension registry + wire file (`extensions.md`) |
|
||||
| Interface implementations | — | **2** | The interface itself |
|
||||
| Non-stdlib dependencies | 0 | budget in `scripts/budgets.env` | — |
|
||||
| Non-stdlib dependencies | 2 direct, 7 modules | budget in `scripts/budgets.env` | — |
|
||||
|
||||
Allowlisted but not yet required: `goldmark` (markdown), `golang.org/x/text` (NFC, ADR-0015),
|
||||
Allowlisted, in use: `goldmark` is not yet imported. Allowlist: `goldmark` (markdown), `golang.org/x/text` (NFC, ADR-0015),
|
||||
`gopkg.in/yaml.v3` (frontmatter, ADR-0020).
|
||||
|
||||
## Latent items — known, deliberately unfixed
|
||||
@@ -41,7 +43,6 @@ with a stated reason. A list nothing drains is a graveyard of known defects.
|
||||
|
||||
| Item | Why it waits | Trigger to fix |
|
||||
|---|---|---|
|
||||
| Path traversal guard on URL → file mapping | Not yet internet-facing | **Before first public deploy — hard blocker; the target is a real server (ADR-0010). Nothing mechanical enforces this — `verify.sh` does not read this list. Make it a table-driven test when the first file read lands.** |
|
||||
| No mechanical check that the counters are *correct* | The coupling gate makes forgetting them impossible, which is the real failure mode; checking values needs code to count | 3rd transform or 2nd route |
|
||||
| No mechanical gate on the untrusted boundary (ADR-0003) | Nothing untrusted exists yet | The comment path, Arc 3 — a test that untrusted input reaches no shortcode or template evaluation |
|
||||
|
||||
@@ -51,6 +52,35 @@ None. Every decision the engine needs before Arc 1 and before the first deploy i
|
||||
|
||||
Every ADR in `decisions.md` is accepted; none is open or proposed.
|
||||
|
||||
## Build queue
|
||||
|
||||
The prompt sequence to a Grav-level engine. **Completed through 1.** Update this line as each lands; a
|
||||
context refresh loses the conversation, not the plan.
|
||||
|
||||
- [x] 0 · ADRs: pagination shape (0028), parse-failure policy (0029), rename (0030), path guard (0031)
|
||||
- [x] 1 · bundle loading: `os.Root`, frontmatter, key/lang, NFC, collisions, skip-loudly
|
||||
- [ ] 2 · serve a bundle at its permalink: `-site`, trailing-slash redirect, goldmark, reference theme
|
||||
- [ ] 3 · language variants: `/bn/…`, fallback chain, `/en/…` redirect *(2nd routing case → resolver)*
|
||||
- [ ] 4 · aliases
|
||||
- [ ] 5 · section index pages, paginated *(first collection page → Query)*
|
||||
- [ ] 6 · settings cascade *(re-adopt from `ideas/deferred-decisions.md`)*
|
||||
- [ ] 7 · declared content types *(re-adopt)*
|
||||
- [ ] 8 · template composition: per-type sets, block override, site `templates/`
|
||||
- [ ] 9 · tags: global namespace, tag listings *(re-adopt)*
|
||||
- [ ] 10 · sequences: series, sparse order, prev/next/archive
|
||||
- [ ] 11 · typography + Bengali numerals transforms
|
||||
- [ ] 12 · shortcodes *(3rd transform → Stage pipeline)*
|
||||
- [ ] 13 · image derivatives *(first Effect)*
|
||||
- [ ] 14 · feeds: primary, per-section, per-tag *(2nd Effect → Effect runner)*
|
||||
- [ ] 15 · sitemap, robots, OpenGraph/JSON-LD
|
||||
- [ ] 16 · in-process page cache with the validity record *(re-adopt)*
|
||||
- [ ] 17 · `check` command
|
||||
- [ ] 18 · `new` command
|
||||
- [ ] 19 · `-dev`: reveal drafts and future-dated, template reload
|
||||
- [ ] 20 · extras *(re-adopt)*
|
||||
- [ ] 21 · polled change detection + Dockerfile
|
||||
- [ ] 22 · complete the reference theme against the full contract
|
||||
|
||||
## Arc retro log
|
||||
|
||||
One line per completed arc: what it cost, what it taught, what it made unnecessary.
|
||||
|
||||
Reference in New Issue
Block a user