Skip to content

Commit 00342fc

Browse files
fix(core): bound ASCII folding allocations for large HTML (#5098)
* fix(core): bound ASCII folding allocations for large HTML * chore(core): consolidate ASCII folding rationale * fix(core): preserve unchanged spans during ASCII folding
1 parent 1db44c6 commit 00342fc

2 files changed

Lines changed: 75 additions & 2 deletions

File tree

‎packages/core/src/compiler/htmlDocument.test.ts‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { spawnSync } from "node:child_process";
2+
import { resolve } from "node:path";
3+
import { pathToFileURL } from "node:url";
14
import { describe, expect, it } from "vitest";
25
import {
36
findStartTags,
@@ -60,6 +63,65 @@ const workGrowth = (scan: (n: number) => void) =>
6063
scannedChars(() => scan(20_000)) / scannedChars(() => scan(10_000));
6164

6265
describe("htmlDocument helpers", () => {
66+
it("strips runtimes from large mixed-case base64 HTML within a bounded heap", () => {
67+
const moduleUrl = pathToFileURL(resolve(__dirname, "htmlDocument.ts")).href;
68+
const result = spawnSync(
69+
process.execPath,
70+
[
71+
"--max-old-space-size=256",
72+
"--import=tsx",
73+
"--input-type=module",
74+
"--eval",
75+
`
76+
import { strict as assert } from "node:assert";
77+
import { stripEmbeddedRuntimeScripts } from ${JSON.stringify(moduleUrl)};
78+
const media = '<img src="data:image/png;base64,' + 'Aa0/'.repeat(8 * 1024 * 1024) + '">';
79+
const runtime = '<SCRIPT src="HYPERFRAME.RUNTIME.IIFE.JS"></SCRIPT>';
80+
assert.equal(stripEmbeddedRuntimeScripts(media + runtime), media);
81+
console.log("large HTML preserved; runtime removed");
82+
`,
83+
],
84+
{ encoding: "utf8", timeout: 60_000 },
85+
);
86+
expect(result.error).toBeUndefined();
87+
expect(result.status, result.stderr).toBe(0);
88+
expect(result.stdout).toContain("large HTML preserved; runtime removed");
89+
}, 65_000);
90+
91+
it.each([
92+
{ first: "a", oldSpace: 96 },
93+
{ first: "A", oldSpace: 128 },
94+
])(
95+
"preserves a large authored script starting with $first within $oldSpace MiB old-space",
96+
({ first, oldSpace }) => {
97+
const moduleUrl = pathToFileURL(resolve(__dirname, "htmlDocument.ts")).href;
98+
const result = spawnSync(
99+
process.execPath,
100+
[
101+
`--max-old-space-size=${oldSpace}`,
102+
"--max-semi-space-size=4",
103+
"--import=tsx",
104+
"--input-type=module",
105+
"--eval",
106+
`
107+
import { strict as assert } from "node:assert";
108+
import { stripEmbeddedRuntimeScripts } from ${JSON.stringify(moduleUrl)};
109+
const data = ${JSON.stringify(first)} + "a".repeat(48 * 1024 * 1024 - 1);
110+
const html = '<!doctype html><html><head></head><body><script>const embeddeddata="' + data + '";</script></body></html>';
111+
assert.ok(stripEmbeddedRuntimeScripts(html) === html);
112+
assert.equal(data.length, 48 * 1024 * 1024);
113+
console.log("authored script preserved");
114+
`,
115+
],
116+
{ encoding: "utf8", timeout: 60_000 },
117+
);
118+
expect(result.error).toBeUndefined();
119+
expect(result.status, result.stderr).toBe(0);
120+
expect(result.stdout).toContain("authored script preserved");
121+
},
122+
65_000,
123+
);
124+
63125
it("keeps a document's <html> attributes when a comment comes before the doctype", () => {
64126
const doc = parseHTMLContent(
65127
'<!-- hyperframes-registry-item: blk -->\n<!doctype html>\n<html lang="en" data-composition-variables="[]"><body></body></html>',
@@ -301,6 +363,15 @@ describe("htmlDocument helpers", () => {
301363
});
302364

303365
describe("findStartTags", () => {
366+
it("preserves Unicode offsets and mixed-case tags across chunk boundaries", () => {
367+
const prefix = "Aİ" + "a".repeat(65_533) + "😀İ";
368+
const html = prefix + "<ImG src=x>" + "A".repeat(65_524) + "<IMG src=y>";
369+
expect(findStartTags(html, "iMg")).toEqual([65_538, 131_073]);
370+
expect(
371+
stripEmbeddedRuntimeScripts(prefix + '<SCRIPT src="HYPERFRAME.RUNTIME.IIFE.JS"></SCRIPT>'),
372+
).toBe(prefix);
373+
});
374+
304375
const at = (html: string, name: string) =>
305376
findStartTags(html, name).map((i) => html.slice(i, html.indexOf(">", i) + 1));
306377

‎packages/core/src/compiler/htmlDocument.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ export function parseHTMLContent(html: string): Document {
3030
return parseHTML(`<!DOCTYPE html><html><head></head><body>${html}</body></html>`).document;
3131
}
3232

33-
/** Lowercases A-Z only, so indexes found in the result are valid in the input ("İ" lowercases to two chars). */
33+
/** ASCII-only chunks bound match arrays; starting at uppercase preserves unchanged spans. */
3434
function lowerAscii(text: string): string {
35-
return text.replace(/[A-Z]+/g, (letters) => letters.toLowerCase());
35+
return text.replace(/[A-Z][\s\S]{0,65535}/g, (chunk) =>
36+
chunk.replace(/[A-Z]+/g, (letters) => letters.toLowerCase()),
37+
);
3638
}
3739

3840
export function stripEmbeddedRuntimeScripts(html: string): string {

0 commit comments

Comments
 (0)