Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_sbj/SPEC.md

28.6 KiB, 1 run

created by r1870400018:21940, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1# SBJ v0 — Signed Binary JDAT
2
3The file format of the oxeweb. An SBJ file is a signed envelope wrapping a tree
4of typed nodes, encoded in BDAT (JDAT's binary form).
5
6This specification is normative. Where it and the implementation disagree, the
7implementation is wrong. The conformance fixtures in `fixtures/` are its teeth.
8
9Companion design document: `~/usr/complement/projects/oxegen/doc/Oxeweb`.
10Implementation plan: `~/usr/complement/projects/oxegen/plan/oxeweb_impl.md`.
11
12---
13
14## 1. File layout
15
16```
17+---------+------------+---------------------+---------------+
18| header | envelope | tree, in BDAT | index |
19| | key, time, | | (optional, |
20| 8 bytes | hash, sig | | derived) |
21+---------+------------+---------------------+---------------+
22 \___________________/
23 hashed and signed
24```
25
26Read in order: header, envelope, tree, then anything trailing.
27
28**The hash covers the tree region only.** Not the header, not the envelope, not
29the index. Two files carrying the same tree are the same document at the same
30address, whether or not either carries an index, and whoever holds a document may
31compute, append, or discard its index freely.
32
33### 1.1 Header
34
358 bytes, fixed:
36
37| Offset | Bytes | Value | Meaning |
38|---|---|---|---|
39| 0 | 4 | `0x53 0x42 0x4A 0x00` (`SBJ\0`) | Magic |
40| 4 | 2 | `u16` big-endian | Format major version. v0 is `0` |
41| 6 | 2 | `u16` big-endian | Length of the envelope region, in bytes |
42
43A reader that does not recognise the magic, or that reads a major version it does
44not implement, stops. It does not guess.
45
46### 1.2 Envelope
47
48A BDAT-encoded `Dat::Map` with exactly these keys, all required:
49
50| Key | Type | Meaning |
51|---|---|---|
52| `"schema"` | `str` | Payload schema, e.g. `"oxeweb/doc/0"` |
53| `"author"` | `bu8` | Author's public key, raw bytes |
54| `"sig_scheme"` | `u32` | Namex id of the signature scheme |
55| `"hash_scheme"` | `u32` | Namex id of the hash scheme |
56| `"time"` | `u64` | Unix milliseconds |
57| `"hash"` | `bu8` | Hash of the tree region |
58| `"sig"` | `bu8` | Signature over the signing input (§1.3) |
59| `"tree_len"` | `c64` | Length of the tree region, in bytes |
60
61The envelope map obeys the canonical encoding rules of §3, like everything else.
62
63The `schema` key is what makes SBJ general. An oxeweb document declares
64`"oxeweb/doc/0"` and its payload is a node tree (§4). A signed administrative
65command would declare its own schema and carry a different payload. The container
66does not care.
67
68v0 defaults: Ed25519 signatures (matching the keys an oxenym already holds) and
69SHA3-256 hashes (which is what `fe2o3_hash` implements). Both are named, never
70assumed. A signature scheme may be replaced freely, since a signature is checked
71once and discarded. A hash scheme may not, because **the hash is the address**.
72
73### 1.3 Signing input
74
75The signature covers, in this order, with no separators:
76
77```
78schema length (u32 BE) || schema bytes || sig_scheme (u32 BE) || hash_scheme (u32 BE)
79 || time (u64 BE) || hash bytes
80```
81
82Signing the hash rather than the tree is what binds the document's permanent
83address to its author. Including the schema and the scheme ids stops an attacker
84re-labelling a signed payload as a different schema, or claiming a weaker hash
85function produced the same address.
86
87**The schema carries its length because it is variable-length and it is not the
88last field.** Without the prefix the preimage is ambiguous: `schema` and `hash`
89are both variable-length, with only fixed-width fields between them, so a byte
90moved from the front of one field into the back of the one before it produces the
91same bytes under a different reading. Two envelopes agreeing on nothing — a
92different schema, a different pair of scheme ids, a different time and a different
93hash — can share one signing input, and therefore one signature. A signature over
94an ambiguous preimage does not say what the signer meant it to say, which is
95exactly the property §1.3 exists to provide.
96
97Whether that is reachable depends on what else the format admits, and v0 makes it
98hard rather than impossible: one hash scheme, of one digest width, means the
99alternative reading needs a hash of the wrong length, which §2 step 4 rejects.
100The prefix is here because that is a fact about today's vocabulary and not about
101the construction. A second hash scheme of a different width, or a second schema,
102removes the accident that is doing the work. The moment to fix a preimage is
103before there is more than one thing signed under it.
104
105Only `schema` needs the prefix. `hash` is variable-length too, but it is the last
106field, so its extent is whatever remains — there is nothing after it to steal
107from or lend to.
108
109### 1.4 Index (optional)
110
111If present, the region after the tree is a BDAT-encoded `Dat::Map` from node id
112(`c64`) to byte offset from the start of the tree region (`c64`). It is derived
113data, outside the hash, and is never trusted: whatever it points at is verified
114by decoding it. A reader may ignore it entirely.
115
116---
117
118## 2. Verification order
119
120A document is verified before it is parsed, and content that fails is never
121parsed at all.
122
1231. Read the header. Check magic and major version.
1242. Decode the envelope. Check every required key is present and typed correctly.
1253. Check `tree_len` against the bytes available. A tree region shorter or longer
126 than declared is a rejection, not a truncation.
1274. Hash the tree region with the named hash scheme. Compare with `hash`.
128 Mismatch is a rejection.
1295. Verify `sig` over the signing input (§1.3) under `author`. Failure is a
130 rejection.
1316. Only now decode the tree, enforcing the depth limit (§5) *during* decoding.
1327. Validate the decoded tree against the schema and the remaining limits (§5).
133
134Steps 1 through 5 touch no content. A caller may perform them and never decode.
135
136---
137
138## 3. Canonical encoding
139
140The hash is taken over the encoded bytes, so a document must encode to exactly
141one byte string, or it has more than one address. Non-canonical bytes are
142**rejected**, never accepted and silently re-encoded.
143
1441. **Field types are fixed by the schema.** A heading's `level` is a `u8`. JDAT
145 would happily encode the number 2 as a `u8` (two bytes) or an `i32` (five),
146 and both decode to the same heading level, giving one document two addresses.
147 The schema decides, and the decoder checks what it got.
1482. **Maps are `Dat::Map`**, never `Dat::OrdMap`. `Dat::Map` is a `BTreeMap`, so
149 key order follows the keys. `OrdMap` follows the author's typing.
1503. **Map keys are strings** (`Dat::Str`), lowercase ASCII, and no key may appear
151 twice.
1524. **No redundant wrappers.** No `Dat::Box`. `Dat::Opt` only where the schema
153 declares a field optional, and an absent optional field is omitted from the
154 map rather than encoded as `none`.
1555. **Strings are well-formed UTF-8, and in Unicode NFC**, with no unpaired
156 surrogates and no control characters (the Unicode `Cc` category: C0, C1, and
157 delete) other than tab and newline. Carriage return is rejected too, so one
158 line ending has one encoding and cannot split a document's address from its
159 twin.
160
161 NFC is what closes the last route by which one logical document could hold two
162 addresses. The letter é may be written as a single code point, or as an `e`
163 followed by a combining acute accent. The two display identically, mean the
164 same thing, and hash differently. Requiring the composed form makes the map
165 from a document's meaning to its address a function again. The normalisation
166 is `fe2o3_text`'s, over tables generated from a pinned Unicode Character
167 Database, and it is verified against the Unicode Consortium's own conformance
168 suite.
1696. **Integers are exactly the declared width.** No promotion, no demotion.
1707. **Lists are `Dat::List`**, not `Dat::Vek`, even where every element shares a
171 kind.
1728. **Nothing is carried that has no effect.** A thing a reader would render
173 identically whether it were present or absent gives one document two
174 encodings, and so two addresses. Three such things exist, and all are
175 rejected:
176 - a `styles` table that is empty,
177 - a style record that is empty,
178 - a style defined in the table that no node references.
179
180 This is the same rule as rule 4's "no redundant wrappers", applied to the
181 style table rather than to the encoding. It is stated separately because it
182 cannot be checked while decoding: whether a style is referenced is only known
183 once the whole tree has been walked.
184
185The authoring compiler canonicalises before signing. The reader pays nothing,
186since it is hashing the bytes anyway.
187
188---
189
190## 4. The node tree (`oxeweb/doc/0`)
191
192### 4.1 How a node carries its kind
193
194A node is a JDAT `usr` daticle: a `u16` kind code, then a `Dat::Map` of fields.
195
196```
197(heading|{
198 "level": (u8|2),
199 "children": [(text|"Style without a cascade")],
200})
201```
202
203The kind code sits *in front of* the payload on the wire, so the decoder knows a
204node is a heading before it reads a byte of the heading. An unknown or forbidden
205kind is refused on the spot, and the byte length JDAT puts in front of every
206compound says exactly how far to seek to reach the next node. A `"kind"` field
207inside the map would require reading the node to find out whether it was allowed
208to.
209
210### 4.2 v0 node kinds
211
212Deliberately small. Growth beyond this is handled two ways: a compatible addition
213carries a fallback (§4.5), and a breaking change bumps the schema version.
214
215| Code | Kind | Fields | Children |
216|---|---|---|---|
217| 1 | `doc` | `title: str`, `lang: str`, `styles: map?` | flow* |
218| 2 | `section` | `title: str?` | flow* |
219| 3 | `para` | | inline* |
220| 4 | `heading` | `level: u8` (1..=6) | inline* |
221| 5 | `list` | `ordered: bool` | `item`+ |
222| 6 | `item` | | flow* |
223| 7 | `box` | | flow* |
224| 8 | `image` | `hash: b32`, `alt: str`, `w: u32?`, `h: u32?` | none |
225| 9 | `text` | *(the daticle is a `str`, not a map)* | none |
226| 10 | `emph` | `strong: bool` | inline* |
227| 11 | `link` | `to: address` | inline* |
228| 12 | `code` | `lang: str?`, `text: str` | none |
229| 13 | `quote` | `cite: str?` | flow* |
230
231*flow* is `section`, `para`, `heading`, `list`, `box`, `image`, `code`, `quote`.
232*inline* is `text`, `emph`, `link`.
233
234Three further codes are **reserved**. They name facilities the engine has and a
235document does not:
236
237| Code | Kind | Fields | What it is | Where it is legal |
238|---|---|---|---|---|
239| 14 | `edit` | `name: str`, `placeholder: str?` | An editable text field | A chrome or an application tree |
240| 15 | `surface` | `app: b32`, `alt: node+` | A pane an application paints | An application tree |
241| 16 | `icon` | `name: str` | One of the engine's own icons | A chrome or an application tree |
242
243**`oxeweb/doc/0` admits the kinds 1 to 13 and no others.** Its admitted set is
244closed. The chrome's address bar and an application's form field are the same engine
245facility, reached through the same `edit`, and a document simply cannot name it: a
246document carrying any of the three is refused, whole, by the same rule that refuses a
247`para` inside a `para`. That is what makes "a document is never a program" structural
248rather than conventional. The kinds are reserved here, in the document's own schema,
249so that the vocabulary a document is held to cannot grow into them by accident, and
250so that a reader meeting one knows what it is refusing.
251
252An `icon` is reserved for a reason of its own, and it is not that an icon is
253dangerous. A document carries a picture as an `image`, which is a content hash: the
254picture is the author's, it is held, and it is the same picture wherever it is read.
255An icon is the opposite — a name by which a tree reaches the *reader's* drawing. A
256document naming one would be letting whichever reader opened it supply the document's
257content, and two readers would show two documents. Style may be the reader's, because
258style is how a thing looks; content may not, because content is what the author said.
259The icon names are also browser actions, and `back` means nothing in a document.
260
261An `icon` names one of a **closed set**, and a name outside it is refused rather than
262drawn as a gap:
263
264```
265back forward home add close find
266```
267
268The set is closed for the same reason a reserved code is refused where an unknown
269code may be admitted by a fallback (§4.5): a fallback exists for what the reader
270cannot know, and the reader knows exactly which icons it has. A chrome naming an icon
271this version does not draw is a fault in the chrome, and the validator is where it is
272caught. Growing the set is a versioned decision, as growing the kinds is.
273
274An `icon` carries **no geometry and no colour**, as a `surface` carries no geometry.
275It is a glyph: it takes its size from `size` and its colour from `fill`, through the
276universal `style` field, so an icon in a bar is sized and inked by the same two
277properties as the text beside it.
278
279`text` is the one exception to "payload is a map": its payload is a `Dat::Str`
280directly, because a text run wrapping a single string in a map would double the
281bytes of the commonest node in every document. `code` carries its source in a
282`text` field rather than as children, since a listing is one preserved string
283rather than a run of formatted spans.
284
285A schema fixes two vocabularies, not one: the node kinds above, and the style
286properties of §4.4. Both are closed, and a chrome tree is wider than a document in
287both — it may carry an `edit`, and it may name `grid`, `border` and `shadow` —
288because it is the same engine drawing a real interface. A document may do neither.
289
290Every node with a map payload may also carry an optional **`style`** field
291(§4.4), a string naming an entry in the document's style table. It is left out of
292the rows above because it is universal.
293
294A content hash reference (`image.hash`, and the `hash` form of an address) is a
295`b32`, a fixed 32-byte string, matching the width of the v0 hash scheme
296(SHA3-256). A variable-length byte string would let the same reference encode two
297ways.
298
299The root node of an `oxeweb/doc/0` payload is always `doc`.
300
301### 4.3 Link addresses
302
303A `link`'s `to` field is a typed address, not a string the renderer parses: a map
304with exactly one entry, whose key selects the address kind.
305
306```
307(link|{ "to": { "name": "news.cricket" }, "children": [...] })
308(link|{ "to": { "hash": (b32|9f86d081...) }, "children": [...] })
309```
310
311`name` carries a NAMES name (`str`); `hash` carries a content address (`b32`). An
312address with no entry, more than one entry, an unknown key, or a mistyped value
313is rejected. Making the address typed rather than a string means the decoder
314tells a name from a hash, and a malformed target is refused at the door rather
315than misread by the renderer.
316
317### 4.4 Styling
318
319The oxeweb replaces the web's cascade with locality. A node names a style; the
320style is defined once, in the document's style table; and a short inherited set
321flows down the tree. No rule reaches across the document, so a style error cannot
322escape the node that made it.
323
324The **style table** is the `doc` node's optional `styles` field: a `Dat::Map`
325from style name (`str`, the same lowercase-ASCII form as a map key) to a **style
326record**. A node's `style` field names an entry, which must exist.
327
328```
329(doc|{
330 "title": "Style without a cascade", "lang": "en",
331 "styles": {
332 "callout": { "bg": "muted", "pad": (u8|3), "fill": "ink" },
333 "lede": { "size": (i8|1) },
334 },
335 "children": [
336 (box|{ "style": "callout", "children": [ (para|{ "style": "lede",
337 "children": [(text|"...")] }) ] }),
338 ],
339})
340```
341
342A style record is a `Dat::Map` carrying any of these optional properties, and
343nothing else. **`oxeweb/doc/0` admits these eight and no others, and the eight are
344frozen.**
345
346| Property | Type | Inherited | Meaning |
347|---|---|---|---|
348| `fill` | `str` | yes | Text colour, a palette name |
349| `size` | `i8` | yes | Type scale step; 0 is the reader's base |
350| `lang` | `str` | yes | Language, BCP-47 |
351| `dir` | `str` | yes | `ltr` or `rtl` |
352| `bg` | `str` | no | Background colour, a palette name |
353| `pad` | `u8` | no | Spacing scale index |
354| `align` | `str` | no | `start`, `center`, `end`, or `justify` |
355| `radius` | `u8` | no | Corner radius, on the spacing scale `pad` uses; 0 is a square corner |
356
357`bg` and `pad` make a tinted, padded box a thing a document draws, and `radius`
358says what shape that box's corners are. It adds no element, no geometry and no
359authority, and a soft-cornered callout, pull-quote or listing is ordinary
360typography — which is what tells it from the properties below, whose subject is
361the layout of a bar and a shelf and which mean nothing in prose.
362
363The style vocabulary is the schema's, exactly as the node vocabulary is (§4.2).
364A chrome tree and an application tree admit the eight above and five more:
365
366| Property | Type | Inherited | Meaning |
367|---|---|---|---|
368| `grid` | `u8` | no | Lay the children out as a grid whose tiles are at least this many base sizes wide, wrapping to as many columns as the width allows, and sharing the width out among them |
369| `pack` | `u8` | no | Lay the children out in a row of tiles exactly this many base sizes wide, packed from the start edge and wrapping when they run out of room |
370| `grow` | `u8` | no | Take this share of the room a packed row has left over; 0 takes none of it |
371| `border` | `[str, u8]` | no | A line round the edge: a palette name, and a width in pixels |
372| `shadow` | `u8` | no | How far the node stands off the surface behind it, in whole steps; 0 is lying flat on it |
373
374```
375"shelf": { "grid": (u8|14) },
376"tile": { "bg": "muted", "pad": (u8|3), "radius": (u8|2),
377 "border": ["muted", (u8|1)], "shadow": (u8|1) },
378```
379
380The browser's chrome is an SBJ tree laid out by the same engine as a document, and
381it is a real interface: a library of tiles, a navigation bar, a control with an
382edge. These five are what it takes to draw one. They are the chrome's and not the
383document's because a document is not an interface and must not be able to dress as
384one — a published document cannot name them, whatever a later reader learns to
385draw, so what a document may look like is settled by the vocabulary it was signed
386under. A `shadow` is elevation, the language of a thing that lifts toward the hand,
387and a paragraph claiming to float above its page is precisely that dressing-up; a
388`border` is the line round the control with an edge. An application's tree admits
389them for the same reason it admits `edit` and `surface`: it is a declared program,
390drawn by the same engine.
391
392That rule is enforced by **addressing**, not by the vocabulary. The band and the
393bars are painted into pixmaps a document is never handed, so it cannot draw there
394whatever it names, and a document that drew a perfect likeness of a control could
395still make nothing happen by it — a document carries no program, and a link goes to
396an address or nowhere. The vocabulary is drawn where it is because there is nothing
397in prose for a shelf's layout to mean, not because a corner is dangerous. All are self-only but `grow`, which is the one property a node names for its
398PARENT to read. It is still locality and not cascade -- a parent looks at the
399children it is laying and at nothing else, so no node's width depends on anything
400outside the row it sits in -- but it is the one place the rule reads sideways
401rather than downwards. It is here because a bar of controls cannot be spelt
402without it: a bar is some controls and one thing that fills, and something has to
403take what the controls did not. A row whose leftover is narrower than one of its
404own tiles wraps instead, and the grower, alone on the next line, takes the whole
405width -- which is a bar folding on a narrow window, and falls out of the rule
406rather than being a case in it.
407
408`grid` and `pack` are two layouts, and a style naming both is **refused**. A grid
409shares its width out among its tiles, which is what a shelf of cards wants -- a
410row with a gap at its end reads as a row missing a card. A packed row leaves each
411tile at the width it names and ends where its tiles do, which is what a bar of
412controls wants -- a lone button stretched across half a window is not a button. A
413style asking for both would leave the reader to choose, and which it chose would
414be a fact about that reader rather than about the tree.
415
416A property's **type is the same in every schema; only its admission differs**.
417`grid` is a `u8` wherever it is legal, so the canonical encoding rules of §3 pin
418its width without consulting the schema, and a document naming `grid` is refused
419for naming it, not for how it wrote it.
420
421A `border` is one property and not two: a colour with no width draws nothing, and
422a width with no colour draws nothing, so the two are named together, as a
423two-element list, and a half-written border cannot be spelt. Its width is in
424pixels rather than on the spacing scale, because a border is a boundary and not a
425measure of room: a hairline is one pixel at any text size.
426
427A `shadow` names an **elevation** and not a shadow: how far the node stands off
428what is behind it, and nothing about what that costs in pixels or in ink. The
429offset, the softness and the colour are the reader's theme's, for the same reason
430the palette is — how a shadow must be drawn depends entirely on what it is drawn
431on, and only the theme knows that. A shadow is a dark stain, and a dark stain on a
432dark page is nearly nothing, so a theme with a near-black page must stain far
433harder than one with a white page to say the same thing. A style that named a
434colour would work in one theme and be invisible in the other; a style that names a
435height works in both. A shadow is ink and not room: it changes nothing about where
436anything sits, so a node that names one occupies exactly the box it would have
437occupied without it.
438
439The **palette** names are `ink`, `muted`, `accent`, and `bg`. They are semantic,
440not literal: the reader's theme resolves them to actual colours, so switching to a
441dark palette recolours every document without touching one. Sizes are scale
442steps, not pixels, so enlarging type reflows rather than clips. There are no raw
443pixels and no hex anywhere in v0; that exactness is fixed mode's business, which
444v0 defers.
445
446**Reader preferences are applied after author styles and always win.** An author
447declares intent — a scale step, a semantic colour — and the reader's base size,
448palette, and direction have the final say.
449
450A `styles` table with a non-string key, a style record with an unknown property
451or an out-of-enum value, or a `style` field naming an entry that does not exist,
452is rejected, naming the offending node or style. A record naming a property that
453exists but that its schema does not admit — `grid` in a document — is rejected
454saying so, and naming the schema that refused it and the trees it is legal in.
455"Unknown style property `grid`" would be a lie told to an author whose chrome
456draws one.
457
458### 4.5 Unknown kinds and forward compatibility
459
460A reader will meet node kinds it does not implement: a document written against a
461later vocabulary, seen by an older client. The web renders unknown tags as their
462raw children and never breaks, which is why it can never be versioned or made
463strict. Hard rejection is the opposite failure: one unknown node would make a
464whole document unreadable to every client that had not yet updated.
465
466The oxeweb takes neither. A node whose kind code is outside the vocabulary is
467permitted **only if** its payload is a map carrying a non-empty **`fallback`**: a
468list of nodes drawn from the kinds the reader *does* know. A reader that does not
469implement the kind renders and validates the fallback; a reader that does uses
470the kind's own fields. An unknown kind whose payload is not a map, or that lacks a
471non-empty fallback, is rejected.
472
473```
474(table 20|{
475 "fallback": [ (list|{ "ordered": (false), "children": [
476 (item|{ "children": [(text|"Q1 revenue: 1.2M")] }),
477 (item|{ "children": [(text|"Q2 revenue: 1.5M")] }),
478 ] }) ],
479 "rows": [ ... ] # a reader that knows kind 20 uses this
480})
481```
482
483This is the same discipline the `surface` node already carries, a mandatory
484semantic alternative, lifted to the vocabulary as a whole. It keeps "reject at the
485door" for genuinely malformed content while letting the vocabulary grow within a
486major version without stranding readers. The fallback is validated in full,
487against the known schema; the unknown kind's other fields are not interpreted, but
488are still held to the canonical encoding rules of §3, since they were decoded to
489get here.
490
491**A kind the reader knows, and the schema does not admit, is refused
492unconditionally.** A fallback does not admit it, and there is no other way in.
493
494The rule above is for a code this version has *never heard of*. A fallback earns
495such a code its place because the reader cannot know what it means and can still
496render something faithful: ignorance is the reason to be generous. The reserved
497codes of §4.2 are the opposite case. The reader knows exactly what code 15 is, and
498knows that `oxeweb/doc/0` admits it nowhere, so it refuses it on sight, fallback or
499no fallback, naming the kind and saying that a document may not carry it.
500
501The two paths must stay two paths. Collapsing them — treating a reserved code as
502merely unknown, and letting a fallback wave it through — would let an author put a
503`surface` in a document today, under a fallback that renders innocently, and have
504every reader that later learned what code 15 meant begin honouring it: a document
505that became a program by waiting. An unknown code is admitted by a fallback because
506ignorance is the reason to be generous. A known and inadmissible code is refused
507because knowledge is the reason not to be.
508
509### 4.6 Node ids
510
511Nodes are identified by their position in a depth-first, pre-order walk of the
512tree, counting from 0 at the root. Ids are not stored in the document. They are
513what the optional index (§1.4) maps to byte offsets, and what an error message
514names when it rejects a node.
515
516---
517
518## 5. Limits
519
520| Limit | Value | Enforced | What it stops |
521|---|---|---|---|
522| Tree region size | 4 MiB | Before decoding | Runaway documents. Media and fonts are references, not bytes; 100,000 words of prose is ~600 KB |
523| Node count | 100,000 | During validation | Layout cost tracks nodes, not bytes |
524| Nesting depth | 64 | **During decoding** | Stack exhaustion. A recursive decoder spends a stack frame per level, and a stack overflow aborts the process rather than returning an error, so a legal document must decode within a standard 2 MiB worker-thread stack. 64 levels dwarfs any real document, which nests perhaps 20 deep, and a tiny file describing a million nested boxes is the cheapest attack against a recursive decoder |
525| Envelope size | 4 KiB | Before decoding | A header claiming a 64 KiB envelope should not be believed for free |
526
527The depth limit is the decoder's, not the validator's, and applies before
528anything has been verified. The others may be raised on evidence, which is
529easier than imposing them later.
530
531---
532
533## 6. Rejection
534
535Every rejection names the failing thing: the byte offset, or the node id and its
536kind, and the rule broken. "Invalid document" is not an error message.
537
538A document that fails at any step renders as an error card and is never
539partially displayed. There is no repair, no quirks mode, and no best effort. The
540web gave that up in 1993 and spent thirty years paying for it.
541
542---
543
544## 7. Conformance fixtures
545
546`fixtures/` holds the format's teeth. Each fixture is a directory:
547
548```
549fixtures/<name>/
550 doc.jdat the document, in JDAT text form (the source of truth)
551 doc.sbj the canonical signed binary artefact
552 meta.jdat expected hash, expected node count, expected depth
553```
554
555Rejection fixtures carry `reject.jdat` instead, declaring the expected error
556(rule broken, offset or node id) so that "it was rejected" cannot pass for "it
557was rejected *for the right reason*".
558
559The v0 suite must include, at minimum: an empty document; one paragraph; every
560node kind once; a styled document exercising the style table, an inherited
561property, and a self-only property; a link by name and a link by hash; an unknown
562kind carrying a valid fallback (accepted); nesting at the depth limit and one past
563it; a tree at the size limit and one past it; each canonicalisation rule of §3
564violated exactly once; a truncated tree; a tree one byte longer than `tree_len`; a
565corrupted hash; a corrupted signature; a signature by the wrong key; a forbidden
566child (a `para` inside a `para`); an empty list; a heading with `level: 0` and one
567with `level: 7`; a `style` naming an entry absent from the table; a style record
568with an out-of-enum value; an unknown kind with no fallback (rejected); a document
569carrying an `edit` node, one carrying a `surface` node, one carrying an `icon` node,
570and one carrying a `surface` node that also carries a valid fallback, all four
571rejected (§4.2, §4.5); an `icon` naming each of the closed set (accepted in a chrome
572and an application tree) and one naming an icon outside it (rejected); a malformed
573link address (two entries); and a document whose bytes are valid BDAT but not valid
574SBJ.