Compare commits

...
1 Commits
Author SHA1 Message Date
Kit Langton 126eadd331 fix(core): terminate exhausted Markdown code budgets 2026-09-04 20:20:10 -04:00
3 changed files with 78 additions and 0 deletions
+2
View File
@@ -246,6 +246,8 @@ export function convertHTMLToMarkdown(html: string) {
block()
return
}
// No amount of trimming can make the empty fenced block fit.
if (!payload) return
const excess = valueBytes - Math.max(0, CONTENT_BYTES - outputBytes)
payload = sliceBytes(payload, Math.max(0, encoder.encode(payload).byteLength - Math.ceil(excess)))
}
@@ -0,0 +1,7 @@
import { writeSync } from "node:fs"
import { convertHTMLToMarkdown } from "../../src/tool/html-markdown"
const html = await Bun.stdin.text()
// Flush readiness before entering a conversion that may block the child event loop.
writeSync(1, "ready\n")
await Bun.write(Bun.stdout, convertHTMLToMarkdown(html))
@@ -0,0 +1,69 @@
import { expect, test } from "bun:test"
import { fileURLToPath } from "node:url"
import { MAX_MARKDOWN_BYTES } from "../src/tool/html-markdown"
const budget = MAX_MARKDOWN_BYTES - 64 * 1024
test.each([
["exhausted budget", budget, "x", "", false],
["one byte short of an empty fence", budget - 13, "x", "", false],
["small fitting block", 64, "x", "\n\n```\nx\n```", false],
["last payload byte fits", budget - 15, "x", "\n\n```\nx\n```", false],
["only an empty fence fits", budget - 14, "x", "\n\n```\n\n```", false],
["Unicode payload truncates at a code point", budget - 18, "😀é", "\n\n```\n😀\n```", false],
["quoted payload fits", budget - 23, "x", "\n\n> ```\n> x\n> ```", true],
["only an empty quoted fence fits", budget - 22, "x", "\n\n> ```\n> \n> ```", true],
["one byte short of an empty quoted fence", budget - 21, "x", "", true],
] as const)(
"finishes bounded code conversion: %s",
async (_name, count, payload, suffix, quoted) => {
const code = `<pre>${payload}</pre>`
const html = `<p>${"x".repeat(count)}</p>${quoted ? `<blockquote>${code}</blockquote>` : code}`
expect(Buffer.byteLength(html)).toBeLessThanOrEqual(MAX_MARKDOWN_BYTES)
const child = Bun.spawn({
cmd: [process.execPath, fileURLToPath(new URL("./fixture/html-markdown.ts", import.meta.url))],
stdin: new Blob([html]),
stdout: "pipe",
stderr: "pipe",
})
let ready = false
let timeout: "startup" | "conversion" | undefined
let timer = setTimeout(() => {
timeout = "startup"
child.kill("SIGKILL")
}, 10_000)
const stdout = (async () => {
let output = ""
for await (const chunk of child.stdout.pipeThrough(new TextDecoderStream())) {
output += chunk
if (ready || !output.startsWith("ready\n")) continue
ready = true
clearTimeout(timer)
// This watchdog runs outside the possibly stuck synchronous converter.
timer = setTimeout(() => {
timeout = "conversion"
child.kill("SIGKILL")
}, 3_000)
}
return output.slice("ready\n".length)
})()
const stderr = new Response(child.stderr).text()
try {
const exitCode = await child.exited
const output = await stdout
expect({ ready, timeout, exitCode, stderr: await stderr }).toEqual({
ready: true,
timeout: undefined,
exitCode: 0,
stderr: "",
})
expect(Buffer.byteLength(output)).toBeLessThanOrEqual(MAX_MARKDOWN_BYTES)
expect(output).toBe("x".repeat(Math.min(count, budget - 2)) + suffix)
} finally {
clearTimeout(timer)
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL")
await child.exited
}
},
15_000,
)