single-file-core 1.6.1 → 1.6.3
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.
- package/core/filename.js +62 -0
- package/core/helper.js +9 -32
- package/modules/css-fonts-minifier.js +34 -13
- package/modules/template-formatter.js +8 -2
- package/package.json +10 -2
- package/processors/compression/compression-constants.js +28 -0
- package/processors/compression/compression.js +3 -1
- package/.github/workflows/publish.yml +0 -88
- package/.github/workflows/test.yml +0 -31
- package/.github/workflows/vendor.yml +0 -39
- package/css-tree-build/README.md +0 -39
- package/css-tree-build/build.js +0 -66
- package/css-tree-build/package-lock.json +0 -511
- package/css-tree-build/package.json +0 -13
- package/deno.lock +0 -19
- package/doc/assets/singlefile-archive-byte-map.svg +0 -276
- package/doc/assets/singlefile-archive-lenses.svg +0 -245
- package/doc/singlefile-archive.md +0 -1968
- package/eslint.config.mjs +0 -74
- package/test/capture/README.md +0 -74
- package/test/capture/canonical-link.js +0 -50
- package/test/capture/common.js +0 -73
- package/test/capture/deferred-content-options.js +0 -71
- package/test/capture/dom.js +0 -26
- package/test/capture/maff-metadata.js +0 -205
- package/test/capture/resource-cap.js +0 -79
- package/test/capture/script-uri-sanitization.js +0 -82
- package/test/fixtures/README.md +0 -18
- package/test/fixtures/classic-digest.html +0 -5
- package/test/fixtures/generate-fixtures.sh +0 -31
- package/test/fixtures/multi-page-dedup.zip.html +0 -70
- package/test/fixtures/multi-page.zip.html +0 -26
- package/test/fixtures/single-page.zip.html +0 -11
- package/test/fixtures/site/multi-page/alpha.html +0 -2
- package/test/fixtures/site/multi-page/beta.html +0 -2
- package/test/fixtures/site/multi-page/index.html +0 -2
- package/test/fixtures/site/multi-page/sub/gamma.html +0 -2
- package/test/fixtures/site/multi-page-dedup/index.html +0 -2
- package/test/fixtures/site/multi-page-dedup/logo.png +0 -0
- package/test/fixtures/site/multi-page-dedup/one.html +0 -2
- package/test/fixtures/site/multi-page-dedup/shared.css +0 -3
- package/test/fixtures/site/multi-page-dedup/two.html +0 -2
- package/test/run.js +0 -109
- package/test/sfz-harness/README.md +0 -87
- package/test/sfz-harness/adopted-stylesheets-hook.js +0 -240
- package/test/sfz-harness/byte-map.js +0 -137
- package/test/sfz-harness/charset-round-trip.js +0 -161
- package/test/sfz-harness/check-determinism.js +0 -57
- package/test/sfz-harness/common.js +0 -154
- package/test/sfz-harness/content-type-sniffing.js +0 -83
- package/test/sfz-harness/css-fonts-minifier.js +0 -235
- package/test/sfz-harness/css-property-filter.js +0 -113
- package/test/sfz-harness/dom-stub.js +0 -13
- package/test/sfz-harness/entry-compression.js +0 -77
- package/test/sfz-harness/filename-characters.js +0 -55
- package/test/sfz-harness/filename-max-length.js +0 -69
- package/test/sfz-harness/font-face-composite.js +0 -135
- package/test/sfz-harness/format-rules.js +0 -617
- package/test/sfz-harness/gen-e2e-page.js +0 -14
- package/test/sfz-harness/inlined-functions.js +0 -82
- package/test/sfz-harness/option-wiring.js +0 -60
- package/test/sfz-harness/pages-archive.js +0 -318
- package/test/sfz-harness/pages-router.js +0 -143
- package/test/sfz-harness/relocation-cost.js +0 -94
- package/test/sfz-harness/search-triggers.js +0 -35
- package/test/sfz-harness/smoke.js +0 -10
- package/test/sfz-harness/stored-trigger.js +0 -95
- package/test/sfz-harness/trigger-seeds.json +0 -120
- package/test/sfz-harness/zip64.js +0 -77
- package/zip-build/README.md +0 -56
- package/zip-build/lib/fflate-streams.js +0 -47
- package/zip-build/lib/fflate.js +0 -2692
- package/zip-build/lib/zip-vendor-worker.js +0 -3
- package/zip-build/lib/zip-vendor.js +0 -9
- package/zip-build/lib/zip.js +0 -25
- package/zip-build/package-lock.json +0 -665
- package/zip-build/package.json +0 -15
- package/zip-build/reserved-property-names.json +0 -10630
- package/zip-build/rollup.config.js +0 -61
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
// §5.2 of doc/singlefile-archive.md quantifies what relocating the extra-data element costs. The
|
|
2
|
-
// figures it carried until 2026-09-12 came from two live captures and one fixture, and two of the
|
|
3
|
-
// three did not reconstruct: the paragraph subtracted the 17 bytes of terminator and end tags from
|
|
4
|
-
// the reservation without subtracting the element, which the appended placement also carries, so
|
|
5
|
-
// it over-counted by the whole element. This pins the corrected arithmetic. The cost is the
|
|
6
|
-
// reservation margin alone, it is positive on every rung whenever an element exists, and the only
|
|
7
|
-
// way to make relocation save bytes is to have no element to relocate.
|
|
8
|
-
import { makePageData, makeOptions, runProcess, freezeDate } from "./common.js";
|
|
9
|
-
|
|
10
|
-
const DECODER = new TextDecoder("windows-1252");
|
|
11
|
-
const OPEN_TAG = "<sfz-extra-data>";
|
|
12
|
-
const CLOSE_TAG = "</sfz-extra-data>";
|
|
13
|
-
const END_TAGS_LENGTH = "</body></html>".length;
|
|
14
|
-
|
|
15
|
-
let failed = false;
|
|
16
|
-
|
|
17
|
-
function check(label, actual, expected) {
|
|
18
|
-
const ok = actual === expected;
|
|
19
|
-
console.log(`${ok ? "PASS" : "FAIL"} ${label}: ${actual}${ok ? "" : " (expected " + expected + ")"}`);
|
|
20
|
-
failed ||= !ok;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function reservationSize(length) {
|
|
24
|
-
return Math.ceil(length * 1.01) + 32;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function inspect(bytes) {
|
|
28
|
-
const text = DECODER.decode(bytes);
|
|
29
|
-
const open = text.indexOf(OPEN_TAG);
|
|
30
|
-
const firstHeader = text.indexOf("PK\x03\x04");
|
|
31
|
-
return {
|
|
32
|
-
total: bytes.length,
|
|
33
|
-
element: open == -1 ? 0 : text.indexOf(CLOSE_TAG, open) + CLOSE_TAG.length - open,
|
|
34
|
-
relocated: open != -1 && open < firstHeader
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function build(seed, targetLength, overrides) {
|
|
39
|
-
const restoreDate = freezeDate();
|
|
40
|
-
const appended = inspect((await runProcess(makePageData(seed, targetLength), makeOptions(overrides))).bytes);
|
|
41
|
-
const relocated = inspect((await runProcess(makePageData(seed, targetLength), makeOptions({ ...overrides, preventAppendedData: true }))).bytes);
|
|
42
|
-
restoreDate();
|
|
43
|
-
return { appended, relocated, cost: relocated.total - appended.total };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// the element changes length between the two passes once the reservation is large enough to move
|
|
47
|
-
// the central-directory offsets it encodes, and then the reservation comes from the first pass
|
|
48
|
-
// while the element written into it comes from the second. These fixtures stay below that, which
|
|
49
|
-
// is what lets the identity be asserted exactly rather than within a tolerance.
|
|
50
|
-
for (const [label, seed, targetLength, closeTagLength] of [
|
|
51
|
-
["comment rung", 1, 64 * 1024, "-->".length],
|
|
52
|
-
["comment rung, larger", 2, 147 * 1024, "-->".length]
|
|
53
|
-
]) {
|
|
54
|
-
const { appended, relocated, cost } = await build(seed, targetLength, {});
|
|
55
|
-
check(`${label}: the element keeps its length`, relocated.element, appended.element);
|
|
56
|
-
check(`${label}: relocates`, relocated.relocated, true);
|
|
57
|
-
check(`${label}: cost`, cost,
|
|
58
|
-
reservationSize(appended.element) - appended.element - closeTagLength - END_TAGS_LENGTH);
|
|
59
|
-
check(`${label}: cost is positive`, cost > 0, true);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// the rung sets the constant, because it is the closing tag the relocated placement stops emitting
|
|
63
|
-
for (const [label, closeTag] of [
|
|
64
|
-
["script rung", "</script>"],
|
|
65
|
-
["svg CDATA rung", "]]></svg>"],
|
|
66
|
-
["plaintext rung", "</plaintext>"]
|
|
67
|
-
]) {
|
|
68
|
-
const startTag = { "</script>": "<script type=sfz-data>", "]]></svg>": "<svg><![CDATA[", "</plaintext>": "<plaintext>" }[closeTag];
|
|
69
|
-
const { appended, cost } = await build(3, 64 * 1024, { extractDataFromPageTags: [startTag, closeTag] });
|
|
70
|
-
check(`${label}: cost`, cost,
|
|
71
|
-
reservationSize(appended.element) - appended.element - closeTag.length - END_TAGS_LENGTH);
|
|
72
|
-
check(`${label}: cost is positive`, cost > 0, true);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// with no element there is nothing to reserve, so the appended run is dropped and nothing replaces
|
|
76
|
-
// it. This is the only case in which suppressing the run makes the file smaller.
|
|
77
|
-
{
|
|
78
|
-
const { appended, cost } = await build(4, 64 * 1024, { extractDataFromPage: false });
|
|
79
|
-
check("extraction disabled: no element", appended.element, 0);
|
|
80
|
-
check("extraction disabled: cost", cost, -("-->".length + END_TAGS_LENGTH));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// the budget triggers relocation once the element no longer fits beside the terminator and the end
|
|
84
|
-
// tags, so the boundary is exactly maxAppendedDataLength - 3 - 14
|
|
85
|
-
{
|
|
86
|
-
const { appended } = await build(5, 64 * 1024, {});
|
|
87
|
-
const fits = appended.element + "-->".length + END_TAGS_LENGTH;
|
|
88
|
-
const atBoundary = await build(5, 64 * 1024, { maxAppendedDataLength: fits });
|
|
89
|
-
const belowBoundary = await build(5, 64 * 1024, { maxAppendedDataLength: fits - 1 });
|
|
90
|
-
check("an element that exactly fits the budget stays appended", atBoundary.appended.relocated, false);
|
|
91
|
-
check("one byte less of budget relocates it", belowBoundary.appended.relocated, true);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
Deno.exit(failed ? 1 : 0);
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { makePageData, makeOptions, runProcess, freezeDate } from "./common.js";
|
|
2
|
-
|
|
3
|
-
const startSeed = Number(Deno.args[0] ?? 1);
|
|
4
|
-
const seedCount = Number(Deno.args[1] ?? 100);
|
|
5
|
-
const contentLength = Number(Deno.args[2] ?? 2 * 1024 * 1024);
|
|
6
|
-
|
|
7
|
-
const found = [];
|
|
8
|
-
const restoreDate = freezeDate();
|
|
9
|
-
const startTime = performance.now();
|
|
10
|
-
try {
|
|
11
|
-
for (let seed = startSeed; seed < startSeed + seedCount; seed++) {
|
|
12
|
-
const options = makeOptions();
|
|
13
|
-
const result = await runProcess(makePageData(seed, contentLength), options);
|
|
14
|
-
if (result.fallbackTag || result.extractionDisabled) {
|
|
15
|
-
found.push({ seed, fallbackTag: result.fallbackTag, pageSize: result.bytes.length });
|
|
16
|
-
console.log(`seed ${seed}: TRIGGER, fell back to ${result.fallbackTag}`);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
} finally {
|
|
20
|
-
restoreDate();
|
|
21
|
-
}
|
|
22
|
-
const seconds = (performance.now() - startTime) / 1000;
|
|
23
|
-
console.log(`${found.length} trigger(s) in ${seedCount} attempts (content ${contentLength} bytes, ${seconds.toFixed(1)}s)`);
|
|
24
|
-
if (found.length) {
|
|
25
|
-
const path = new URL("trigger-seeds.json", import.meta.url).pathname;
|
|
26
|
-
let existing = { contentLength, seeds: [] };
|
|
27
|
-
try {
|
|
28
|
-
existing = JSON.parse(Deno.readTextFileSync(path));
|
|
29
|
-
} catch {
|
|
30
|
-
// first run
|
|
31
|
-
}
|
|
32
|
-
const seeds = [...existing.seeds, ...found.filter(entry => !existing.seeds.some(known => known.seed == entry.seed))];
|
|
33
|
-
Deno.writeTextFileSync(path, JSON.stringify({ contentLength, seeds }, null, "\t") + "\n");
|
|
34
|
-
console.log(`saved to trigger-seeds.json (${seeds.length} total)`);
|
|
35
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { makePageData, makeOptions, runProcess } from "./common.js";
|
|
2
|
-
|
|
3
|
-
const options = makeOptions();
|
|
4
|
-
const pageData = makePageData(1, 256 * 1024);
|
|
5
|
-
const result = await runProcess(pageData, options);
|
|
6
|
-
console.log("page size:", result.bytes.length);
|
|
7
|
-
console.log("fallback tag:", result.fallbackTag);
|
|
8
|
-
console.log("extraction disabled:", result.extractionDisabled);
|
|
9
|
-
const head = new TextDecoder().decode(result.bytes.subarray(0, 200));
|
|
10
|
-
console.log("head:", JSON.stringify(head));
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { makePageData, makeOptions, runProcess, mulberry32 } from "./common.js";
|
|
2
|
-
|
|
3
|
-
function storedResource(name, literals) {
|
|
4
|
-
const rand = mulberry32(0xbeef);
|
|
5
|
-
const bytes = new Uint8Array(Math.max(4096, 256 + literals.length * 512)).map(() => (rand() * 256) | 0);
|
|
6
|
-
const encoder = new TextEncoder();
|
|
7
|
-
let offset = 256;
|
|
8
|
-
for (const literal of literals) {
|
|
9
|
-
bytes.set(encoder.encode(literal), offset);
|
|
10
|
-
offset += 512;
|
|
11
|
-
}
|
|
12
|
-
return { name, extension: ".jpg", content: bytes };
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
let failed = false;
|
|
16
|
-
|
|
17
|
-
function check(label, actual, expected) {
|
|
18
|
-
const ok = actual === expected;
|
|
19
|
-
console.log(`${ok ? "PASS" : "FAIL"} ${label}: ${actual}${ok ? "" : " (expected " + expected + ")"}`);
|
|
20
|
-
failed ||= !ok;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
{
|
|
24
|
-
const options = makeOptions();
|
|
25
|
-
const pageData = makePageData(1, 64 * 1024);
|
|
26
|
-
pageData.resources.images.push(storedResource("photo.jpg", ["-->"]));
|
|
27
|
-
const result = await runProcess(pageData, options);
|
|
28
|
-
check("stored '-->' falls back to", result.fallbackTag, "<script type=sfz-data>");
|
|
29
|
-
check("stored '-->' keeps extraction", result.extractionDisabled, false);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
{
|
|
33
|
-
const options = makeOptions();
|
|
34
|
-
const pageData = makePageData(2, 64 * 1024);
|
|
35
|
-
pageData.resources.images.push(storedResource("photo.jpg",
|
|
36
|
-
["-->", "</script>", "</style>", "</noframes>", "</noembed>", "</iframe>", "</xmp>", "]]>"]));
|
|
37
|
-
const result = await runProcess(pageData, options);
|
|
38
|
-
check("all closers exhaust to", result.fallbackTag, "<plaintext>");
|
|
39
|
-
check("all closers keep extraction", result.extractionDisabled, false);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
{
|
|
43
|
-
const options = makeOptions();
|
|
44
|
-
const pageData = makePageData(3, 64 * 1024);
|
|
45
|
-
pageData.resources.images.push(storedResource("photo.jpg", ["</xmp>"]));
|
|
46
|
-
const result = await runProcess(pageData, options);
|
|
47
|
-
check("stored '</xmp>' alone stays on comment path", result.fallbackTag, null);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// the rung patterns are matched over the archive BYTES rather than a decoded string, so these
|
|
51
|
-
// four pin the parts of that match a byte scan is easy to get wrong: closers are matched
|
|
52
|
-
// case-insensitively, a closer only counts when a tag-name terminator follows it, and the
|
|
53
|
-
// comment closer is '-->' or '--!>' and nothing else in between
|
|
54
|
-
|
|
55
|
-
{
|
|
56
|
-
const options = makeOptions();
|
|
57
|
-
const pageData = makePageData(4, 64 * 1024);
|
|
58
|
-
pageData.resources.images.push(storedResource("photo.jpg", ["-->", "</SCRIPT>"]));
|
|
59
|
-
const result = await runProcess(pageData, options);
|
|
60
|
-
check("an upper-case closer counts, so '</SCRIPT>' escalates to", result.fallbackTag, "<style type=sfz-data>");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
{
|
|
64
|
-
const options = makeOptions();
|
|
65
|
-
const pageData = makePageData(5, 64 * 1024);
|
|
66
|
-
pageData.resources.images.push(storedResource("photo.jpg", ["-->", "</script\tsrc"]));
|
|
67
|
-
const result = await runProcess(pageData, options);
|
|
68
|
-
check("a tab terminates a closer, so '</script\\t' escalates to", result.fallbackTag, "<style type=sfz-data>");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
{
|
|
72
|
-
const options = makeOptions();
|
|
73
|
-
const pageData = makePageData(6, 64 * 1024);
|
|
74
|
-
pageData.resources.images.push(storedResource("photo.jpg", ["-->", "</scriptx"]));
|
|
75
|
-
const result = await runProcess(pageData, options);
|
|
76
|
-
check("'</scriptx' is not a closer, so it stops at", result.fallbackTag, "<script type=sfz-data>");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
{
|
|
80
|
-
const options = makeOptions();
|
|
81
|
-
const pageData = makePageData(7, 64 * 1024);
|
|
82
|
-
pageData.resources.images.push(storedResource("photo.jpg", ["--!>"]));
|
|
83
|
-
const result = await runProcess(pageData, options);
|
|
84
|
-
check("'--!>' closes a comment, so it falls back to", result.fallbackTag, "<script type=sfz-data>");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
{
|
|
88
|
-
const options = makeOptions();
|
|
89
|
-
const pageData = makePageData(8, 64 * 1024);
|
|
90
|
-
pageData.resources.images.push(storedResource("photo.jpg", ["--?>"]));
|
|
91
|
-
const result = await runProcess(pageData, options);
|
|
92
|
-
check("'--?>' does not close a comment", result.fallbackTag, null);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
Deno.exit(failed ? 1 : 0);
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"contentLength": 2097152,
|
|
3
|
-
"seeds": [
|
|
4
|
-
{
|
|
5
|
-
"seed": 68,
|
|
6
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
7
|
-
"pageSize": 723106
|
|
8
|
-
},
|
|
9
|
-
{
|
|
10
|
-
"seed": 154,
|
|
11
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
12
|
-
"pageSize": 724555
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
"seed": 176,
|
|
16
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
17
|
-
"pageSize": 724734
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"seed": 184,
|
|
21
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
22
|
-
"pageSize": 724449
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
"seed": 212,
|
|
26
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
27
|
-
"pageSize": 723683
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"seed": 223,
|
|
31
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
32
|
-
"pageSize": 724277
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
"seed": 224,
|
|
36
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
37
|
-
"pageSize": 724265
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"seed": 232,
|
|
41
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
42
|
-
"pageSize": 723856
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"seed": 256,
|
|
46
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
47
|
-
"pageSize": 722938
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
"seed": 282,
|
|
51
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
52
|
-
"pageSize": 724570
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"seed": 288,
|
|
56
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
57
|
-
"pageSize": 725625
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
"seed": 343,
|
|
61
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
62
|
-
"pageSize": 724582
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
"seed": 351,
|
|
66
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
67
|
-
"pageSize": 724787
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
"seed": 372,
|
|
71
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
72
|
-
"pageSize": 723867
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
"seed": 379,
|
|
76
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
77
|
-
"pageSize": 725136
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
"seed": 383,
|
|
81
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
82
|
-
"pageSize": 725078
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
"seed": 404,
|
|
86
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
87
|
-
"pageSize": 723429
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
"seed": 426,
|
|
91
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
92
|
-
"pageSize": 724508
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
"seed": 441,
|
|
96
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
97
|
-
"pageSize": 724645
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
"seed": 445,
|
|
101
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
102
|
-
"pageSize": 724563
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
"seed": 475,
|
|
106
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
107
|
-
"pageSize": 723429
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
"seed": 495,
|
|
111
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
112
|
-
"pageSize": 723247
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
"seed": 500,
|
|
116
|
-
"fallbackTag": "<script type=sfz-data>",
|
|
117
|
-
"pageSize": 724039
|
|
118
|
-
}
|
|
119
|
-
]
|
|
120
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
// §5.7 states that the page.pdf central-record injection applies its accounting to the zip64
|
|
2
|
-
// end of central directory record when one is present, and claimed the combination "has been
|
|
3
|
-
// verified on a forced-zip64 build". No such build existed here: the writer has no zip64 lever
|
|
4
|
-
// and §8.3 described the specimen only in prose, so nothing checked the branch. It runs only past
|
|
5
|
-
// 4 GiB or 65535 entries, which a saved page never reaches, so a defect in it would be invisible.
|
|
6
|
-
//
|
|
7
|
-
// zipWriter.options is a live reference to the object the ZipWriter was constructed with, so the
|
|
8
|
-
// writeEntries callback can force zip64 on without any production change.
|
|
9
|
-
import "./dom-stub.js";
|
|
10
|
-
import { createArchive } from "../../processors/compression/compression.js";
|
|
11
|
-
import { TextReader, ZipReader, BlobReader } from "../../vendor/zip/zip.js";
|
|
12
|
-
import { FIXED_DATE, blobBytes } from "./common.js";
|
|
13
|
-
|
|
14
|
-
const ZIP64_EOCD_SIGNATURE = 0x06064b50;
|
|
15
|
-
const ZIP64_LOCATOR_SIGNATURE = 0x07064b50;
|
|
16
|
-
const EOCD_SIGNATURE = 0x06054b50;
|
|
17
|
-
|
|
18
|
-
let failed = false;
|
|
19
|
-
|
|
20
|
-
function check(label, actual, expected) {
|
|
21
|
-
const ok = actual === expected;
|
|
22
|
-
console.log(`${ok ? "PASS" : "FAIL"} ${label}: ${actual}${ok ? "" : " (expected " + expected + ")"}`);
|
|
23
|
-
failed ||= !ok;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const PDF = new TextEncoder().encode("%PDF-1.4\n1 0 obj\n<< /X (zip64) >>\nendobj\ntrailer\n<<>>\n%%EOF\n");
|
|
27
|
-
const pageData = { doctype: "<!DOCTYPE html>", content: "<html><body>zip64</body></html>", title: "zip64" };
|
|
28
|
-
const archiveOptions = { url: "https://example.com/", selfExtractingArchive: true, embeddedPdf: PDF };
|
|
29
|
-
|
|
30
|
-
const blob = await createArchive(pageData, archiveOptions, "/* zip script stub */", async zipWriter => {
|
|
31
|
-
zipWriter.options.zip64 = true;
|
|
32
|
-
await zipWriter.add("index.html", new TextReader("<html><body>zip64</body></html>"));
|
|
33
|
-
await zipWriter.add("manifest.json", new TextReader("{}"));
|
|
34
|
-
}, FIXED_DATE);
|
|
35
|
-
const bytes = await blobBytes(blob);
|
|
36
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
37
|
-
|
|
38
|
-
// locate the three records from the end, the way a reader does
|
|
39
|
-
let eocd = -1;
|
|
40
|
-
for (let index = bytes.length - 22; index >= 0 && eocd == -1; index--) {
|
|
41
|
-
if (view.getUint32(index, true) == EOCD_SIGNATURE) {
|
|
42
|
-
eocd = index;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
check("the archive ends with an EOCD record", eocd != -1, true);
|
|
46
|
-
const locator = eocd - 20;
|
|
47
|
-
check("a zip64 locator precedes the EOCD", view.getUint32(locator, true), ZIP64_LOCATOR_SIGNATURE);
|
|
48
|
-
const zip64 = Number(view.getBigUint64(locator + 8, true));
|
|
49
|
-
check("the locator points at the zip64 record", view.getUint32(zip64, true), ZIP64_EOCD_SIGNATURE);
|
|
50
|
-
|
|
51
|
-
// §5.7: the EOCD's saturated fields stay at their sentinels, and the writer saturates all of
|
|
52
|
-
// them once zip64 is emitted rather than only the one that overflowed
|
|
53
|
-
check("the EOCD entry count is a sentinel", view.getUint16(eocd + 8, true), 0xFFFF);
|
|
54
|
-
check("the EOCD total count is a sentinel", view.getUint16(eocd + 10, true), 0xFFFF);
|
|
55
|
-
check("the EOCD directory size is a sentinel", view.getUint32(eocd + 12, true), 0xFFFFFFFF);
|
|
56
|
-
check("the EOCD directory offset is a sentinel", view.getUint32(eocd + 16, true), 0xFFFFFFFF);
|
|
57
|
-
|
|
58
|
-
// the injection's accounting lands in the zip64 record: three entries, not the writer's two
|
|
59
|
-
check("the zip64 record counts the injected record on this disk", Number(view.getBigUint64(zip64 + 24, true)), 3);
|
|
60
|
-
check("the zip64 record counts it in the total", Number(view.getBigUint64(zip64 + 32, true)), 3);
|
|
61
|
-
|
|
62
|
-
// the directory begins at the injected record, which sits ahead of the writer's own directory
|
|
63
|
-
const directoryOffset = Number(view.getBigUint64(zip64 + 48, true));
|
|
64
|
-
const directorySize = Number(view.getBigUint64(zip64 + 40, true));
|
|
65
|
-
check("the zip64 directory offset points at the injected record",
|
|
66
|
-
view.getUint32(directoryOffset, true), 0x02014b50);
|
|
67
|
-
check("the first record in the directory is page.pdf",
|
|
68
|
-
new TextDecoder().decode(bytes.subarray(directoryOffset + 46, directoryOffset + 54)), "page.pdf");
|
|
69
|
-
check("the directory size covers every record", directoryOffset + directorySize, zip64);
|
|
70
|
-
|
|
71
|
-
// and the result is still an archive every reader can read
|
|
72
|
-
const reader = new ZipReader(new BlobReader(new Blob([bytes])));
|
|
73
|
-
const entries = await reader.getEntries();
|
|
74
|
-
await reader.close();
|
|
75
|
-
check("a reader lists all three entries", entries.map(entry => entry.filename).join(","), "page.pdf,index.html,manifest.json");
|
|
76
|
-
|
|
77
|
-
Deno.exit(failed ? 1 : 0);
|
package/zip-build/README.md
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
# Rebuilding `vendor/zip`
|
|
2
|
-
|
|
3
|
-
The three files in [`../vendor/zip`](../vendor/zip) are custom builds of
|
|
4
|
-
[@zip.js/zip.js](https://www.npmjs.com/package/@zip.js/zip.js) with
|
|
5
|
-
[fflate](https://www.npmjs.com/package/fflate) as `DecompressionStream`
|
|
6
|
-
fallback:
|
|
7
|
-
|
|
8
|
-
- `zip.min.js` — extraction bundle used by self-extracting pages
|
|
9
|
-
(`ZipReader`, `HttpRangeReader`, blob/text/data-URI readers and writers,
|
|
10
|
-
no web workers, fflate fallback for browsers without
|
|
11
|
-
`DecompressionStream`)
|
|
12
|
-
- `zip.js` — creation bundle used when saving pages (full zip-core plus
|
|
13
|
-
`deflateRaw`/`inflateRaw`)
|
|
14
|
-
- `z-worker.js` — web worker script for the creation bundle
|
|
15
|
-
|
|
16
|
-
This directory rebuilds them byte-for-byte from their sources. All
|
|
17
|
-
dependencies are pinned to exact versions by `package.json` and
|
|
18
|
-
`package-lock.json`, and the build embeds no timestamps, so the output is
|
|
19
|
-
reproducible on any platform with a recent Node.js version:
|
|
20
|
-
|
|
21
|
-
```sh
|
|
22
|
-
cd zip-build
|
|
23
|
-
npm ci
|
|
24
|
-
npm run build
|
|
25
|
-
git diff --exit-code ../vendor/zip
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
An empty diff means the checked-in files match the sources exactly. The
|
|
29
|
-
GitHub Actions workflow `.github/workflows/vendor.yml` runs the same steps
|
|
30
|
-
on every push.
|
|
31
|
-
|
|
32
|
-
## Files
|
|
33
|
-
|
|
34
|
-
- `lib/zip.js`, `lib/zip-vendor.js`, `lib/zip-vendor-worker.js` — bundle
|
|
35
|
-
entry points; they import the zip.js sources from the pinned npm package
|
|
36
|
-
- `lib/fflate.js` — vendored [fflate](https://github.com/101arrowz/fflate)
|
|
37
|
-
0.8.3 (MIT)
|
|
38
|
-
- `lib/fflate-streams.js` — `CompressionStream`/`DecompressionStream`
|
|
39
|
-
adapter over fflate
|
|
40
|
-
- `reserved-property-names.json` — property names protected from terser
|
|
41
|
-
property mangling; generated by `reserved-property-names.js` in the
|
|
42
|
-
[zip.js repository](https://github.com/gildas-lormeau/zip.js) at the
|
|
43
|
-
pinned version (the script is not part of the npm package)
|
|
44
|
-
|
|
45
|
-
## Updating zip.js
|
|
46
|
-
|
|
47
|
-
1. Bump `@zip.js/zip.js` in `package.json` and run `npm install`.
|
|
48
|
-
2. Regenerate `reserved-property-names.json` from a checkout of the zip.js
|
|
49
|
-
repository at the matching tag, with its dev dependencies installed:
|
|
50
|
-
|
|
51
|
-
```sh
|
|
52
|
-
node --input-type=module -e "import { getReservedPropertyNames } from './reserved-property-names.js'; console.log(JSON.stringify([...getReservedPropertyNames()], null, '\t'))" > path/to/single-file-core/zip-build/reserved-property-names.json
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
3. Run `npm run build` and commit the regenerated files in `../vendor/zip`
|
|
56
|
-
together with the changes in this directory.
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/* global TransformStream */
|
|
2
|
-
|
|
3
|
-
import { Deflate, Inflate } from "./fflate.js";
|
|
4
|
-
|
|
5
|
-
const FORMAT_DEFLATE_RAW = "deflate-raw";
|
|
6
|
-
|
|
7
|
-
class FflateStream extends TransformStream {
|
|
8
|
-
constructor(codec) {
|
|
9
|
-
super({
|
|
10
|
-
start(controller) {
|
|
11
|
-
codec.ondata = chunk => {
|
|
12
|
-
if (chunk.length) {
|
|
13
|
-
controller.enqueue(chunk);
|
|
14
|
-
}
|
|
15
|
-
};
|
|
16
|
-
},
|
|
17
|
-
transform(chunk) {
|
|
18
|
-
codec.push(chunk);
|
|
19
|
-
},
|
|
20
|
-
flush() {
|
|
21
|
-
codec.push(new Uint8Array(0), true);
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
class CompressionStreamFallback extends FflateStream {
|
|
28
|
-
constructor(format, { level } = {}) {
|
|
29
|
-
checkFormat(format);
|
|
30
|
-
super(new Deflate(level === undefined ? {} : { level }));
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
class DecompressionStreamFallback extends FflateStream {
|
|
35
|
-
constructor(format) {
|
|
36
|
-
checkFormat(format);
|
|
37
|
-
super(new Inflate());
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function checkFormat(format) {
|
|
42
|
-
if (format != FORMAT_DEFLATE_RAW) {
|
|
43
|
-
throw new TypeError("Unsupported compression format: " + format);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export { CompressionStreamFallback, DecompressionStreamFallback };
|