single-file-core 1.5.125 → 1.5.127
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/index.js +5 -0
- package/core/lib/processor-helper-common.js +16 -12
- package/core/util.js +3 -8
- package/doc/singlefile-archive.md +90 -35
- package/modules/html-images-alt-minifier.js +4 -4
- package/package.json +2 -2
- package/processors/compression/compression-packager.js +3 -7
- package/processors/compression/compression.js +111 -52
- package/processors/hooks/content/content-hooks-frames-web.js +40 -30
- package/test/sfz-harness/font-face-composite.js +135 -0
- package/test/sfz-harness/format-rules.js +74 -8
- package/test/sfz-harness/pages-archive.js +79 -1
- package/test/sfz-harness/stored-trigger.js +46 -1
- package/test/sfz-harness/trigger-seeds.json +46 -46
- package/vendor/zip/z-worker.js +1 -1
- package/vendor/zip/zip.js +649 -165
- package/vendor/zip/zip.min.js +1 -1
- package/zip-build/package-lock.json +4 -4
- package/zip-build/package.json +1 -1
- package/zip-build/reserved-property-names.json +23 -2
|
@@ -40,9 +40,7 @@ import {
|
|
|
40
40
|
router
|
|
41
41
|
} from "./compression-router.js";
|
|
42
42
|
|
|
43
|
-
const { Blob, fetch, TextEncoder,
|
|
44
|
-
|
|
45
|
-
const TEXT_DECODER = new TextDecoder("windows-1252");
|
|
43
|
+
const { Blob, fetch, TextEncoder, DOMParser } = globalThis;
|
|
46
44
|
|
|
47
45
|
const COMPRESSIBLE_CONTENT_TYPES = ["application/javascript", "application/x-javascript", "application/ecmascript", "application/json", "application/ld+json", "application/manifest+json", "application/xml", "application/xhtml+xml", "application/rss+xml", "application/atom+xml", "image/svg+xml"];
|
|
48
46
|
const TEXT_CONTENT_TYPE_PREFIX = "text/";
|
|
@@ -63,19 +61,20 @@ const EMBEDDED_DATA_TAGS = [
|
|
|
63
61
|
...EXTRA_DATA_TAGS,
|
|
64
62
|
];
|
|
65
63
|
const DATA_IDENTIFIER = "sfz-data";
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
[
|
|
69
|
-
[
|
|
70
|
-
[
|
|
71
|
-
[
|
|
72
|
-
[
|
|
73
|
-
[
|
|
74
|
-
[
|
|
64
|
+
const TAG_NAME_TERMINATORS = "\t\n\f\r />";
|
|
65
|
+
const EXTRA_DATA_PATTERNS = [
|
|
66
|
+
[["<script"], ["</script", TAG_NAME_TERMINATORS]],
|
|
67
|
+
[["<style"], ["</style", TAG_NAME_TERMINATORS]],
|
|
68
|
+
[["<noframes"], ["</noframes", TAG_NAME_TERMINATORS]],
|
|
69
|
+
[["<noembed"], ["</noembed", TAG_NAME_TERMINATORS]],
|
|
70
|
+
[["<iframe"], ["</iframe", TAG_NAME_TERMINATORS]],
|
|
71
|
+
[["<xmp"], ["</xmp", TAG_NAME_TERMINATORS]],
|
|
72
|
+
[["<![CDATA["], ["]]>"]],
|
|
73
|
+
[["<plaintext"], ["</plaintext", TAG_NAME_TERMINATORS]]
|
|
75
74
|
];
|
|
76
|
-
const
|
|
77
|
-
[
|
|
78
|
-
...
|
|
75
|
+
const EMBEDDED_DATA_PATTERNS = [
|
|
76
|
+
[["<!--"], ["-->"], ["--!>"], ["<!-", undefined, true]],
|
|
77
|
+
...EXTRA_DATA_PATTERNS,
|
|
79
78
|
];
|
|
80
79
|
const CRC32_TABLE = new Uint32Array(256).map((_, indexTable) => {
|
|
81
80
|
let crc = indexTable;
|
|
@@ -89,7 +88,8 @@ const PNG_CHUNK_CRC_LENGTH = 4;
|
|
|
89
88
|
const PNG_SIGNATURE_LENGTH = 8;
|
|
90
89
|
const PNG_IHDR_LENGTH = 25;
|
|
91
90
|
const COMMENT_LENGTH_FIELD_LENGTH = 2;
|
|
92
|
-
const
|
|
91
|
+
const MAX_ZIP_COMMENT_LENGTH = 65535;
|
|
92
|
+
const DEFAULT_MAX_APPENDED_DATA_LENGTH = 16361;
|
|
93
93
|
const PDF_ENTRY_FILENAME = "page.pdf";
|
|
94
94
|
const PRESCAN_WINDOW_LENGTH = 1024;
|
|
95
95
|
const PNG_TEXT_CHUNK_HEADER_LENGTH = 12;
|
|
@@ -105,7 +105,6 @@ const CENTRAL_FILE_HEADER_SIGNATURE = 0x02014b50;
|
|
|
105
105
|
const END_OF_CENTRAL_DIR_SIGNATURE = 0x06054b50;
|
|
106
106
|
const ZIP64_END_OF_CENTRAL_DIR_SIGNATURE = 0x06064b50;
|
|
107
107
|
const ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE = 0x07064b50;
|
|
108
|
-
const LANGUAGE_ENCODING_FLAG = 0x0800;
|
|
109
108
|
|
|
110
109
|
const browser = globalThis.browser;
|
|
111
110
|
|
|
@@ -121,6 +120,7 @@ const PROCESS_OPTION_NAMES = [
|
|
|
121
120
|
"insertMetaCSP",
|
|
122
121
|
"insertMetaNoIndex",
|
|
123
122
|
"insertTextBody",
|
|
123
|
+
"maxAppendedDataLength",
|
|
124
124
|
"password",
|
|
125
125
|
"preventAppendedData",
|
|
126
126
|
"selfExtractingArchive",
|
|
@@ -227,18 +227,14 @@ async function buildArchive(pageData, options, script, entriesData, zipWriterOpt
|
|
|
227
227
|
const lfCodes = [];
|
|
228
228
|
let crc32 = -1;
|
|
229
229
|
if (!options.extractDataFromPageTags || options.extractDataFromPageTags[0] != "<plaintext>") {
|
|
230
|
-
const
|
|
230
|
+
const zipData = data.subarray(startOffset);
|
|
231
231
|
if (options.extractDataFromPageTags) {
|
|
232
232
|
const tagIndex = getExtraDataTagIndex(options.extractDataFromPageTags);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return findExtraDataTags(textContent, pageData, options, script, entriesData, zipWriterOptions, tagIndex + 1);
|
|
236
|
-
}
|
|
237
|
-
} else {
|
|
238
|
-
const [startRegExp, endRegExp] = EMBEDDED_DATA_REGEXPS[0];
|
|
239
|
-
if (textContent.match(startRegExp) || textContent.match(endRegExp)) {
|
|
240
|
-
return findExtraDataTags(textContent, pageData, options, script, entriesData, zipWriterOptions);
|
|
233
|
+
if (containsDataPattern(zipData, EXTRA_DATA_PATTERNS[tagIndex])) {
|
|
234
|
+
return findExtraDataTags(zipData, pageData, options, script, entriesData, zipWriterOptions, tagIndex + 1);
|
|
241
235
|
}
|
|
236
|
+
} else if (containsDataPattern(zipData, EMBEDDED_DATA_PATTERNS[0])) {
|
|
237
|
+
return findExtraDataTags(zipData, pageData, options, script, entriesData, zipWriterOptions);
|
|
242
238
|
}
|
|
243
239
|
}
|
|
244
240
|
if (options.extractDataFromPage) {
|
|
@@ -278,7 +274,7 @@ async function buildArchive(pageData, options, script, entriesData, zipWriterOpt
|
|
|
278
274
|
const payloadView = new DataView(payload.buffer);
|
|
279
275
|
words.forEach((word, indexWord) => payloadView.setUint32(indexWord * 4, word, true));
|
|
280
276
|
extraData = "<sfz-extra-data>" + base64Encode(deflateRaw(payload)) + "</sfz-extra-data>";
|
|
281
|
-
if (options.preventAppendedData || extraData.length >
|
|
277
|
+
if (options.preventAppendedData || extraData.length > getMaxAppendedDataLength(options) - pageContent.length - endTags.length - (options.embeddedImage ? PNG_IEND_LENGTH + PNG_CHUNK_CRC_LENGTH : 0)) {
|
|
282
278
|
if (!options.extraDataSize) {
|
|
283
279
|
options.preventAppendedData = true;
|
|
284
280
|
options.extraDataSize = getReservationSize(extraData.length);
|
|
@@ -304,7 +300,7 @@ async function buildArchive(pageData, options, script, entriesData, zipWriterOpt
|
|
|
304
300
|
if (options.declareAppendedData) {
|
|
305
301
|
const appendedDataLength = pageContent.length - data.length +
|
|
306
302
|
(options.embeddedImage ? PNG_CHUNK_CRC_LENGTH + PNG_IEND_LENGTH : 0);
|
|
307
|
-
if (appendedDataLength && appendedDataLength <=
|
|
303
|
+
if (appendedDataLength && appendedDataLength <= MAX_ZIP_COMMENT_LENGTH && isDeclaredLengthHidden(pageContent, zipDataEnd, appendedDataLength, options)) {
|
|
308
304
|
new DataView(pageContent.buffer, pageContent.byteOffset).setUint16(zipDataEnd, appendedDataLength, true);
|
|
309
305
|
}
|
|
310
306
|
}
|
|
@@ -324,6 +320,10 @@ async function buildArchive(pageData, options, script, entriesData, zipWriterOpt
|
|
|
324
320
|
}
|
|
325
321
|
}
|
|
326
322
|
|
|
323
|
+
function getMaxAppendedDataLength(options) {
|
|
324
|
+
return options.maxAppendedDataLength === undefined ? DEFAULT_MAX_APPENDED_DATA_LENGTH : options.maxAppendedDataLength;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
327
|
function isDeclaredLengthHidden(pageContent, zipDataEnd, appendedDataLength, options) {
|
|
328
328
|
if (options.extractDataFromPageTags && options.extractDataFromPageTags[0] == "<plaintext>") {
|
|
329
329
|
return true;
|
|
@@ -331,9 +331,7 @@ function isDeclaredLengthHidden(pageContent, zipDataEnd, appendedDataLength, opt
|
|
|
331
331
|
const tail = pageContent.slice(zipDataEnd - WRAPPER_PATTERN_WINDOW_LENGTH, zipDataEnd + COMMENT_LENGTH_FIELD_LENGTH);
|
|
332
332
|
new DataView(tail.buffer).setUint16(WRAPPER_PATTERN_WINDOW_LENGTH, appendedDataLength, true);
|
|
333
333
|
const tagIndex = options.extractDataFromPageTags ? getExtraDataTagIndex(options.extractDataFromPageTags) + 1 : 0;
|
|
334
|
-
|
|
335
|
-
const tailText = TEXT_DECODER.decode(tail);
|
|
336
|
-
return !tailText.match(startRegExp) && !tailText.match(endRegExp);
|
|
334
|
+
return !containsDataPattern(tail, EMBEDDED_DATA_PATTERNS[tagIndex]);
|
|
337
335
|
}
|
|
338
336
|
|
|
339
337
|
function getCRC32(data, indexData = 0) {
|
|
@@ -359,7 +357,6 @@ function getPDFEntry(embeddedPdf, lastModDate = new Date()) {
|
|
|
359
357
|
const localHeaderView = new DataView(localHeader.buffer);
|
|
360
358
|
localHeaderView.setUint32(0, LOCAL_FILE_HEADER_SIGNATURE, true);
|
|
361
359
|
localHeaderView.setUint16(4, 20, true);
|
|
362
|
-
localHeaderView.setUint16(6, LANGUAGE_ENCODING_FLAG, true);
|
|
363
360
|
localHeaderView.setUint16(10, dosTime, true);
|
|
364
361
|
localHeaderView.setUint16(12, dosDate, true);
|
|
365
362
|
localHeaderView.setUint32(14, crc32, true);
|
|
@@ -372,7 +369,6 @@ function getPDFEntry(embeddedPdf, lastModDate = new Date()) {
|
|
|
372
369
|
centralRecordView.setUint32(0, CENTRAL_FILE_HEADER_SIGNATURE, true);
|
|
373
370
|
centralRecordView.setUint16(4, 0x0300, true);
|
|
374
371
|
centralRecordView.setUint16(6, 20, true);
|
|
375
|
-
centralRecordView.setUint16(8, LANGUAGE_ENCODING_FLAG, true);
|
|
376
372
|
centralRecordView.setUint16(12, dosTime, true);
|
|
377
373
|
centralRecordView.setUint16(14, dosDate, true);
|
|
378
374
|
centralRecordView.setUint32(16, crc32, true);
|
|
@@ -546,8 +542,7 @@ function getStartHTMLArray(pageData, options, lastModDate, startTag = "") {
|
|
|
546
542
|
const embeddedPdf = new Uint8Array(options.embeddedPdf);
|
|
547
543
|
pdfEntry = options.preventEmbeddedPdfEntry ? undefined : getPDFEntry(embeddedPdf, lastModDate);
|
|
548
544
|
const localHeader = pdfEntry ? pdfEntry.localHeader : new Uint8Array(0);
|
|
549
|
-
const
|
|
550
|
-
const pdfTagIndex = findEmbeddedDataTagIndex(embeddedPdfText);
|
|
545
|
+
const pdfTagIndex = findEmbeddedDataTagIndex(concatArrays(localHeader, embeddedPdf));
|
|
551
546
|
if (pdfTagIndex == -1) {
|
|
552
547
|
dropUnhiddenFace(options, "embeddedPdf", EMBEDDED_PDF_LABEL);
|
|
553
548
|
pdfEntry = undefined;
|
|
@@ -623,12 +618,11 @@ function getExtraDataTagIndex(extractDataFromPageTags) {
|
|
|
623
618
|
return tagIndex;
|
|
624
619
|
}
|
|
625
620
|
|
|
626
|
-
function findExtraDataTags(
|
|
627
|
-
const regExpsTag = EXTRA_DATA_REGEXPS[indexExtractDataFromPageTags];
|
|
621
|
+
function findExtraDataTags(zipData, pageData, options, script, entriesData, zipWriterOptions, indexExtractDataFromPageTags = 0) {
|
|
628
622
|
const plaintextTag = EXTRA_DATA_TAGS[indexExtractDataFromPageTags][0] == "<plaintext>";
|
|
629
|
-
const matchTag = !plaintextTag && (
|
|
623
|
+
const matchTag = !plaintextTag && containsDataPattern(zipData, EXTRA_DATA_PATTERNS[indexExtractDataFromPageTags]);
|
|
630
624
|
if (matchTag) {
|
|
631
|
-
return findExtraDataTags(
|
|
625
|
+
return findExtraDataTags(zipData, pageData, options, script, entriesData, zipWriterOptions, indexExtractDataFromPageTags + 1);
|
|
632
626
|
} else {
|
|
633
627
|
options.extractDataFromPageTags = EXTRA_DATA_TAGS[indexExtractDataFromPageTags];
|
|
634
628
|
if (options.extractDataFromPageTags[0] == "<plaintext>") {
|
|
@@ -638,24 +632,93 @@ function findExtraDataTags(textContent, pageData, options, script, entriesData,
|
|
|
638
632
|
}
|
|
639
633
|
}
|
|
640
634
|
|
|
641
|
-
function findEmbeddedDataTagIndex(
|
|
642
|
-
const tagIndex =
|
|
635
|
+
function findEmbeddedDataTagIndex(data, fromIndex = 0) {
|
|
636
|
+
const tagIndex = EMBEDDED_DATA_PATTERNS.slice(fromIndex, -1).findIndex(patterns => !containsDataPattern(data, patterns));
|
|
643
637
|
return tagIndex == -1 ? -1 : tagIndex + fromIndex;
|
|
644
638
|
}
|
|
645
639
|
|
|
640
|
+
function containsDataPattern(data, patterns) {
|
|
641
|
+
const patternsByCharCode = new Map();
|
|
642
|
+
for (const pattern of patterns) {
|
|
643
|
+
const [text, , atEnd] = pattern;
|
|
644
|
+
if (atEnd) {
|
|
645
|
+
if (matchesDataPatternAt(data, pattern, data.length - text.length)) {
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
} else {
|
|
649
|
+
const charCode = text.charCodeAt(0);
|
|
650
|
+
indexPatternByCharCode(patternsByCharCode, charCode, pattern);
|
|
651
|
+
const alternateCharCode = getAlternateCharCode(charCode);
|
|
652
|
+
if (alternateCharCode != -1) {
|
|
653
|
+
indexPatternByCharCode(patternsByCharCode, alternateCharCode, pattern);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
for (const [charCode, candidates] of patternsByCharCode) {
|
|
658
|
+
for (let index = data.indexOf(charCode); index != -1; index = data.indexOf(charCode, index + 1)) {
|
|
659
|
+
for (let indexCandidate = 0; indexCandidate < candidates.length; indexCandidate++) {
|
|
660
|
+
if (matchesDataPatternAt(data, candidates[indexCandidate], index)) {
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function indexPatternByCharCode(patternsByCharCode, charCode, pattern) {
|
|
670
|
+
if (!patternsByCharCode.has(charCode)) {
|
|
671
|
+
patternsByCharCode.set(charCode, []);
|
|
672
|
+
}
|
|
673
|
+
patternsByCharCode.get(charCode).push(pattern);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function matchesDataPatternAt(data, [text, terminators], index) {
|
|
677
|
+
const textLength = text.length;
|
|
678
|
+
if (index < 0 || index + textLength > data.length) {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
for (let indexText = 0; indexText < textLength; indexText++) {
|
|
682
|
+
const charCode = text.charCodeAt(indexText);
|
|
683
|
+
const code = data[index + indexText];
|
|
684
|
+
if (code != charCode && code != getAlternateCharCode(charCode)) {
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return terminators === undefined || isTerminatorCode(terminators, data[index + textLength]);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function isTerminatorCode(terminators, code) {
|
|
692
|
+
return code !== undefined && terminators.includes(String.fromCharCode(code));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function getAlternateCharCode(charCode) {
|
|
696
|
+
const lowerCharCode = charCode | 0x20;
|
|
697
|
+
return lowerCharCode >= 0x61 && lowerCharCode <= 0x7a ? charCode ^ 0x20 : -1;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function concatArrays(...arrays) {
|
|
701
|
+
const result = new Uint8Array(arrays.reduce((length, array) => length + array.length, 0));
|
|
702
|
+
let offset = 0;
|
|
703
|
+
arrays.forEach(array => {
|
|
704
|
+
result.set(array, offset);
|
|
705
|
+
offset += array.length;
|
|
706
|
+
});
|
|
707
|
+
return result;
|
|
708
|
+
}
|
|
709
|
+
|
|
646
710
|
function getImageHTMLChunk(pageData, options, lastModDate) {
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
let tagIndex = findEmbeddedDataTagIndex(embeddedImageText);
|
|
711
|
+
const embeddedImageData = concatArrays(getEmbeddedImageData(options.embeddedImage), new Uint8Array(4), PNG_ZIP_CHUNK_TYPE_KEYWORD);
|
|
712
|
+
let tagIndex = findEmbeddedDataTagIndex(embeddedImageData);
|
|
650
713
|
while (tagIndex != -1) {
|
|
651
714
|
const [startTag, endTag] = EMBEDDED_DATA_TAGS[tagIndex];
|
|
652
715
|
const startHTMLData = getStartHTMLArray(pageData, options, lastModDate, startTag);
|
|
653
716
|
const htmlData = new Uint8Array([...getLength(startHTMLData.htmlArray.length + 4), ...[0x74, 0x45, 0x58, 0x74, 0x50, 0x4e, 0x47, 0], ...startHTMLData.htmlArray]);
|
|
654
717
|
const htmlDataCRC = getCRC32(htmlData, 4);
|
|
655
|
-
const
|
|
718
|
+
const wrappedData = concatArrays(htmlDataCRC, embeddedImageData);
|
|
656
719
|
if ((tagIndex == 0 && (htmlDataCRC[0] == 0x3e || (htmlDataCRC[0] == 0x2d && htmlDataCRC[1] == 0x3e))) ||
|
|
657
|
-
findEmbeddedDataTagIndex(
|
|
658
|
-
tagIndex = findEmbeddedDataTagIndex(
|
|
720
|
+
findEmbeddedDataTagIndex(wrappedData, tagIndex) != tagIndex) {
|
|
721
|
+
tagIndex = findEmbeddedDataTagIndex(embeddedImageData, tagIndex + 1);
|
|
659
722
|
} else {
|
|
660
723
|
return { endTag, startHTMLData, htmlData, htmlDataCRC };
|
|
661
724
|
}
|
|
@@ -747,11 +810,7 @@ async function getContent() {
|
|
|
747
810
|
});
|
|
748
811
|
return new Promise((resolve, reject) => {
|
|
749
812
|
let aborted = false;
|
|
750
|
-
|
|
751
|
-
extractDataFromDocument();
|
|
752
|
-
} else {
|
|
753
|
-
getPageData();
|
|
754
|
-
}
|
|
813
|
+
getPageData();
|
|
755
814
|
|
|
756
815
|
async function extractDataFromDocument() {
|
|
757
816
|
try {
|
|
@@ -242,39 +242,46 @@
|
|
|
242
242
|
}
|
|
243
243
|
return boundingRect;
|
|
244
244
|
};
|
|
245
|
+
Element.prototype.getBoundingClientRect.toString = function () { return "function getBoundingClientRect() { [native code] }"; };
|
|
246
|
+
setFunctionName(Element.prototype.getBoundingClientRect, "getBoundingClientRect");
|
|
245
247
|
}
|
|
246
248
|
}
|
|
247
249
|
if (!globalThis._singleFileImage) {
|
|
248
|
-
const
|
|
249
|
-
globalThis._singleFileImage =
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
};
|
|
275
|
-
|
|
250
|
+
const NativeImage = globalThis.Image;
|
|
251
|
+
globalThis._singleFileImage = NativeImage;
|
|
252
|
+
const ImageWrapper = function Image() {
|
|
253
|
+
const image = new NativeImage(...arguments);
|
|
254
|
+
const result = new NativeImage(...arguments);
|
|
255
|
+
result.__defineSetter__("src", value => {
|
|
256
|
+
image.src = value;
|
|
257
|
+
document.dispatchEvent(new CustomEvent(LOAD_IMAGE_EVENT, { detail: image.src }));
|
|
258
|
+
});
|
|
259
|
+
result.__defineGetter__("src", () => image.src);
|
|
260
|
+
result.__defineSetter__("srcset", value => {
|
|
261
|
+
document.dispatchEvent(new CustomEvent(LOAD_IMAGE_EVENT));
|
|
262
|
+
image.srcset = value;
|
|
263
|
+
});
|
|
264
|
+
result.__defineGetter__("srcset", () => image.srcset);
|
|
265
|
+
result.__defineGetter__("height", () => image.height);
|
|
266
|
+
result.__defineGetter__("width", () => image.width);
|
|
267
|
+
result.__defineGetter__("naturalHeight", () => image.naturalHeight);
|
|
268
|
+
result.__defineGetter__("naturalWidth", () => image.naturalWidth);
|
|
269
|
+
if (image.decode) {
|
|
270
|
+
const decode = function decode() { return image.decode(); };
|
|
271
|
+
decode.toString = function () { return "function decode() { [native code] }"; };
|
|
272
|
+
setFunctionName(decode, "decode");
|
|
273
|
+
result.__defineGetter__("decode", () => decode);
|
|
274
|
+
}
|
|
275
|
+
image.onload = image.onloadend = image.onerror = event => {
|
|
276
|
+
document.dispatchEvent(new CustomEvent(IMAGE_LOADED_EVENT, { detail: image.src }));
|
|
277
|
+
result.dispatchEvent(new Event(event.type, event));
|
|
276
278
|
};
|
|
277
|
-
|
|
279
|
+
return result;
|
|
280
|
+
};
|
|
281
|
+
ImageWrapper.prototype = NativeImage.prototype;
|
|
282
|
+
ImageWrapper.toString = function () { return "function Image() { [native code] }"; };
|
|
283
|
+
setFunctionName(ImageWrapper, "Image");
|
|
284
|
+
globalThis.__defineGetter__("Image", () => ImageWrapper);
|
|
278
285
|
}
|
|
279
286
|
const verticalZoomFactor = clientHeight / scrollHeight;
|
|
280
287
|
const horizontalZoomFactor = clientWidth / scrollWidth;
|
|
@@ -371,9 +378,10 @@
|
|
|
371
378
|
|
|
372
379
|
if (globalThis.CSS && globalThis.CSS.paintWorklet && globalThis.CSS.paintWorklet.addModule) {
|
|
373
380
|
const addModule = globalThis.CSS.paintWorklet.addModule;
|
|
374
|
-
globalThis.CSS.paintWorklet.addModule = function (moduleURL
|
|
381
|
+
globalThis.CSS.paintWorklet.addModule = function (moduleURL) {
|
|
375
382
|
try {
|
|
376
383
|
const result = addModule.apply(globalThis.CSS.paintWorklet, arguments);
|
|
384
|
+
const options = arguments[1];
|
|
377
385
|
moduleURL = new URL(moduleURL, document.baseURI).href;
|
|
378
386
|
document.dispatchEvent(new CustomEvent(NEW_WORKLET_EVENT, { detail: { moduleURL, options } }));
|
|
379
387
|
return result;
|
|
@@ -382,6 +390,8 @@
|
|
|
382
390
|
throw error;
|
|
383
391
|
}
|
|
384
392
|
};
|
|
393
|
+
globalThis.CSS.paintWorklet.addModule.toString = function () { return "function addModule() { [native code] }"; };
|
|
394
|
+
setFunctionName(globalThis.CSS.paintWorklet.addModule, "addModule");
|
|
385
395
|
}
|
|
386
396
|
|
|
387
397
|
if (globalThis.FontFace) {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Several @font-face rules that declare the same family with the same style descriptors are one
|
|
2
|
+
// composite face, not a stack where the last rule wins. CSS Fonts 4 §5.2: "When the matched face is
|
|
3
|
+
// a composite face, user agents must use the procedure above on each of the faces in the composite
|
|
4
|
+
// face in reverse order of @font-face rule definition", selecting a face only "if the effective
|
|
5
|
+
// character map supports the character in question". §4.5.1 says the same for the case where the
|
|
6
|
+
// ranges are identical rather than merely overlapping: "If the unicode ranges overlap for a set of
|
|
7
|
+
// @font-face rules with the same family and style descriptor values, the rules are ordered in the
|
|
8
|
+
// reverse order they were defined; the last rule defined is the first to be checked for a given
|
|
9
|
+
// character." So the later rule wins per character, and a character it has no glyph for falls back
|
|
10
|
+
// to the earlier rule of the same family.
|
|
11
|
+
//
|
|
12
|
+
// Dropping the earlier rule therefore loses glyphs. science.org declares icomoon twice, weight 400
|
|
13
|
+
// style normal in both, first a 103-codepoint icon set and then a 30-codepoint one holding only the
|
|
14
|
+
// slideshow arrows. The banner close button is content:"\e928", which lives in the first font only,
|
|
15
|
+
// so keeping the last rule alone rendered a tofu box reading "E9 28" where the X had been.
|
|
16
|
+
//
|
|
17
|
+
// Pooling the sources of every rule sharing a key breaks it the same way even when both rules are
|
|
18
|
+
// kept: one winning source gets written into all of them, so both rules end up naming the same font
|
|
19
|
+
// and the other one is gone just as surely. Each rule keeps its own sources.
|
|
20
|
+
//
|
|
21
|
+
// Rules that are duplicates outright, same key and same src, are still emitted once: they are the
|
|
22
|
+
// same member of the composite declared twice, so dropping one changes nothing.
|
|
23
|
+
import * as cssTree from "../../vendor/css-tree.js";
|
|
24
|
+
|
|
25
|
+
// helper.js reaches the frame hooks, which install themselves against window and document as they
|
|
26
|
+
// are evaluated: the stubs go in before the dynamic import
|
|
27
|
+
globalThis.window = globalThis;
|
|
28
|
+
globalThis.document = {};
|
|
29
|
+
globalThis.Document = class Document { };
|
|
30
|
+
globalThis.MutationObserver = class MutationObserver { observe() { } };
|
|
31
|
+
const { getProcessorHelperCommonClass } = await import("../../core/lib/processor-helper-common.js");
|
|
32
|
+
|
|
33
|
+
const ProcessorHelperCommon = getProcessorHelperCommonClass({}, cssTree);
|
|
34
|
+
|
|
35
|
+
// the real subclasses pick one source out of the list and rewrite the rule with it; the contract
|
|
36
|
+
// under test is which rules survive and which sources each one is handed, so this records that and
|
|
37
|
+
// keeps every rule
|
|
38
|
+
class TestProcessorHelper extends ProcessorHelperCommon {
|
|
39
|
+
constructor() {
|
|
40
|
+
super();
|
|
41
|
+
this.processedRules = [];
|
|
42
|
+
}
|
|
43
|
+
async processFontFaceRule(ruleData, fontInfo) {
|
|
44
|
+
this.processedRules.push({
|
|
45
|
+
family: this.getPropertyValue(ruleData, "font-family"),
|
|
46
|
+
sources: fontInfo.map(source => source.src)
|
|
47
|
+
});
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function run(css) {
|
|
53
|
+
const helper = new TestProcessorHelper();
|
|
54
|
+
const stylesheets = new Map([[0, { stylesheet: cssTree.parse(css) }]]);
|
|
55
|
+
await helper.removeAlternativeFonts({}, stylesheets, new Map(), new Map());
|
|
56
|
+
const remaining = [];
|
|
57
|
+
stylesheets.get(0).stylesheet.children.forEach(ruleData => {
|
|
58
|
+
if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
|
|
59
|
+
remaining.push(helper.getPropertyValue(ruleData, "src"));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return { processed: helper.processedRules, remaining };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let failures = 0;
|
|
66
|
+
|
|
67
|
+
// a rule that was dropped when it should have been kept leaves a hole in the list, and reporting
|
|
68
|
+
// that as a failed comparison is more use than throwing on the way to the assertion
|
|
69
|
+
function sourcesOf(processedRules, index) {
|
|
70
|
+
const rule = processedRules[index];
|
|
71
|
+
return rule ? rule.sources : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function check(label, actual, expected) {
|
|
75
|
+
const pass = JSON.stringify(actual) == JSON.stringify(expected);
|
|
76
|
+
console.log((pass ? "PASS " : "FAIL ") + label + ": " + JSON.stringify(actual));
|
|
77
|
+
if (!pass) {
|
|
78
|
+
console.log(" expected: " + JSON.stringify(expected));
|
|
79
|
+
failures++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const TWO_FACES = `
|
|
84
|
+
@font-face{font-family:icomoon;src:url(big.ttf) format("truetype"),url(big.woff) format("woff");font-weight:400;font-style:normal}
|
|
85
|
+
@font-face{font-family:icomoon;src:url(small.ttf) format("truetype"),url(small.woff) format("woff");font-weight:400;font-style:normal}`;
|
|
86
|
+
|
|
87
|
+
const two = await run(TWO_FACES);
|
|
88
|
+
check("both members of the composite face are kept", two.processed.length, 2);
|
|
89
|
+
check("the earlier rule keeps its own sources", sourcesOf(two.processed, 0).sort(), ["url(big.ttf)format(\"truetype\")", "url(big.woff)format(\"woff\")"]);
|
|
90
|
+
check("the later rule keeps its own sources", (sourcesOf(two.processed, 1) || []).sort(), ["url(small.ttf)format(\"truetype\")", "url(small.woff)format(\"woff\")"]);
|
|
91
|
+
check("neither rule is removed from the stylesheet", two.remaining.length, 2);
|
|
92
|
+
|
|
93
|
+
const DUPLICATE = `
|
|
94
|
+
@font-face{font-family:icomoon;src:url(one.woff) format("woff");font-weight:400;font-style:normal}
|
|
95
|
+
@font-face{font-family:icomoon;src:url(one.woff) format("woff");font-weight:400;font-style:normal}`;
|
|
96
|
+
|
|
97
|
+
const duplicate = await run(DUPLICATE);
|
|
98
|
+
check("an outright duplicate rule is emitted once", duplicate.processed.length, 1);
|
|
99
|
+
check("the duplicate is removed from the stylesheet", duplicate.remaining.length, 1);
|
|
100
|
+
|
|
101
|
+
// unicode-range is part of the font key, so the subsetting idiom was never affected by the
|
|
102
|
+
// shadowing bug; it is pinned here because the fix moved what the key is used for
|
|
103
|
+
const RANGES = `
|
|
104
|
+
@font-face{font-family:sub;src:url(latin.woff2) format("woff2");unicode-range:U+0-7F}
|
|
105
|
+
@font-face{font-family:sub;src:url(greek.woff2) format("woff2");unicode-range:U+370-3FF}`;
|
|
106
|
+
|
|
107
|
+
const ranges = await run(RANGES);
|
|
108
|
+
check("faces split by unicode-range are all kept", ranges.processed.length, 2);
|
|
109
|
+
check("each range keeps its own source", ranges.processed.map(rule => rule.sources), [["url(latin.woff2)format(\"woff2\")"], ["url(greek.woff2)format(\"woff2\")"]]);
|
|
110
|
+
|
|
111
|
+
// a rule declaring the same source twice contributes it once, in the position its later declaration
|
|
112
|
+
// gives it. The sources are compared after the separating comma is stripped, so a repeat is
|
|
113
|
+
// recognised wherever it sits: the value is split by a regexp that keeps that comma, and comparing
|
|
114
|
+
// the raw pieces made the last source in a list unequal to the same source anywhere before it
|
|
115
|
+
const REPEATED = `
|
|
116
|
+
@font-face{font-family:repeat;src:url(a.woff) format("woff"),url(b.woff) format("woff"),url(a.woff) format("woff"),url(c.woff) format("woff");font-weight:400}`;
|
|
117
|
+
|
|
118
|
+
const repeated = await run(REPEATED);
|
|
119
|
+
check("a source repeated inside one rule is listed once", (sourcesOf(repeated.processed, 0) || []).length, 3);
|
|
120
|
+
|
|
121
|
+
const REPEATED_LAST = `
|
|
122
|
+
@font-face{font-family:repeat;src:url(a.woff) format("woff"),url(b.woff) format("woff"),url(a.woff) format("woff");font-weight:400}`;
|
|
123
|
+
|
|
124
|
+
const repeatedLast = await run(REPEATED_LAST);
|
|
125
|
+
check("a repeat in last position is recognised too", (sourcesOf(repeatedLast.processed, 0) || []).length, 2);
|
|
126
|
+
// the list is held in reverse of the order it is written back in, so the entry the rule declares
|
|
127
|
+
// last comes first here: a.woff keeps the position its second declaration gives it
|
|
128
|
+
check("the repeat keeps the position of its later declaration", sourcesOf(repeatedLast.processed, 0), ["url(a.woff)format(\"woff\")", "url(b.woff)format(\"woff\")"]);
|
|
129
|
+
|
|
130
|
+
if (failures) {
|
|
131
|
+
console.log("\n" + failures + " check(s) failed");
|
|
132
|
+
Deno.exit(1);
|
|
133
|
+
} else {
|
|
134
|
+
console.log("\nall checks passed");
|
|
135
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "./dom-stub.js";
|
|
2
|
-
import { makePageData, makeOptions, runProcess, mulberry32 } from "./common.js";
|
|
2
|
+
import { makePageData, makeOptions, runProcess, mulberry32, freezeDate } from "./common.js";
|
|
3
3
|
import { ZipReader, ZipWriter, BlobReader } from "../../vendor/zip/zip.js";
|
|
4
4
|
|
|
5
5
|
// the quote is there because the escaper the title shares with the table of contents encodes
|
|
@@ -239,6 +239,65 @@ function countIdentifiers(text) {
|
|
|
239
239
|
check("a relocated archive declares no comment", view.getUint16(bytes.length - 2, true), 0);
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
function makeNewlinePageData(seed, newlineCount) {
|
|
243
|
+
const pageData = makePageData(seed, 4 * 1024);
|
|
244
|
+
const rand = mulberry32(seed);
|
|
245
|
+
const newlines = ["\n", "\r", "\r\n"];
|
|
246
|
+
let content = "";
|
|
247
|
+
for (let index = 0; index < newlineCount; index++) {
|
|
248
|
+
content += newlines[(rand() * 3) | 0];
|
|
249
|
+
}
|
|
250
|
+
pageData.resources.stylesheets.push({ name: "newlines.txt", extension: ".txt", content, url: "https://example.com/newlines.txt" });
|
|
251
|
+
return pageData;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// libarchive gives up looking for the end of central directory record 16383 bytes from the end of
|
|
255
|
+
// the file, so the default budget is 16361 appended bytes: that window minus the 22-byte record.
|
|
256
|
+
// This fixture's payload lands between the default and the 65535-byte comment ceiling, which is
|
|
257
|
+
// the range the old budget kept appended and bsdtar could not open.
|
|
258
|
+
// The Date must be frozen: `archiveTime` (compression.js:155) puts an ISO timestamp in the
|
|
259
|
+
// archive, whose milliseconds move a few newline bytes in and out of the recovered range and
|
|
260
|
+
// therefore change the payload length build to build. The boundary checks below compare a budget
|
|
261
|
+
// against a run measured in an EARLIER build, so without freezing they are off by a few bytes one
|
|
262
|
+
// run in three. check-determinism.js asserts both halves of that
|
|
263
|
+
{
|
|
264
|
+
const restoreDate = freezeDate();
|
|
265
|
+
try {
|
|
266
|
+
const wide = makeOptions({ disableCompression: true, maxAppendedDataLength: 65535 });
|
|
267
|
+
const { bytes: wideBytes } = await runProcess(makeNewlinePageData(43, 80 * 1000), wide);
|
|
268
|
+
const wideTail = readAppendedData(wideBytes).trailing;
|
|
269
|
+
check("a wider budget keeps the payload appended", wide.extraDataSize, undefined);
|
|
270
|
+
check("the fixture overflows the default budget", wideTail > 16361, true);
|
|
271
|
+
check("the fixture fits the comment ceiling", wideTail <= 65535, true);
|
|
272
|
+
|
|
273
|
+
const byDefault = makeOptions({ disableCompression: true });
|
|
274
|
+
const { bytes } = await runProcess(makeNewlinePageData(43, 80 * 1000), byDefault);
|
|
275
|
+
check("the default budget relocates the payload", byDefault.extraDataSize > 0, true);
|
|
276
|
+
check("the default budget keeps the record in libarchive's window", readAppendedData(bytes).trailing <= 16361, true);
|
|
277
|
+
|
|
278
|
+
const fitting = makeOptions({ disableCompression: true, maxAppendedDataLength: wideTail });
|
|
279
|
+
await runProcess(makeNewlinePageData(43, 80 * 1000), fitting);
|
|
280
|
+
check("a budget matching the run to the byte keeps it appended", fitting.extraDataSize, undefined);
|
|
281
|
+
|
|
282
|
+
const tight = makeOptions({ disableCompression: true, maxAppendedDataLength: wideTail - 1 });
|
|
283
|
+
await runProcess(makeNewlinePageData(43, 80 * 1000), tight);
|
|
284
|
+
check("one byte below the run relocates it", tight.extraDataSize > 0, true);
|
|
285
|
+
} finally {
|
|
286
|
+
restoreDate();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// the budget and the comment ceiling are two different limits, and only the second is a property
|
|
291
|
+
// of the format. A budget raised past 65535 produces a run the comment-length field cannot hold,
|
|
292
|
+
// which setUint16 would write back modulo 65536: the writer leaves it undeclared instead
|
|
293
|
+
{
|
|
294
|
+
const options = makeOptions({ disableCompression: true, declareAppendedData: true, maxAppendedDataLength: Number.MAX_SAFE_INTEGER });
|
|
295
|
+
const { bytes } = await runProcess(makeNewlinePageData(44, 320 * 1000), options);
|
|
296
|
+
const { declared, trailing } = readAppendedData(bytes);
|
|
297
|
+
check("a run past the comment ceiling stays appended", trailing > 65535, true);
|
|
298
|
+
check("a run past the comment ceiling is left undeclared", declared, 0);
|
|
299
|
+
}
|
|
300
|
+
|
|
242
301
|
{
|
|
243
302
|
const options = makeOptions({ embeddedPdf: PDF });
|
|
244
303
|
const pageData = makePageData(15, 4 * 1024);
|
|
@@ -373,9 +432,13 @@ const ALL_FACE_RUNGS = "<!--sfz-data<script<style<noframes<noembed<iframe<xmp<![
|
|
|
373
432
|
check("no page.pdf entry is left behind", entries.some(entry => entry.filename.endsWith("page.pdf")), false);
|
|
374
433
|
}
|
|
375
434
|
|
|
376
|
-
// page.pdf is the only record the writer builds by hand, so it is the only place the
|
|
377
|
-
//
|
|
378
|
-
// through
|
|
435
|
+
// page.pdf is the only record the writer builds by hand, so it is the only place the language
|
|
436
|
+
// encoding flag can disagree with the rest of the archive, and a reader would then decode that one
|
|
437
|
+
// name through a different path than every other name in the same file. Which way the flag goes is
|
|
438
|
+
// the ZIP writer's business, not this format's: it sets bit 11 only when a name or a comment holds
|
|
439
|
+
// a byte outside printable ASCII, so every name in this fixture is flagless. What is asserted here
|
|
440
|
+
// is the agreement, so this still fails if the hand-built record diverges and still passes if the
|
|
441
|
+
// writer changes its rule again
|
|
379
442
|
{
|
|
380
443
|
const options = makeOptions({ embeddedPdf: PDF });
|
|
381
444
|
const pageData = makePageData(23, 4 * 1024);
|
|
@@ -384,11 +447,14 @@ const ALL_FACE_RUNGS = "<!--sfz-data<script<style<noframes<noembed<iframe<xmp<![
|
|
|
384
447
|
const zipReader = new ZipReader(new BlobReader(new Blob([bytes])));
|
|
385
448
|
const entries = await zipReader.getEntries();
|
|
386
449
|
await zipReader.close();
|
|
387
|
-
|
|
388
|
-
check("
|
|
450
|
+
const pdfEntry = entries.find(entry => entry.filename == "page.pdf");
|
|
451
|
+
check("the pdf entry is listed", Boolean(pdfEntry), true);
|
|
452
|
+
check("the hand-built central record declares its name like the records the writer produces",
|
|
453
|
+
entries.every(entry => Boolean(entry.filenameUTF8) == Boolean(pdfEntry.filenameUTF8)), true);
|
|
389
454
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
390
|
-
const localFlags = entries.map(entry => view.getUint16(entry.offset + 6, true));
|
|
391
|
-
check("
|
|
455
|
+
const localFlags = new Map(entries.map(entry => [entry.filename, view.getUint16(entry.offset + 6, true) & 0x0800]));
|
|
456
|
+
check("the hand-built local header declares its name like the headers the writer produces",
|
|
457
|
+
[...localFlags.values()].every(flag => flag == localFlags.get("page.pdf")), true);
|
|
392
458
|
}
|
|
393
459
|
|
|
394
460
|
{
|