mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Answer stability round 1: the docs name V8's array limit in one text node, a test pins Invalid array length, and item 106 takes the readers' RangeError and JavaScriptCore's Out of memory
This commit is contained in:
+6
-4
@@ -83,10 +83,12 @@
|
||||
- Fix a cyclic document, in which an object holds itself: `adfToLosslessMarkdown` and
|
||||
`adfToPortableMarkdown` refuse it as `not-an-adf-document` and `isAdfDocument` returns `false`,
|
||||
where all three hung.
|
||||
- Fix a document whose markdown runs past the longest string the JavaScript engine holds, about 537
|
||||
million characters on V8: `adfToLosslessMarkdown` and `adfToPortableMarkdown` refuse it as
|
||||
`unsupported-node-shape`, where they threw. `losslessMarkdownToAdf` refuses compact JSON in an
|
||||
`adf:` fence in time linear in it, where a megabyte of deeply nested JSON threw.
|
||||
- Fix a document whose markdown would be longer than the longest string or array the JavaScript
|
||||
engine builds, on V8 about 537 million characters, or about 134 million in one text node:
|
||||
`adfToLosslessMarkdown` and `adfToPortableMarkdown` refuse it as `unsupported-node-shape`, where
|
||||
they threw.
|
||||
- Fix compact JSON in an `adf:` fence: `losslessMarkdownToAdf` refuses it as
|
||||
`unsupported-node-shape`, where a megabyte of deeply nested JSON threw.
|
||||
- Add `adfToPortableMarkdown` and `portableMarkdownToAdf`, a lossy pair converting ADF to and from
|
||||
markdown GitHub, GitLab and Obsidian render: alerts, callouts, task lists, `==highlights==` and
|
||||
pipe tables. A task item keeps its paragraphs, then its nested task lists; the rest of the item,
|
||||
|
||||
@@ -442,7 +442,7 @@ The emitter raises these, and so does a parse, which asks the emitter how to spe
|
||||
| Code | Fires when | What you can do |
|
||||
| --- | --- | --- |
|
||||
| `unsupported-nesting-depth` | blocks, marks, an attribute's JSON or a carried node's JSON nest past 500 levels | keep the ADF and pass the document over, or show it read-only; flatten the input where you are the one who wrote it |
|
||||
| `unsupported-node-shape` | parsing, in `losslessMarkdownToAdf`: markdown spells a node with an attribute, value, argument or body its type does not take, or without one it needs, wraps a directive mark around nothing or only hard breaks, writes as a directive a node or mark the lossless flavour spells as CommonMark, or puts a reserved directive out of place: `!adf:textBreak{}` parting nothing, `!adf:doc` anywhere but as the whole document. Emitting, in `adfToLosslessMarkdown`: a node not riding the carry that carries one mark type twice, a block or a non-text inline node holding `text`, or a leaf block or a non-text inline node holding `content`. Emitting, in both writers: a document whose markdown runs past the longest string the JavaScript engine holds, about 537 million characters on V8 | fix what the message names; [`spec/flavour.md`](https://github.com/larvit/adf-codec/blob/main/spec/flavour.md) lists every type's attributes and body. Past the engine's longest string, keep the ADF and show the document read-only |
|
||||
| `unsupported-node-shape` | parsing, in `losslessMarkdownToAdf`: markdown spells a node with an attribute, value, argument or body its type does not take, or without one it needs, wraps a directive mark around nothing or only hard breaks, writes as a directive a node or mark the lossless flavour spells as CommonMark, or puts a reserved directive out of place: `!adf:textBreak{}` parting nothing, `!adf:doc` anywhere but as the whole document. Emitting, in `adfToLosslessMarkdown`: a node not riding the carry that carries one mark type twice, a block or a non-text inline node holding `text`, or a leaf block or a non-text inline node holding `content`. Emitting, in `adfToPortableMarkdown`: a line or task list the portable writer cannot spell, a bug in this library. Emitting, in both writers: a document whose markdown would be longer than the longest string or array the JavaScript engine builds — on V8, about 537 million characters of markdown, or about 134 million in one text node | fix what the message names; [`spec/flavour.md`](https://github.com/larvit/adf-codec/blob/main/spec/flavour.md) lists every type's attributes and body. For a portable line or task list, report it. Past the engine's longest string, keep the ADF and pass the document over, or show it read-only |
|
||||
|
||||
## The guarantees
|
||||
|
||||
|
||||
+3
-3
@@ -461,9 +461,9 @@ handles one cause alike whichever node, attribute or direction raised it.
|
||||
knows which direction it called and parting them after `0.1.0` is MAJOR (2026-09-23).
|
||||
- A non-finite number takes two codes: `unsupported-node-shape` parsing, `not-an-adf-document`
|
||||
emitting — no document holds one, so no round-trip crosses them (2026-09-23).
|
||||
- A document whose markdown runs past the longest string the JavaScript engine holds is
|
||||
`unsupported-node-shape` at the document's path, in either writer: the document does not convert,
|
||||
and the bound is the engine's own. A carried node reaches it on about 1 MB of deeply nested JSON,
|
||||
- A document whose markdown would be longer than the longest string or array the JavaScript engine
|
||||
builds is `unsupported-node-shape` at the document's path, in either writer: the document does not
|
||||
convert, and the bound is the engine's own. A carried node reaches it on about 1 MB of deeply nested JSON,
|
||||
since the block carry indents two spaces a level; `todo.md` item 107 spells it in a length linear
|
||||
in its JSON (2026-10-11).
|
||||
- The markdown readers and `htmlToAdf` refuse a value that is not a string as `not-a-string`, at
|
||||
|
||||
@@ -49,7 +49,7 @@ test('refuses a document whose markdown runs past the engine\'s longest string',
|
||||
let value: JsonValue = new Array<JsonValue>(2_200_000).fill(0)
|
||||
for (let level = 0; level < 495; level += 1) value = [value]
|
||||
const result = adfToLosslessMarkdown(document({ attrs: { value }, type: 'unknownBlock' }))
|
||||
assert.match(markdown(result), /^unsupported-node-shape: the output runs past what this JavaScript engine holds: /)
|
||||
assert.match(markdown(result), /^unsupported-node-shape: the output would be longer than the longest string or array this JavaScript engine builds \(/)
|
||||
assert.deepEqual(path(result), [])
|
||||
})
|
||||
|
||||
|
||||
+14
-2
@@ -4,7 +4,11 @@ import { fileURLToPath } from 'node:url'
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
import { success, withinEngineLimits } from './result.ts'
|
||||
import { success, withinEngineLimits, type Result } from './result.ts'
|
||||
|
||||
function code(result: Result<string>): string {
|
||||
return result.ok ? `converted ${result.value}` : result.error.code
|
||||
}
|
||||
|
||||
const sourceRoot = dirname(fileURLToPath(import.meta.url))
|
||||
const union = /export type ConvertErrorCode =\n((?:\s+\| '[a-z-]+'\n)+)/
|
||||
@@ -39,7 +43,15 @@ test('refuses where SpiderMonkey runs out of string, and lets a stack overflow o
|
||||
withinEngineLimits(() => {
|
||||
throw overflow
|
||||
}),
|
||||
{ error: { code: 'unsupported-node-shape', message: 'the output runs past what this JavaScript engine holds: allocation size overflow', path: [] }, ok: false },
|
||||
{ error: { code: 'unsupported-node-shape', message: 'the output would be longer than the longest string or array this JavaScript engine builds (allocation size overflow)', path: [] }, ok: false },
|
||||
)
|
||||
assert.equal(
|
||||
code(
|
||||
withinEngineLimits(() => {
|
||||
throw new RangeError('Invalid array length')
|
||||
}),
|
||||
),
|
||||
'unsupported-node-shape',
|
||||
)
|
||||
assert.throws(() =>
|
||||
withinEngineLimits(() => {
|
||||
|
||||
+1
-1
@@ -52,6 +52,6 @@ export function withinEngineLimits<T>(convert: () => Result<T>): Result<T> {
|
||||
} catch (error) {
|
||||
// A stack overflow is a bug (docs/decisions.md §Nothing recurses unbounded), so it stays a throw.
|
||||
if (!(error instanceof Error) || !engineLimitThrows.includes(`${error.name}: ${error.message}`)) throw error
|
||||
return failure('unsupported-node-shape', `the output runs past what this JavaScript engine holds: ${error.message}`, [])
|
||||
return failure('unsupported-node-shape', `the output would be longer than the longest string or array this JavaScript engine builds (${error.message})`, [])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,9 @@ of markdown, a `!adf:blockquote` holding a list nested 490 levels whose paragrap
|
||||
lazy lines `b\`, crashed Node 24 out of heap: `directiveNode` re-spells the subtree, indenting each
|
||||
line to its depth. `adfToLosslessMarkdown` on nine text nodes sharing one 2^28-character string
|
||||
got Bun OOM-killed under 8 GB: `escape` in `line-escaping.ts` pushes one array element per
|
||||
character. The length to refuse past is a number no entry states yet (AGENTS.md §Stated numbers):
|
||||
character. Where the heap holds out, the readers' re-spelling can still throw a `RangeError`, and
|
||||
JavaScriptCore's `Out of memory`, which the writers refuse, may also mean a failed allocation a
|
||||
retry survives. The length to refuse past is a number no entry states yet (AGENTS.md §Stated numbers):
|
||||
V8's 2^29 − 24 on every engine, so the same document converts alike everywhere, is the candidate.
|
||||
Found by the principles audit of item 83, 2026-10-11.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user