single-file-core 1.6.0 → 1.6.2

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.
Files changed (77) hide show
  1. package/core/helper.js +5 -2
  2. package/modules/css-fonts-minifier.js +34 -13
  3. package/modules/template-formatter.js +8 -2
  4. package/package.json +10 -2
  5. package/vendor/zip/zip.js +4 -0
  6. package/.github/workflows/publish.yml +0 -88
  7. package/.github/workflows/test.yml +0 -31
  8. package/.github/workflows/vendor.yml +0 -39
  9. package/css-tree-build/README.md +0 -39
  10. package/css-tree-build/build.js +0 -66
  11. package/css-tree-build/package-lock.json +0 -511
  12. package/css-tree-build/package.json +0 -13
  13. package/deno.lock +0 -19
  14. package/doc/assets/singlefile-archive-byte-map.svg +0 -276
  15. package/doc/assets/singlefile-archive-lenses.svg +0 -245
  16. package/doc/singlefile-archive.md +0 -1968
  17. package/eslint.config.mjs +0 -74
  18. package/test/capture/README.md +0 -74
  19. package/test/capture/canonical-link.js +0 -50
  20. package/test/capture/common.js +0 -73
  21. package/test/capture/deferred-content-options.js +0 -71
  22. package/test/capture/dom.js +0 -26
  23. package/test/capture/maff-metadata.js +0 -205
  24. package/test/capture/resource-cap.js +0 -79
  25. package/test/capture/script-uri-sanitization.js +0 -82
  26. package/test/fixtures/README.md +0 -18
  27. package/test/fixtures/classic-digest.html +0 -5
  28. package/test/fixtures/generate-fixtures.sh +0 -31
  29. package/test/fixtures/multi-page-dedup.zip.html +0 -70
  30. package/test/fixtures/multi-page.zip.html +0 -26
  31. package/test/fixtures/single-page.zip.html +0 -11
  32. package/test/fixtures/site/multi-page/alpha.html +0 -2
  33. package/test/fixtures/site/multi-page/beta.html +0 -2
  34. package/test/fixtures/site/multi-page/index.html +0 -2
  35. package/test/fixtures/site/multi-page/sub/gamma.html +0 -2
  36. package/test/fixtures/site/multi-page-dedup/index.html +0 -2
  37. package/test/fixtures/site/multi-page-dedup/logo.png +0 -0
  38. package/test/fixtures/site/multi-page-dedup/one.html +0 -2
  39. package/test/fixtures/site/multi-page-dedup/shared.css +0 -3
  40. package/test/fixtures/site/multi-page-dedup/two.html +0 -2
  41. package/test/run.js +0 -109
  42. package/test/sfz-harness/README.md +0 -87
  43. package/test/sfz-harness/adopted-stylesheets-hook.js +0 -240
  44. package/test/sfz-harness/byte-map.js +0 -137
  45. package/test/sfz-harness/charset-round-trip.js +0 -161
  46. package/test/sfz-harness/check-determinism.js +0 -57
  47. package/test/sfz-harness/common.js +0 -154
  48. package/test/sfz-harness/content-type-sniffing.js +0 -83
  49. package/test/sfz-harness/css-fonts-minifier.js +0 -235
  50. package/test/sfz-harness/css-property-filter.js +0 -113
  51. package/test/sfz-harness/dom-stub.js +0 -13
  52. package/test/sfz-harness/entry-compression.js +0 -77
  53. package/test/sfz-harness/filename-characters.js +0 -55
  54. package/test/sfz-harness/filename-max-length.js +0 -69
  55. package/test/sfz-harness/font-face-composite.js +0 -135
  56. package/test/sfz-harness/format-rules.js +0 -617
  57. package/test/sfz-harness/gen-e2e-page.js +0 -14
  58. package/test/sfz-harness/inlined-functions.js +0 -82
  59. package/test/sfz-harness/option-wiring.js +0 -60
  60. package/test/sfz-harness/pages-archive.js +0 -318
  61. package/test/sfz-harness/pages-router.js +0 -143
  62. package/test/sfz-harness/relocation-cost.js +0 -94
  63. package/test/sfz-harness/search-triggers.js +0 -35
  64. package/test/sfz-harness/smoke.js +0 -10
  65. package/test/sfz-harness/stored-trigger.js +0 -95
  66. package/test/sfz-harness/trigger-seeds.json +0 -120
  67. package/test/sfz-harness/zip64.js +0 -77
  68. package/zip-build/README.md +0 -56
  69. package/zip-build/lib/fflate-streams.js +0 -47
  70. package/zip-build/lib/fflate.js +0 -2692
  71. package/zip-build/lib/zip-vendor-worker.js +0 -3
  72. package/zip-build/lib/zip-vendor.js +0 -3
  73. package/zip-build/lib/zip.js +0 -25
  74. package/zip-build/package-lock.json +0 -665
  75. package/zip-build/package.json +0 -15
  76. package/zip-build/reserved-property-names.json +0 -10630
  77. package/zip-build/rollup.config.js +0 -61
