mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
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
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
findStartTags,
|
||||
@@ -60,6 +63,65 @@ const workGrowth = (scan: (n: number) => void) =>
|
||||
scannedChars(() => scan(20_000)) / scannedChars(() => scan(10_000));
|
||||
|
||||
describe("htmlDocument helpers", () => {
|
||||
it("strips runtimes from large mixed-case base64 HTML within a bounded heap", () => {
|
||||
const moduleUrl = pathToFileURL(resolve(__dirname, "htmlDocument.ts")).href;
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--max-old-space-size=256",
|
||||
"--import=tsx",
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
`
|
||||
import { strict as assert } from "node:assert";
|
||||
import { stripEmbeddedRuntimeScripts } from ${JSON.stringify(moduleUrl)};
|
||||
const media = '<img src="data:image/png;base64,' + 'Aa0/'.repeat(8 * 1024 * 1024) + '">';
|
||||
const runtime = '<SCRIPT src="HYPERFRAME.RUNTIME.IIFE.JS"></SCRIPT>';
|
||||
assert.equal(stripEmbeddedRuntimeScripts(media + runtime), media);
|
||||
console.log("large HTML preserved; runtime removed");
|
||||
`,
|
||||
],
|
||||
{ encoding: "utf8", timeout: 60_000 },
|
||||
);
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.stdout).toContain("large HTML preserved; runtime removed");
|
||||
}, 65_000);
|
||||
|
||||
it.each([
|
||||
{ first: "a", oldSpace: 96 },
|
||||
{ first: "A", oldSpace: 128 },
|
||||
])(
|
||||
"preserves a large authored script starting with $first within $oldSpace MiB old-space",
|
||||
({ first, oldSpace }) => {
|
||||
const moduleUrl = pathToFileURL(resolve(__dirname, "htmlDocument.ts")).href;
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
`--max-old-space-size=${oldSpace}`,
|
||||
"--max-semi-space-size=4",
|
||||
"--import=tsx",
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
`
|
||||
import { strict as assert } from "node:assert";
|
||||
import { stripEmbeddedRuntimeScripts } from ${JSON.stringify(moduleUrl)};
|
||||
const data = ${JSON.stringify(first)} + "a".repeat(48 * 1024 * 1024 - 1);
|
||||
const html = '<!doctype html><html><head></head><body><script>const embeddeddata="' + data + '";</script></body></html>';
|
||||
assert.ok(stripEmbeddedRuntimeScripts(html) === html);
|
||||
assert.equal(data.length, 48 * 1024 * 1024);
|
||||
console.log("authored script preserved");
|
||||
`,
|
||||
],
|
||||
{ encoding: "utf8", timeout: 60_000 },
|
||||
);
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.stdout).toContain("authored script preserved");
|
||||
},
|
||||
65_000,
|
||||
);
|
||||
|
||||
it("keeps a document's <html> attributes when a comment comes before the doctype", () => {
|
||||
const doc = parseHTMLContent(
|
||||
'<!-- hyperframes-registry-item: blk -->\n<!doctype html>\n<html lang="en" data-composition-variables="[]"><body></body></html>',
|
||||
@@ -301,6 +363,15 @@ describe("htmlDocument helpers", () => {
|
||||
});
|
||||
|
||||
describe("findStartTags", () => {
|
||||
it("preserves Unicode offsets and mixed-case tags across chunk boundaries", () => {
|
||||
const prefix = "Aİ" + "a".repeat(65_533) + "😀İ";
|
||||
const html = prefix + "<ImG src=x>" + "A".repeat(65_524) + "<IMG src=y>";
|
||||
expect(findStartTags(html, "iMg")).toEqual([65_538, 131_073]);
|
||||
expect(
|
||||
stripEmbeddedRuntimeScripts(prefix + '<SCRIPT src="HYPERFRAME.RUNTIME.IIFE.JS"></SCRIPT>'),
|
||||
).toBe(prefix);
|
||||
});
|
||||
|
||||
const at = (html: string, name: string) =>
|
||||
findStartTags(html, name).map((i) => html.slice(i, html.indexOf(">", i) + 1));
|
||||
|
||||
|
||||
@@ -30,9 +30,11 @@ export function parseHTMLContent(html: string): Document {
|
||||
return parseHTML(`<!DOCTYPE html><html><head></head><body>${html}</body></html>`).document;
|
||||
}
|
||||
|
||||
/** Lowercases A-Z only, so indexes found in the result are valid in the input ("İ" lowercases to two chars). */
|
||||
/** ASCII-only chunks bound match arrays; starting at uppercase preserves unchanged spans. */
|
||||
function lowerAscii(text: string): string {
|
||||
return text.replace(/[A-Z]+/g, (letters) => letters.toLowerCase());
|
||||
return text.replace(/[A-Z][\s\S]{0,65535}/g, (chunk) =>
|
||||
chunk.replace(/[A-Z]+/g, (letters) => letters.toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
export function stripEmbeddedRuntimeScripts(html: string): string {
|
||||
|
||||
Reference in New Issue
Block a user