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.
@@ -40,9 +40,7 @@ import {
40
40
  router
41
41
  } from "./compression-router.js";
42
42
 
43
- const { Blob, fetch, TextEncoder, TextDecoder, DOMParser } = globalThis;
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 EXTRA_DATA_REGEXPS = [
67
- [/<script/i, /<\/script[\t\n\f\r />]/i],
68
- [/<style/i, /<\/style[\t\n\f\r />]/i],
69
- [/<noframes/i, /<\/noframes[\t\n\f\r />]/i],
70
- [/<noembed/i, /<\/noembed[\t\n\f\r />]/i],
71
- [/<iframe/i, /<\/iframe[\t\n\f\r />]/i],
72
- [/<xmp/i, /<\/xmp[\t\n\f\r />]/i],
73
- [/<!\[CDATA\[/i, /\]\]>/],
74
- [/<plaintext/i, /<\/plaintext[\t\n\f\r />]/i]
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 EMBEDDED_DATA_REGEXPS = [
77
- [/<!--/i, /--!?>|<!-$/i],
78
- ...EXTRA_DATA_REGEXPS,
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 MAX_APPENDED_DATA_LENGTH = 65535;
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 textContent = TEXT_DECODER.decode(data.subarray(startOffset));
230
+ const zipData = data.subarray(startOffset);
231
231
  if (options.extractDataFromPageTags) {
232
232
  const tagIndex = getExtraDataTagIndex(options.extractDataFromPageTags);
233
- const regExpsTag = EXTRA_DATA_REGEXPS[tagIndex];
234
- if (textContent.match(regExpsTag[0]) || textContent.match(regExpsTag[1])) {
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 > MAX_APPENDED_DATA_LENGTH - pageContent.length - endTags.length - (options.embeddedImage ? PNG_IEND_LENGTH + PNG_CHUNK_CRC_LENGTH : 0)) {
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 <= MAX_APPENDED_DATA_LENGTH && isDeclaredLengthHidden(pageContent, zipDataEnd, appendedDataLength, options)) {
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
- const [startRegExp, endRegExp] = EMBEDDED_DATA_REGEXPS[tagIndex];
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 embeddedPdfText = TEXT_DECODER.decode(localHeader) + TEXT_DECODER.decode(embeddedPdf);
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(textContent, pageData, options, script, entriesData, zipWriterOptions, indexExtractDataFromPageTags = 0) {
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 && (textContent.match(regExpsTag[0]) || textContent.match(regExpsTag[1]));
623
+ const matchTag = !plaintextTag && containsDataPattern(zipData, EXTRA_DATA_PATTERNS[indexExtractDataFromPageTags]);
630
624
  if (matchTag) {
631
- return findExtraDataTags(textContent, pageData, options, script, entriesData, zipWriterOptions, indexExtractDataFromPageTags + 1);
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(text, fromIndex = 0) {
642
- const tagIndex = EMBEDDED_DATA_REGEXPS.slice(fromIndex, -1).findIndex(([startRegExp, endRegExp]) => !text.match(startRegExp) && !text.match(endRegExp));
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 embeddedImageText = TEXT_DECODER.decode(getEmbeddedImageData(options.embeddedImage)) +
648
- TEXT_DECODER.decode(new Uint8Array(4)) + TEXT_DECODER.decode(PNG_ZIP_CHUNK_TYPE_KEYWORD);
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 wrappedText = TEXT_DECODER.decode(htmlDataCRC) + embeddedImageText;
718
+ const wrappedData = concatArrays(htmlDataCRC, embeddedImageData);
656
719
  if ((tagIndex == 0 && (htmlDataCRC[0] == 0x3e || (htmlDataCRC[0] == 0x2d && htmlDataCRC[1] == 0x3e))) ||
657
- findEmbeddedDataTagIndex(wrappedText, tagIndex) != tagIndex) {
658
- tagIndex = findEmbeddedDataTagIndex(embeddedImageText, tagIndex + 1);
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
- if (location.protocol == "file:") {
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 Image = globalThis.Image;
249
- globalThis._singleFileImage = globalThis.Image;
250
- globalThis.__defineGetter__("Image", function () {
251
- return function () {
252
- const image = new Image(...arguments);
253
- const result = new Image(...arguments);
254
- result.__defineSetter__("src", value => {
255
- image.src = value;
256
- document.dispatchEvent(new CustomEvent(LOAD_IMAGE_EVENT, { detail: image.src }));
257
- });
258
- result.__defineGetter__("src", () => image.src);
259
- result.__defineSetter__("srcset", value => {
260
- document.dispatchEvent(new CustomEvent(LOAD_IMAGE_EVENT));
261
- image.srcset = value;
262
- });
263
- result.__defineGetter__("srcset", () => image.srcset);
264
- result.__defineGetter__("height", () => image.height);
265
- result.__defineGetter__("width", () => image.width);
266
- result.__defineGetter__("naturalHeight", () => image.naturalHeight);
267
- result.__defineGetter__("naturalWidth", () => image.naturalWidth);
268
- if (image.decode) {
269
- result.__defineGetter__("decode", () => () => image.decode());
270
- }
271
- image.onload = image.onloadend = image.onerror = event => {
272
- document.dispatchEvent(new CustomEvent(IMAGE_LOADED_EVENT, { detail: image.src }));
273
- result.dispatchEvent(new Event(event.type, event));
274
- };
275
- return result;
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, options) {
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
- // language encoding flag can go missing: without it a reader decodes that one name
378
- // through CP437 while reading every other name in the same archive as UTF-8
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
- check("the pdf entry is listed", entries.some(entry => entry.filename == "page.pdf"), true);
388
- check("every central record declares utf-8 names", entries.every(entry => entry.filenameUTF8), true);
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("every local header declares utf-8 names", localFlags.every(flags => Boolean(flags & 0x0800)), true);
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
  {