package/eslint.config.mjs DELETED
@@ -1,74 +0,0 @@
1
- import js from "@eslint/js";
2
-
3
- export default [
4
- {
5
- ignores: [
6
- "vendor/**",
7
- "zip-build/lib/**"
8
- ]
9
- },
10
- js.configs.recommended,
11
- {
12
- languageOptions: {
13
- ecmaVersion: 2025,
14
- sourceType: "module",
15
- globals: {
16
- console: "readonly",
17
- }
18
- },
19
- rules: {
20
- "linebreak-style": [
21
- "error",
22
- "unix"
23
- ],
24
- "quotes": [
25
- "error",
26
- "double"
27
- ],
28
- "semi": [
29
- "error",
30
- "always"
31
- ],
32
- "no-console": [
33
- "warn"
34
- ],
35
- "no-empty": [
36
- "error",
37
- {
38
- "allowEmptyCatch": true
39
- }
40
- ]
41
- }
42
- },
43
- {
44
- files: ["test/sfz-harness/**"],
45
- languageOptions: {
46
- globals: {
47
- Deno: "readonly",
48
- setTimeout: "readonly",
49
- Blob: "readonly",
50
- TextDecoder: "readonly",
51
- TextEncoder: "readonly",
52
- URL: "readonly",
53
- performance: "readonly"
54
- }
55
- },
56
- rules: {
57
- "no-console": "off"
58
- }
59
- },
60
- {
61
- files: ["test/capture/**", "test/run.js"],
62
- languageOptions: {
63
- globals: {
64
- Deno: "readonly",
65
- Response: "readonly",
66
- TextDecoder: "readonly",
67
- URL: "readonly"
68
- }
69
- },
70
- rules: {
71
- "no-console": "off"
72
- }
73
- }
74
- ];
@@ -1,74 +0,0 @@
1
- # Capture harness
2
-
3
- Tests that drive the real capture pipeline — `getPageData()`, `Processor`, `loadPage`, the batch
4
- fetch layer — in Deno, with an injected fetch and a parser instead of a browser. Every resource a
5
- capture asks for is served from a map declared in the suite, so there is no network and no page.
6
-
7
- It exists because the [SFZ harness](../sfz-harness/README.md) next door covers the archive writer and
8
- its neighbours, and nothing covered `core/index.js`. A defect in the capture pipeline could only be
9
- caught by driving Chrome from `single-file-cli`, in another repository, against a published build.
10
-
11
- Run them with Deno, from the repository root:
12
-
13
- ```
14
- npm test
15
- ```
16
-
17
- or this directory alone, through the same runner:
18
-
19
- ```
20
- deno run --allow-read --allow-run test/run.js capture
21
- ```
22
-
23
- or one suite at a time, which needs no runner:
24
-
25
- ```
26
- deno run --allow-read test/capture/resource-cap.js
27
- ```
28
-
29
- `common.js` and `dom.js` are named in the runner's `NOT_SUITES` list because they assert
30
- nothing. Every other `.js` file here is run.
31
-
32
- Unlike the SFZ harness, these download `@b-fuze/deno-dom` from JSR, so a cold cache needs network.
33
- The version is pinned in `dom.js` and `deno.lock` carries its integrity hash, so a cold run fetches
34
- that exact build or fails. `test/*` is ignored by `.gitignore` with one exception per directory, so a
35
- new test directory needs its own `!` line or nothing in it is ever committed.
36
-
37
- ## The suites
38
-
39
- | Script | What it covers |
40
- |---|---|
41
- | `resource-cap.js` | That `maxResourceSize` applies to what the capture fetches and never to the page document itself. A page supplied as content is untouched, a page fetched by `saveRawPage` is untouched, an image over the cap is still dropped, frame content supplied as data is untouched, and a frame fetched in raw mode is still dropped. The raw-page case is a regression test: the cap used to empty the document, so a 2.5 MB page was saved as 525 bytes with no body, exit code 0 and no warning. |
42
-
43
- ## How it works
44
-
45
- `dom.js` installs the globals core reads when its modules are evaluated — `DOMParser`, `Document`,
46
- `window`, `MutationObserver`. Import it before core, which is why `common.js` imports `single-file.js`
47
- dynamically.
48
-
49
- `common.js` exports `capture(resources, options)`, which returns the saved page as a string. Two
50
- things about it are forced by core rather than chosen. `init()` builds the util instance once per
51
- process and returns early ever after, so the injected fetch cannot be swapped per capture: one
52
- dispatcher is installed and `capture()` points it at the map for the run in progress. And a capture
53
- that passes no document never runs `preProcessDoc`, so the arrays it would have produced have to be
54
- supplied empty — `processWorklets` and its neighbours read `.length` with no guard.
55
-
56
- `frameData(windowId, baseURI, content)` builds the frame data a content script would have captured,
57
- matched to a frame element carrying the same window id. `html(body, head)` wraps a fixture.
58
-
59
- ## What it cannot test
60
-
61
- Anything that reads a live document: `preProcessDoc`, `removeHiddenElements` and its marked elements,
62
- and the `getComputedStyle` callers in `core/infobar.js` and `modules/css-fonts-minifier.js`. Leave
63
- those options off here. The browser rigs in `single-file-cli` and `single-file-tests` cover them.
64
-
65
- deno-dom is not a browser parser. It materializes a whole `NodeList` when `children` is read, and
66
- `buildTrackIdMap` walks the tree child by child, so a fixture with 100k siblings overflows the stack.
67
- Size a fixture with long text in few elements.
68
-
69
- ## Adding a case
70
-
71
- Same rule as the SFZ harness: add checks to the suite that already covers the area rather than making
72
- a file per rule, write the comment that says *why* the rule exists, and confirm the check can fail.
73
- For `resource-cap.js` that was done by reverting the `&& !this.options.rootDocument` conjunct in
74
- `core/index.js`: exactly one check goes red, which is the check that names it.
@@ -1,50 +0,0 @@
1
- import { capture, html } from "./common.js";
2
-
3
- const PAGE_URL = "https://example.com/page.html";
4
- const FILE_URL = "file:///tmp/page.html";
5
- const PAGE = html("<h1>page</h1>");
6
-
7
- const resources = {
8
- [PAGE_URL]: { body: PAGE },
9
- [FILE_URL]: { body: PAGE }
10
- };
11
-
12
- let failed = false;
13
-
14
- // insertCanonicalLink was forced to true in single-file.js after the options were merged, so it read
15
- // as an option in three places and could be set from none: the CLI flag was written, measured doing
16
- // nothing, and removed again rather than shipped. It now defaults to true instead of being forced,
17
- // which is what makes the flag and the extension config key mean anything.
18
- {
19
- const content = await capture(resources, { url: PAGE_URL, content: PAGE });
20
- check("a canonical link is inserted by default", content.includes("rel=\"canonical\""), true);
21
- }
22
-
23
- {
24
- const content = await capture(resources, { url: PAGE_URL, content: PAGE, insertCanonicalLink: false });
25
- check("insertCanonicalLink false suppresses it", content.includes("rel=\"canonical\""), false);
26
- }
27
-
28
- {
29
- const content = await capture(resources, { url: PAGE_URL, content: PAGE, insertCanonicalLink: true });
30
- check("insertCanonicalLink true keeps it", content.includes("rel=\"canonical\""), true);
31
- }
32
-
33
- // The href guard is the reason the option is safe to default on: a page saved from disk has no
34
- // canonical URL to point at, and the element is skipped rather than written with a file: href.
35
- {
36
- const content = await capture(resources, { url: FILE_URL, content: PAGE });
37
- check("a page saved from file: gets no canonical link", content.includes("rel=\"canonical\""), false);
38
- }
39
-
40
- if (failed) {
41
- console.log("FAILED");
42
- Deno.exit(1);
43
- }
44
- console.log("OK");
45
-
46
- function check(label, actual, expected) {
47
- const ok = actual === expected;
48
- console.log(`${ok ? "PASS" : "FAIL"} ${label}: ${actual}${ok ? "" : " (expected " + expected + ")"}`);
49
- failed ||= !ok;
50
- }
@@ -1,73 +0,0 @@
1
- import "./dom.js";
2
-
3
- const { init, getPageData, helper } = await import("../../single-file.js");
4
-
5
- const WIN_ID_ATTRIBUTE_NAME = helper.WIN_ID_ATTRIBUTE_NAME;
6
-
7
- // preProcessDoc fills these from the live document, and it only runs when a doc is passed. A capture
8
- // driven from here passes none, so the arrays it would have produced have to be supplied empty:
9
- // processWorklets and its neighbours read .length with no guard.
10
- const EMPTY_DOC_DATA = {
11
- adoptedStyleSheets: [],
12
- canvases: [],
13
- fonts: [],
14
- images: [],
15
- posters: [],
16
- referrer: "",
17
- shadowRoots: [],
18
- stylesheets: [],
19
- usedFonts: [],
20
- videos: [],
21
- worklets: []
22
- };
23
-
24
- // init() builds the util instance once per process and returns early ever after, so the fetch cannot
25
- // be swapped per capture. One dispatcher is installed here and capture() points it at the resources
26
- // of the run in progress; captures are sequential, so nothing races.
27
- let resources = new Map();
28
-
29
- const initOptions = {
30
- fetch: fetchResource,
31
- frameFetch: fetchResource
32
- };
33
-
34
- init(initOptions);
35
-
36
- export {
37
- capture,
38
- frameData,
39
- html,
40
- helper,
41
- WIN_ID_ATTRIBUTE_NAME
42
- };
43
-
44
- async function capture(pageResources, options) {
45
- resources = pageResources instanceof Map ? pageResources : new Map(Object.entries(pageResources));
46
- const pageData = await getPageData({ ...EMPTY_DOC_DATA, ...options }, initOptions, null, null);
47
- return pageData.content;
48
- }
49
-
50
- function fetchResource(url) {
51
- const resource = resources.get(url);
52
- if (!resource) {
53
- return Promise.resolve(new Response("", { status: 404 }));
54
- }
55
- const contentType = resource.contentType || "text/html";
56
- return Promise.resolve(new Response(resource.body, {
57
- status: resource.status || 200,
58
- headers: { "content-type": contentType }
59
- }));
60
- }
61
-
62
- // A frame whose content was captured by the content script arrives as frame data keyed by the window
63
- // id its element carries. Outside raw mode this is the only way a frame is ever filled.
64
- function frameData(windowId, baseURI, content) {
65
- return { ...EMPTY_DOC_DATA, windowId, baseURI, content, scrollPosition: { x: 0, y: 0 } };
66
- }
67
-
68
- // deno-dom materializes a whole NodeList when children is read, and buildTrackIdMap walks the tree
69
- // child by child, so a fixture with 100k siblings overflows the stack. Size a fixture with long text
70
- // in few elements, never with many elements.
71
- function html(body, head = "") {
72
- return "<!DOCTYPE html><html><head>" + head + "</head><body>" + body + "</body></html>";
73
- }
@@ -1,71 +0,0 @@
1
- import { helper } from "./common.js";
2
-
3
- // The loadDeferredImages* options were renamed to loadDeferredContent* because they never applied to
4
- // images only: they govern every kind of deferred content, including frames and whole virtualized
5
- // message lists. The old names stay accepted because they are public API in two ways core cannot
6
- // see - a library caller passing them to getPageData, and another extension passing them through the
7
- // external capture API - so this suite pins the compatibility shim rather than the rename.
8
-
9
- const DEPRECATED_NAMES = {
10
- loadDeferredImages: "loadDeferredContent",
11
- loadDeferredImagesMaxIdleTime: "loadDeferredContentMaxIdleTime",
12
- loadDeferredImagesBlockCookies: "loadDeferredContentBlockCookies",
13
- loadDeferredImagesBlockStorage: "loadDeferredContentBlockStorage",
14
- loadDeferredImagesKeepZoomLevel: "loadDeferredContentKeepZoomLevel",
15
- loadDeferredImagesDispatchScrollEvent: "loadDeferredContentDispatchScrollEvent",
16
- loadDeferredImagesBeforeFrames: "loadDeferredContentBeforeFrames"
17
- };
18
-
19
- let failed = false;
20
-
21
- {
22
- for (const deprecatedName of Object.keys(DEPRECATED_NAMES)) {
23
- const optionName = DEPRECATED_NAMES[deprecatedName];
24
- const value = deprecatedName.endsWith("MaxIdleTime") ? 3000 : true;
25
- const normalized = helper.normalizeOptions({ [deprecatedName]: value });
26
- check(`${deprecatedName} fills ${optionName}`, normalized[optionName], value);
27
- }
28
- }
29
-
30
- // A false or 0 must survive: testing the deprecated value for truthiness instead of for undefined
31
- // would silently drop every option a user turned off, which is the half that matters most here.
32
- {
33
- const normalized = helper.normalizeOptions({ loadDeferredImages: false, loadDeferredImagesMaxIdleTime: 0 });
34
- check("a deprecated false is carried over", normalized.loadDeferredContent, false);
35
- check("a deprecated 0 is carried over", normalized.loadDeferredContentMaxIdleTime, 0);
36
- }
37
-
38
- {
39
- const normalized = helper.normalizeOptions({ loadDeferredImages: false, loadDeferredContent: true });
40
- check("the new name wins when both are set", normalized.loadDeferredContent, true);
41
- }
42
-
43
- // getPageData is handed the caller's own object, so filling the new names in place would edit an
44
- // object the caller still holds and may reuse for a second capture.
45
- {
46
- const options = { loadDeferredImages: true };
47
- const normalized = helper.normalizeOptions(options);
48
- check("the caller's object is not mutated", options.loadDeferredContent, undefined);
49
- check("a copy is returned instead", normalized.loadDeferredContent, true);
50
- }
51
-
52
- {
53
- const options = { removeFrames: true };
54
- check("an options object without deprecated names is returned as is", helper.normalizeOptions(options), options);
55
- }
56
-
57
- {
58
- check("a missing options object is tolerated", helper.normalizeOptions(undefined), undefined);
59
- }
60
-
61
- if (failed) {
62
- console.log("FAILED");
63
- Deno.exit(1);
64
- }
65
- console.log("OK");
66
-
67
- function check(label, actual, expected) {
68
- const ok = actual === expected;
69
- console.log(`${ok ? "PASS" : "FAIL"} ${label}: ${actual}${ok ? "" : " (expected " + expected + ")"}`);
70
- failed ||= !ok;
71
- }
@@ -1,26 +0,0 @@
1
- // Three modules read globals when they are evaluated, so every one of them has to exist before core
2
- // is imported: core/util.js captures DOMParser, and processors/hooks/content/content-hooks-frames.js
3
- // reads globalThis.window, then calls init() and new MutationObserver(init) at module scope. That
4
- // hook belongs to the page world and does nothing useful here; it only has to load without throwing.
5
- // Import this module first and import single-file.js dynamically, the way common.js does.
6
- import { DOMParser, Document, Element } from "jsr:@b-fuze/deno-dom@0.1.56";
7
-
8
- globalThis.DOMParser = DOMParser;
9
- globalThis.Document = Document;
10
- globalThis.window = globalThis;
11
- globalThis.MutationObserver = class {
12
- observe() { }
13
- disconnect() { }
14
- };
15
-
16
- // deno-dom implements neither of these, so removeEmbedScripts throws here and nowhere else. Mapping
17
- // them onto the qualified-name methods is faithful for what deno-dom can represent, which is only
18
- // null-namespace attributes: it drops the prefix of xlink:href and lowercases nothing, so the
19
- // namespaced and mixed-case cases cannot be written as a fixture at all. Those are covered by the
20
- // browser suite in single-file-cli, which drives a real DOM.
21
- Element.prototype.setAttributeNS = function (namespaceURI, qualifiedName, value) {
22
- this.setAttribute(qualifiedName, value);
23
- };
24
- Element.prototype.removeAttributeNS = function (namespaceURI, localName) {
25
- this.removeAttribute(localName);
26
- };
@@ -1,205 +0,0 @@
1
- import "./dom.js";
2
-
3
- // core/util.js captures globalThis.DOMParser when it loads, and deno-dom throws on "text/xml", so
4
- // the MAFF metadata could not be parsed here at all. This substitutes a stub for that one mime type,
5
- // installed before common.js imports core, the same ordering constraint dom.js itself documents.
6
- //
7
- // What the stub stands for and what it does not. The two defects fixed alongside this suite are both
8
- // about what core does with what the parser HANDS BACK — an attribute that came back null, and which
9
- // of two almost-identical option fields gets written out — so a stub returning null or a string
10
- // covers them faithfully. It does NOT cover the parse: that `RDF > Description > originalurl` matches
11
- // `<RDF:RDF><RDF:Description><MAF:originalurl>` by local name, and that getAttributeNS resolves the
12
- // RDF prefix, are properties of a real XML DOM that only a browser suite can confirm.
13
- const XML_DOCUMENTS = new Map();
14
- const NativeDOMParser = globalThis.DOMParser;
15
-
16
- class StubXMLDocument {
17
- constructor(values) {
18
- this.values = values;
19
- }
20
- // undefined means the element is absent, null means it is present with no RDF:resource attribute
21
- querySelector(selector) {
22
- const localName = selector.split(">").pop().trim();
23
- const value = this.values[localName];
24
- return value === undefined ? null : { getAttributeNS: () => value };
25
- }
26
- }
27
-
28
- globalThis.DOMParser = class {
29
- parseFromString(content, mimeType) {
30
- if (mimeType == "text/xml") {
31
- return new StubXMLDocument(XML_DOCUMENTS.get(content) || {});
32
- }
33
- return new NativeDOMParser().parseFromString(content, mimeType);
34
- }
35
- };
36
-
37
- const { capture, frameData, html, WIN_ID_ATTRIBUTE_NAME } = await import("./common.js");
38
-
39
- const PAGE_URL = "https://example.com/page.html";
40
- const RDF_URL = "https://example.com/index.rdf";
41
- const FRAME_URL = "https://example.com/frame-dir/frame.html";
42
- const FRAME_RDF_URL = "https://example.com/frame-dir/index.rdf";
43
- const ORIGINAL_URL = "https://original.example/real.html";
44
- const ARCHIVE_TIME = "Mon, 01 Jan 2024 10:20:30 GMT";
45
- const ARCHIVE_TIME_MS = new Date(ARCHIVE_TIME).getTime();
46
- const COMPLETE = { originalurl: ORIGINAL_URL, archivetime: ARCHIVE_TIME };
47
- const PAGE = html("<h1>page</h1>");
48
- const FRAME_PAGE = html("<h1>frame</h1>");
49
- const HOST_PAGE = html("<h1>host</h1><iframe src=\"" + FRAME_URL + "\" " + WIN_ID_ATTRIBUTE_NAME + "=\"0.1\"></iframe>");
50
-
51
- // The canonical link would be the obvious place to read the recovered url, but deno-dom reflects
52
- // neither the href nor the type property, so core's `element.href = ...` leaves no attribute behind.
53
- // Two observables survive that: the SingleFile comment, which core builds from options.saveUrl, and
54
- // the embedded options block, which is where the second defect lives.
55
- const OPTIONS_BLOCK = /<script data-single-file-options[^>]*>([^<]*)<\/script>/;
56
-
57
- let fixtureIndex = 0;
58
-
59
- function rdf(values) {
60
- const content = "<?xml version=\"1.0\"?><!-- fixture " + (fixtureIndex++) + " -->";
61
- XML_DOCUMENTS.set(content, values);
62
- return content;
63
- }
64
-
65
- class CountingResources extends Map {
66
- constructor(entries) {
67
- super(entries);
68
- this.counts = new Map();
69
- }
70
- get(key) {
71
- this.counts.set(key, (this.counts.get(key) || 0) + 1);
72
- return super.get(key);
73
- }
74
- countOf(key) {
75
- return this.counts.get(key) || 0;
76
- }
77
- }
78
-
79
- function resources(rdfContent) {
80
- const entries = [
81
- [PAGE_URL, { body: PAGE }],
82
- [FRAME_URL, { body: FRAME_PAGE }]
83
- ];
84
- if (rdfContent !== undefined) {
85
- entries.push([RDF_URL, { body: rdfContent, contentType: "text/xml" }]);
86
- }
87
- return new CountingResources(entries);
88
- }
89
-
90
- function commentURL(content) {
91
- const match = content.match(/ url: ([^\n]*)/);
92
- return match && match[1].trim();
93
- }
94
-
95
- function embeddedOptions(content) {
96
- const match = content.match(OPTIONS_BLOCK);
97
- return match && JSON.parse(match[1]);
98
- }
99
-
100
- let failed = false;
101
-
102
- // readMaffMetadata is the new name of enableMaff, which was implemented in core and set by nothing:
103
- // no CLI flag, no config key, no UI anywhere. That is what made renaming it free, and exposing it is
104
- // what made the two defects below reachable by a user.
105
- {
106
- const map = resources(rdf(COMPLETE));
107
- const content = await capture(map, { url: PAGE_URL, content: PAGE, insertSingleFileComment: true });
108
- check("index.rdf is not requested when the option is off", map.countOf(RDF_URL), 0);
109
- check("the page url is saved when the option is off", commentURL(content), PAGE_URL);
110
- }
111
-
112
- {
113
- const map = resources(rdf(COMPLETE));
114
- const content = await capture(map, { url: PAGE_URL, content: PAGE, readMaffMetadata: true, insertSingleFileComment: true });
115
- check("index.rdf is requested when the option is on", map.countOf(RDF_URL), 1);
116
- check("the original url is recovered", commentURL(content), ORIGINAL_URL);
117
- }
118
-
119
- // THE CRASH. An originalurl with no RDF:resource made getAttributeNS return null, saveUrl became
120
- // null, and the canonical link then called .match() on it and failed the whole capture with a
121
- // TypeError. The archivetime branch beside it had always guarded its own value, so the two halves of
122
- // one method disagreed with each other.
123
- {
124
- let threw = false;
125
- let content = "";
126
- try {
127
- content = await capture(resources(rdf({ originalurl: null, archivetime: ARCHIVE_TIME })), {
128
- url: PAGE_URL,
129
- content: PAGE,
130
- readMaffMetadata: true,
131
- insertSingleFileComment: true
132
- });
133
- } catch {
134
- threw = true;
135
- }
136
- check("an originalurl with no resource attribute does not fail the capture", threw, false);
137
- check("and the page url is kept", commentURL(content), PAGE_URL);
138
- }
139
-
140
- {
141
- const content = await capture(resources(rdf({ originalurl: ORIGINAL_URL, archivetime: null })), {
142
- url: PAGE_URL,
143
- content: PAGE,
144
- readMaffMetadata: true,
145
- insertSingleFileComment: true
146
- });
147
- check("an archivetime with no resource attribute is tolerated", commentURL(content), ORIGINAL_URL);
148
- }
149
-
150
- {
151
- const content = await capture(resources(), { url: PAGE_URL, content: PAGE, readMaffMetadata: true, insertSingleFileComment: true });
152
- check("a missing index.rdf leaves the page url in place", commentURL(content), PAGE_URL);
153
- }
154
-
155
- // THE INCONSISTENCY. saveFilenameTemplateData wrote saveUrl from options.url, one line above writing
156
- // saveDate from the value MAFF had just recovered, so the embedded block paired the archive's date
157
- // with the extracted copy's path, and a recompute in the editor resolved {url-*} against the wrong
158
- // one. options.saveUrl and options.url are identical on every other code path, which is what let it
159
- // sit unnoticed.
160
- {
161
- const content = await capture(resources(rdf(COMPLETE)), {
162
- url: PAGE_URL,
163
- content: PAGE,
164
- readMaffMetadata: true,
165
- saveFilenameTemplateData: true
166
- });
167
- const embedded = embeddedOptions(content);
168
- check("the embedded options block exists", Boolean(embedded), true);
169
- if (embedded) {
170
- check("the embedded saveUrl is the recovered one", embedded.saveUrl, ORIGINAL_URL);
171
- check("the embedded saveDate is the recovered one", embedded.saveDate, ARCHIVE_TIME_MS);
172
- }
173
- }
174
-
175
- {
176
- const content = await capture(resources(rdf(COMPLETE)), { url: PAGE_URL, content: PAGE, saveFilenameTemplateData: true });
177
- const embedded = embeddedOptions(content);
178
- check("the embedded saveUrl is the page url when the option is off", embedded && embedded.saveUrl, PAGE_URL);
179
- }
180
-
181
- // Only the root document reaches Processor.initialize, because Runner.run guards that call with
182
- // `if (this.root)`. That guard is the whole reason a page with twenty frames does not make twenty
183
- // pointless index.rdf requests, and nothing else pins it. Note initializeProcessor resets a list of
184
- // root-only options for frames and readMaffMetadata is deliberately NOT in it: next to this guard
185
- // such a reset is dead code, which is exactly what adding one and watching nothing change proved.
186
- {
187
- const map = resources(rdf(COMPLETE));
188
- const frames = [frameData("0.1", FRAME_URL, FRAME_PAGE)];
189
- const content = await capture(map, { url: PAGE_URL, content: HOST_PAGE, frames, readMaffMetadata: true });
190
- check("the root asks for its index.rdf", map.countOf(RDF_URL), 1);
191
- check("a frame does not ask for one of its own", map.countOf(FRAME_RDF_URL), 0);
192
- check("the frame is still captured", content.includes("frame"), true);
193
- }
194
-
195
- if (failed) {
196
- console.log("FAILED");
197
- Deno.exit(1);
198
- }
199
- console.log("OK");
200
-
201
- function check(label, actual, expected) {
202
- const ok = actual === expected;
203
- console.log(`${ok ? "PASS" : "FAIL"} ${label}: ${actual}${ok ? "" : " (expected " + expected + ")"}`);
204
- failed ||= !ok;
205
- }
@@ -1,79 +0,0 @@
1
- import { capture, frameData, html, WIN_ID_ATTRIBUTE_NAME } from "./common.js";
2
-
3
- const PAGE_URL = "https://example.com/big.html";
4
- const HOST_URL = "https://example.com/host.html";
5
- const IMAGE_URL = "https://example.com/big.png";
6
- const PAGE_MARKER = "BIG PAGE MARKER";
7
- const HOST_MARKER = "HOST PAGE MARKER";
8
-
9
- // One paragraph of 2.1 MB rather than many small ones, for the reason common.js gives.
10
- const BIG_PAGE = html("<h1>" + PAGE_MARKER + "</h1><p>" + "filler ".repeat(300000) + "</p>");
11
- const HOST_PAGE = html("<h1>" + HOST_MARKER + "</h1><iframe src=\"" + PAGE_URL + "\" " + WIN_ID_ATTRIBUTE_NAME + "=\"0.1\"></iframe>");
12
- const IMAGE_PAGE = html("<h1>" + HOST_MARKER + "</h1><img src=\"" + IMAGE_URL + "\">");
13
- const BIG_IMAGE = new Uint8Array(2 * 1024 * 1024).fill(0x21);
14
-
15
- const resources = {
16
- [PAGE_URL]: { body: BIG_PAGE },
17
- [HOST_URL]: { body: HOST_PAGE },
18
- [IMAGE_URL]: { body: BIG_IMAGE, contentType: "image/png" }
19
- };
20
-
21
- // One megabyte, so every fixture above is over it and the default of ten is not in the way.
22
- const CAP = { maxResourceSizeEnabled: true, maxResourceSize: 1 };
23
-
24
- let failed = false;
25
-
26
- // The content a browser captured is handed to core as a string and never fetched, so the cap has no
27
- // point at which it could fire. This is what every extension save and every non-raw CLI capture does.
28
- {
29
- const content = await capture(resources, { url: PAGE_URL, content: BIG_PAGE, ...CAP });
30
- check("a page supplied as content is never capped", content.includes(PAGE_MARKER), true);
31
- }
32
-
33
- // The regression test. loadPage fetches the document itself in raw mode, and until rootDocument was
34
- // excluded the cap emptied it: a 2.5 MB page came out as 525 bytes with no body at all, exit code 0
35
- // and no warning. The cap is documented to apply to "images, fonts, stylesheets, scripts, frames,
36
- // videos and audios", never to the page.
37
- {
38
- const content = await capture(resources, { url: PAGE_URL, saveRawPage: true, ...CAP });
39
- check("a raw page over the cap keeps its content", content.includes(PAGE_MARKER), true);
40
- }
41
-
42
- // The control for the test above: the same cap, in the same capture, still has to drop a resource.
43
- // A fix that exempted everything would pass the raw-page check and break the option.
44
- {
45
- const capped = await capture(resources, { url: HOST_URL, content: IMAGE_PAGE, ...CAP });
46
- const uncapped = await capture(resources, { url: HOST_URL, content: IMAGE_PAGE });
47
- check("an image over the cap is left out", capped.includes("data:image/png;base64"), false);
48
- check("the page holding it is kept", capped.includes(HOST_MARKER), true);
49
- check("the same image is embedded with the cap off", uncapped.includes("data:image/png;base64"), true);
50
- }
51
-
52
- // Frame content captured by the content script arrives as data, like the top document above, so the
53
- // cap cannot reach it either.
54
- {
55
- const frames = [frameData("0.1", PAGE_URL, BIG_PAGE)];
56
- const content = await capture(resources, { url: HOST_URL, content: HOST_PAGE, frames, ...CAP });
57
- check("a frame supplied as data is never capped", content.includes(PAGE_MARKER), true);
58
- check("its host is kept", content.includes(HOST_MARKER), true);
59
- }
60
-
61
- // In raw mode there is no frame data: resolveFrameURLs pushes a frame with no content and its runner
62
- // fetches the frame document, which is the one caller the cap is meant for. Dropping it is correct.
63
- {
64
- const content = await capture(resources, { url: HOST_URL, saveRawPage: true, ...CAP });
65
- check("a raw frame over the cap is dropped", content.includes(PAGE_MARKER), false);
66
- check("its host is kept", content.includes(HOST_MARKER), true);
67
- }
68
-
69
- if (failed) {
70
- console.log("FAILED");
71
- Deno.exit(1);
72
- }
73
- console.log("OK");
74
-
75
- function check(label, actual, expected) {
76
- const ok = actual === expected;
77
- console.log(`${ok ? "PASS" : "FAIL"} ${label}: ${actual}${ok ? "" : " (expected " + expected + ")"}`);
78
- failed ||= !ok;
79
- }