diff --git a/packages/markup/src/syntax.ts b/packages/markup/src/syntax.ts index 75f5f74ee..0a7dfb4b5 100644 --- a/packages/markup/src/syntax.ts +++ b/packages/markup/src/syntax.ts @@ -11,7 +11,10 @@ import type { const NAME = /^[A-Za-z_][A-Za-z0-9_.:-]*/u; const ALIAS = /^[a-z][a-z0-9_-]{0,63}$/u; -const REFERENCE = /^[A-Za-z_][A-Za-z0-9_.:-]*(?:\.[A-Za-z_][A-Za-z0-9_.:-]*)*$/u; +// Strictly segmented: `.` separates segments and can never appear inside one. An inner +// character class that also accepts `.` lets a rejecting input partition its dots between +// two quantified parts, which is exponential backtracking on a synchronous parse thread. +const REFERENCE = /^[A-Za-z_][A-Za-z0-9_:-]*(?:\.[A-Za-z_][A-Za-z0-9_:-]*)*$/u; type OpeningTag = { readonly name: string; diff --git a/packages/markup/test/syntax.test.ts b/packages/markup/test/syntax.test.ts new file mode 100644 index 000000000..6ae06bad4 --- /dev/null +++ b/packages/markup/test/syntax.test.ts @@ -0,0 +1,32 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { MarkupFrontendError } from "../src/error.js"; +import { parseOpeningTag } from "../src/syntax.js"; + +const source = (text: string) => ({ name: "test.markup", text }); + +test("reference attributes accept dotted segment paths", () => { + const parsed = parseOpeningTag(source(``), 0); + assert.deepEqual(parsed.attributes["value"], { kind: "reference", path: "timeline.start" }); + assert.deepEqual(parsed.attributes["other"], { kind: "reference", path: "ns:record-1.x" }); +}); + +test("reference attributes reject empty or trailing segments", () => { + for (const path of ["a.", "a..b", ".a", "a.b."]) { + assert.throws( + () => parseOpeningTag(source(``), 0), + (error: unknown) => error instanceof MarkupFrontendError && error.code === "MARKUP_REFERENCE", + ); + } +}); + +test("a long malformed reference rejects without exponential backtracking", () => { + const path = `a${".b".repeat(40)}!`; + const startedAt = Date.now(); + assert.throws( + () => parseOpeningTag(source(``), 0), + (error: unknown) => error instanceof MarkupFrontendError && error.code === "MARKUP_REFERENCE", + ); + assert(Date.now() - startedAt < 1000, "malformed reference validation took too long"); +});