Skip to content

Commit efe17a5

Browse files
committed
fix(core): bound ASCII folding allocations for large HTML
1 parent 568dc2a commit efe17a5

2 files changed

Lines changed: 46 additions & 1 deletion

File tree

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

Lines changed: 37 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,31 @@ 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+
6391
it("keeps a document's <html> attributes when a comment comes before the doctype", () => {
6492
const doc = parseHTMLContent(
6593
'<!-- hyperframes-registry-item: blk -->\n<!doctype html>\n<html lang="en" data-composition-variables="[]"><body></body></html>',
@@ -301,6 +329,15 @@ describe("htmlDocument helpers", () => {
301329
});
302330

303331
describe("findStartTags", () => {
332+
it("preserves Unicode offsets and mixed-case tags across chunk boundaries", () => {
333+
const prefix = "a".repeat(65_535) + "😀İ";
334+
const html = prefix + "<ImG src=x>" + "A".repeat(65_521) + "<IMG src=y>";
335+
expect(findStartTags(html, "iMg")).toEqual([65_538, 131_070]);
336+
expect(
337+
stripEmbeddedRuntimeScripts(prefix + '<SCRIPT src="HYPERFRAME.RUNTIME.IIFE.JS"></SCRIPT>'),
338+
).toBe(prefix);
339+
});
340+
304341
const at = (html: string, name: string) =>
305342
findStartTags(html, name).map((i) => html.slice(i, html.indexOf(">", i) + 1));
306343

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,15 @@ export function parseHTMLContent(html: string): Document {
3232

3333
/** Lowercases A-Z only, so indexes found in the result are valid in the input ("İ" lowercases to two chars). */
3434
function lowerAscii(text: string): string {
35-
return text.replace(/[A-Z]+/g, (letters) => letters.toLowerCase());
35+
// V8 collects every regex match before invoking callbacks; bound that temporary array.
36+
const chunkSize = 64 * 1024;
37+
let lowered = "";
38+
for (let offset = 0; offset < text.length; offset += chunkSize) {
39+
lowered += text
40+
.slice(offset, offset + chunkSize)
41+
.replace(/[A-Z]+/g, (letters) => letters.toLowerCase());
42+
}
43+
return lowered;
3644
}
3745

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

0 commit comments

Comments
 (0)