Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions packages/core/src/compiler/htmlDocument.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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>',
Expand Down Expand Up @@ -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));

Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/compiler/htmlDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading