single-file-core 1.5.115 → 1.5.117

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,85 @@
1
+ // The CSS-removal option runs every declaration through css-tree's lexer and drops the ones it
2
+ // judges invalid. css-tree reports two different failures the same way — a real value mismatch
3
+ // (SyntaxMatchError) and a property it simply has no entry for (SyntaxReferenceError) — so the
4
+ // original `!matchProperty(...).matched` deleted valid declarations whose property is missing from
5
+ // the vendored dictionary. That silently flattened every CSS-defined SVG gradient: stop-opacity was
6
+ // dropped, both stops became opaque, and the gradient rendered as a solid block. It hid for a long
7
+ // time because stop-color survived alongside it — a var() value takes a different branch and is
8
+ // never handed to the lexer at all, so the colour was right and only the fade was gone.
9
+ //
10
+ // The rule this pins: unknown must fail open. A dropped valid declaration breaks rendering; a kept
11
+ // invalid one is ignored by the browser.
12
+ import * as cssTree from "../../vendor/css-tree.js";
13
+ import { isUnsupportedPropertyValue } from "../../modules/css-rules-minifier.js";
14
+
15
+ // valid declarations whose property the vendored css-tree does not know. Every one of these was
16
+ // deleted before the fix. The SVG paint-server and filter properties are the ones that matter in
17
+ // practice; the last two are here as the moving target — any property newer than the vendored
18
+ // dictionary lands in this bucket, and the list will keep growing
19
+ const MUST_KEEP = [
20
+ ["stop-color", "#2a78d6"],
21
+ ["stop-opacity", ".20"],
22
+ ["stop-opacity", "0"],
23
+ ["flood-color", "red"],
24
+ ["flood-opacity", "0.5"],
25
+ ["lighting-color", "white"],
26
+ ["text-box-trim", "trim-both"],
27
+ ["corner-shape", "squircle"]
28
+ ];
29
+
30
+ // declarations css-tree knows and correctly rejects. These keep the filter honest: the fix must not
31
+ // turn the check off, only stop it firing on properties css-tree has never heard of
32
+ const MUST_DROP = [
33
+ ["margin-trim", "block"],
34
+ ["opacity", "not-a-number"],
35
+ ["color", "12px"]
36
+ ];
37
+
38
+ // properties close enough to the broken ones to be worth stating outright, because they are the
39
+ // reason the bug looked like an SVG quirk rather than a dictionary gap: css-tree does know these,
40
+ // so gradients that used them were unaffected and the failure looked arbitrary
41
+ const KNOWN_GOOD = [
42
+ ["fill-opacity", "0.5"],
43
+ ["stroke-opacity", "0.5"],
44
+ ["fill", "red"],
45
+ ["vector-effect", "non-scaling-stroke"]
46
+ ];
47
+
48
+ let failed = false;
49
+
50
+ function check(label, actual, expected) {
51
+ const ok = actual === expected;
52
+ console.log(`${ok ? "PASS" : "FAIL"} ${label}: ${actual}${ok ? "" : " (expected " + expected + ")"}`);
53
+ failed ||= !ok;
54
+ }
55
+
56
+ function isDropped(property, declaration) {
57
+ return isUnsupportedPropertyValue(property, cssTree.parse(declaration, { context: "value" }));
58
+ }
59
+
60
+ for (const [property, declaration] of MUST_KEEP) {
61
+ check(`kept: ${property}: ${declaration}`, isDropped(property, declaration), false);
62
+ }
63
+
64
+ for (const [property, declaration] of MUST_DROP) {
65
+ check(`dropped: ${property}: ${declaration}`, isDropped(property, declaration), true);
66
+ }
67
+
68
+ for (const [property, declaration] of KNOWN_GOOD) {
69
+ check(`kept: ${property}: ${declaration}`, isDropped(property, declaration), false);
70
+ }
71
+
72
+ // the distinction the fix rests on. If a css-tree upgrade ever collapses these two error types into
73
+ // one, the predicate above cannot tell "unknown" from "wrong" any more and needs rewriting rather
74
+ // than adjusting
75
+ const unknownProperty = cssTree.lexer.matchProperty("stop-opacity", cssTree.parse("0", { context: "value" }));
76
+ const wrongValue = cssTree.lexer.matchProperty("margin-trim", cssTree.parse("block", { context: "value" }));
77
+ check("unknown property reports SyntaxReferenceError", unknownProperty.error && unknownProperty.error.name, "SyntaxReferenceError");
78
+ check("wrong value reports SyntaxMatchError", wrongValue.error && wrongValue.error.name, "SyntaxMatchError");
79
+
80
+ if (failed) {
81
+ console.log("FAILED");
82
+ Deno.exit(1);
83
+ } else {
84
+ console.log("PASSED");
85
+ }
@@ -2,7 +2,9 @@ import "./dom-stub.js";
2
2
  import { makePageData, makeOptions, runProcess, mulberry32 } from "./common.js";
3
3
  import { ZipReader, BlobReader } from "../../vendor/zip/zip.js";
4
4
 
5
- const TITLE = "日本語 — café & <b>";
5
+ // the quote is there because the escaper the title shares with the table of contents encodes
6
+ // it for an attribute value, where it matters, and a title has to round-trip through that too
7
+ const TITLE = "日本語 — café & <b> \"quoted\"";
6
8
  const PDF = new TextEncoder().encode("%PDF-1.4\n1 0 obj\n<<>>\nendobj\ntrailer\n<<>>\n%%EOF\n");
7
9
 
8
10
  let failed = false;
@@ -207,6 +209,34 @@ function countIdentifiers(text) {
207
209
  check("the cdata rung leaves the appended data placement alone", options.preventAppendedData !== true, true);
208
210
  }
209
211
 
212
+ // the rung a caller names is resolved by its start tag, not by the identity of the array holding
213
+ // it. The option is internal and is normally set from the module's own EXTRA_DATA_TAGS, so an
214
+ // identity match worked by accident; a caller passing an equal pair of its own indexed the regexp
215
+ // table with -1 and threw a bare TypeError out of library internals
216
+ {
217
+ const options = makeOptions({ extractDataFromPageTags: ["<noframes>", "</noframes>"] });
218
+ const pageData = makePageData(31, 4 * 1024);
219
+ let error;
220
+ try {
221
+ await runProcess(pageData, options);
222
+ } catch (caught) {
223
+ error = caught;
224
+ }
225
+ check("a rung named by an equal pair is resolved", error, undefined);
226
+ }
227
+
228
+ {
229
+ const options = makeOptions({ extractDataFromPageTags: ["<marquee>", "</marquee>"] });
230
+ const pageData = makePageData(32, 4 * 1024);
231
+ let message;
232
+ try {
233
+ await runProcess(pageData, options);
234
+ } catch (error) {
235
+ message = error.message;
236
+ }
237
+ check("a rung that is not in the ladder names itself", message, "Unknown data tags: <marquee>");
238
+ }
239
+
210
240
  // the terminator is the whole test for this rung, so a payload holding it must step past
211
241
  {
212
242
  const content = new Uint8Array(4096).fill(0x21);
@@ -0,0 +1,82 @@
1
+ /*
2
+ * Copyright 2010-2026 Gildas Lormeau
3
+ * contact : gildas.lormeau <at> gmail.com
4
+ *
5
+ * This file is part of SingleFile.
6
+ *
7
+ * The code in this file is free software: you can redistribute it and/or
8
+ * modify it under the terms of the GNU Affero General Public License
9
+ * (GNU AGPL) as published by the Free Software Foundation, either version 3
10
+ * of the License, or (at your option) any later version.
11
+ *
12
+ * The code in this file is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
15
+ * General Public License for more details.
16
+ *
17
+ * As additional permission under GNU AGPL version 3 section 7, you may
18
+ * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
19
+ * AGPL normally required by section 4, provided you include this license
20
+ * notice and a URL through which recipients can access the Corresponding
21
+ * Source.
22
+ */
23
+
24
+ // `createArchive` builds the script of a self-extracting archive by serializing functions with
25
+ // toString() and pasting the text into the generated page. That page has no module scope, so
26
+ // anything such a function names has to be declared inside it. An import is the trap: it survives
27
+ // bundling, the call still reads correctly in the source, and the archive then fails at runtime
28
+ // with a bare ReferenceError while the page stays blank.
29
+ //
30
+ // It is worse than it looks, because a minifier can hide it. `display` called an imported
31
+ // `getDoctypeString`, and terser inlined that helper into the release bundle, so released
32
+ // archives worked while every unminified build produced archives that threw
33
+ // `ReferenceError: getDoctypeString is not defined` and rendered nothing.
34
+ //
35
+ // The invariant below is what makes that impossible to reintroduce: a module holding an inlined
36
+ // function declares that function and nothing else, so there is no module scope to close over.
37
+
38
+ const MODULES = [
39
+ { path: "../../processors/compression/compression-display.js", functions: ["display"] },
40
+ { path: "../../processors/compression/compression-extract.js", functions: ["extract"] },
41
+ { path: "../../processors/compression/compression-router.js", functions: ["router"] }
42
+ ];
43
+ const TOP_LEVEL_DECLARATION = /^(?:export\s+)?(?:async\s+)?(?:function|const|let|var|class)\s+([A-Za-z_$][\w$]*)/;
44
+
45
+ let failures = 0;
46
+
47
+ for (const { path, functions } of MODULES) {
48
+ const source = await Deno.readTextFile(new URL(path, import.meta.url));
49
+ const name = path.split("/").pop();
50
+ const lines = source.split("\n");
51
+ const imports = lines.filter(line => /^import[\s{]/.test(line) || /^\s*import\s*\(/.test(line));
52
+ check(name + " imports nothing", imports.length === 0, imports.join(" | "));
53
+ const declared = [];
54
+ lines.forEach(line => {
55
+ if (line === line.trimStart()) {
56
+ const match = line.match(TOP_LEVEL_DECLARATION);
57
+ if (match) {
58
+ declared.push(match[1]);
59
+ }
60
+ }
61
+ });
62
+ const unexpected = declared.filter(identifier => !functions.includes(identifier));
63
+ check(name + " declares nothing at module scope but " + functions.join(", "),
64
+ unexpected.length === 0, unexpected.join(", "));
65
+ }
66
+
67
+ // the one inlined function that does live beside module scope, in compression.js, states the rule
68
+ // in a comment and binds every global it uses itself; assert that binding is still there
69
+ const compression = await Deno.readTextFile(new URL("../../processors/compression/compression.js", import.meta.url));
70
+ const getContentBody = compression.slice(compression.indexOf("async function getContent()"));
71
+ check("getContent binds its globals locally",
72
+ /const \{[^}]*\} = globalThis;/.test(getContentBody.slice(0, 2000)));
73
+
74
+ console.log(failures ? `\n${failures} check(s) FAILED` : "\nall checks passed");
75
+ Deno.exit(failures ? 1 : 0);
76
+
77
+ function check(label, condition, detail) {
78
+ if (!condition) {
79
+ failures++;
80
+ }
81
+ console.log((condition ? "PASS" : "FAIL") + " " + label + (condition || !detail ? "" : ": " + detail));
82
+ }