single-file-core 1.5.84 → 1.5.86

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.
@@ -26,7 +26,7 @@
26
26
  /*
27
27
  * Generated by PEG.js 0.10.0.
28
28
  *
29
- * http://pegjs.org/
29
+ * https://github.com/pegjs/pegjs
30
30
  */
31
31
 
32
32
  /* Modified manually by Gildas Lormeau in order to support async/await callbacks */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-core",
3
- "version": "1.5.84",
3
+ "version": "1.5.86",
4
4
  "description": "SingleFile Core",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
@@ -29,7 +29,7 @@ export {
29
29
 
30
30
  async function display(document, docContent, { disableFramePointerEvents } = {}) {
31
31
  docContent = docContent.replace(/<noscript/gi, "<template disabled-noscript");
32
- docContent = docContent.replaceAll(/<\/noscript/gi, "</template");
32
+ docContent = docContent.replace(/<\/noscript/gi, "</template");
33
33
  const doc = (new DOMParser()).parseFromString(docContent, "text/html");
34
34
  if (disableFramePointerEvents) {
35
35
  doc.querySelectorAll("iframe").forEach(element => {
@@ -25,6 +25,7 @@
25
25
 
26
26
  import {
27
27
  configure,
28
+ deflateRaw,
28
29
  BlobReader,
29
30
  TextReader,
30
31
  ZipWriter,
@@ -37,7 +38,10 @@ import {
37
38
  display
38
39
  } from "./compression-display.js";
39
40
 
40
- const { Blob, fetch, TextEncoder, DOMParser } = globalThis;
41
+ const { Blob, fetch, TextEncoder, TextDecoder, DOMParser } = globalThis;
42
+
43
+ // windows-1252 never decodes bytes >= 0x80 into the ASCII range, the scanned patterns are all ASCII
44
+ const TEXT_DECODER = new TextDecoder("windows-1252");
41
45
 
42
46
  const NO_COMPRESSION_EXTENSIONS = [".jpg", ".jpeg", ".png", ".avi", ".apng", ".pdf", ".woff2", ".mp4", ".mp3", ".ogg", ".webp", ".webm", ".avi", ".mpeg", ".ts", ".ogv", ".heif", ".heic"];
43
47
  const SCRIPT_PATH = "/lib/single-file-zip.min.js";
@@ -56,17 +60,17 @@ const EMBEDDED_DATA_TAGS = [
56
60
  ...EXTRA_DATA_TAGS,
57
61
  ];
58
62
  const EXTRA_DATA_REGEXPS = [
59
- [/<noscript/i, /<\/noscript>/i],
60
- [/<noframes/i, /<\/noframes>/i],
61
- [/<noembed/i, /<\/noembed>/i],
62
- [/<script/i, /<\/script>/i],
63
- [/<style/i, /<\/style>/i],
64
- [/<iframe/i, /<\/iframe>/i],
65
- [/<xmp/i, /<\/xmp>/i],
66
- [/<plaintext/i, /<\/plaintext>/i]
63
+ [/<noscript/i, /<\/noscript[\t\n\f\r />]/i],
64
+ [/<noframes/i, /<\/noframes[\t\n\f\r />]/i],
65
+ [/<noembed/i, /<\/noembed[\t\n\f\r />]/i],
66
+ [/<script/i, /<\/script[\t\n\f\r />]/i],
67
+ [/<style/i, /<\/style[\t\n\f\r />]/i],
68
+ [/<iframe/i, /<\/iframe[\t\n\f\r />]/i],
69
+ [/<xmp/i, /<\/xmp[\t\n\f\r />]/i],
70
+ [/<plaintext/i, /<\/plaintext[\t\n\f\r />]/i]
67
71
  ];
68
72
  const EMBEDDED_DATA_REGEXPS = [
69
- [/<!--/i, /-->/i],
73
+ [/<!--/i, /--!?>/i],
70
74
  ...EXTRA_DATA_REGEXPS,
71
75
  ];
72
76
  const CRC32_TABLE = new Uint32Array(256).map((_, indexTable) => {
@@ -83,7 +87,8 @@ const PNG_IHDR_LENGTH = 25;
83
87
  const browser = globalThis.browser;
84
88
 
85
89
  export {
86
- process
90
+ process,
91
+ createArchive
87
92
  };
88
93
 
89
94
  async function process(pageData, options, lastModDate = new Date()) {
@@ -91,20 +96,28 @@ async function process(pageData, options, lastModDate = new Date()) {
91
96
  if (options.zipScript) {
92
97
  script = options.zipScript;
93
98
  } else if (browser && browser.runtime && browser.runtime.getURL) {
94
- configure({ workerScripts: { deflate: ["/lib/single-file-z-worker.js"] } });
99
+ configure({ workerURI: "/lib/single-file-z-worker.js" });
95
100
  script = await (await fetch(browser.runtime.getURL(SCRIPT_PATH))).text();
96
101
  }
102
+ return createArchive(pageData, options, script, zipWriter => {
103
+ pageData.url = options.url;
104
+ pageData.archiveTime = (new Date()).toISOString();
105
+ return addPageResources(zipWriter, pageData, { password: options.password, disableCompression: options.disableCompression }, options.createRootDirectory ? String(Date.now()) + "_" + (options.tabId || 0) + "/" : "", options.url);
106
+ }, lastModDate);
107
+ }
108
+
109
+ async function createArchive(pageData, options, script, writeEntries, lastModDate = new Date()) {
97
110
  const zipDataWriter = new Uint8ArrayWriter();
98
111
  zipDataWriter.init();
99
112
  zipDataWriter.writable.size = 0;
100
113
  let extraDataOffset, extraData, embeddedImageDataOffset, endTag;
101
114
  if (options.embeddedImage) {
102
- options.embeddedImage = Array.from(options.embeddedImage);
115
+ options.embeddedImage = new Uint8Array(options.embeddedImage);
103
116
  const embeddedImageData = options.embeddedImage.slice(PNG_SIGNATURE_LENGTH + PNG_IHDR_LENGTH, options.embeddedImage.length - PNG_IEND_LENGTH);
104
117
  await writeData(zipDataWriter.writable, options.embeddedImage.slice(0, PNG_SIGNATURE_LENGTH + PNG_IHDR_LENGTH));
105
118
  if (options.selfExtractingArchive) {
106
- const embeddedImageText = embeddedImageData.reduce((text, charCode) => text + String.fromCharCode(charCode), "");
107
- const tagIndex = EMBEDDED_DATA_REGEXPS.findIndex(tests => !embeddedImageText.match(tests[1]));
119
+ const embeddedImageText = TEXT_DECODER.decode(embeddedImageData);
120
+ const tagIndex = EMBEDDED_DATA_REGEXPS.slice(0, -1).findIndex(tests => !embeddedImageText.match(tests[1]));
108
121
  let startTag;
109
122
  [startTag, endTag] = tagIndex == -1 ? ["", ""] : EMBEDDED_DATA_TAGS[tagIndex];
110
123
  const htmlArray = getStartHTMLArray(pageData, options, startTag);
@@ -129,33 +142,45 @@ async function process(pageData, options, lastModDate = new Date()) {
129
142
  } else if (!options.embeddedImage && options.embeddedPdf) {
130
143
  await writeData(zipDataWriter.writable, new Uint8Array(options.embeddedPdf));
131
144
  }
132
- const zipWriter = new ZipWriter(zipDataWriter, { bufferedWrite: true, keepOrder: true, lastModDate });
145
+ const zipWriter = new ZipWriter(zipDataWriter, { bufferedWrite: true, keepOrder: true, lastModDate, useCompressionStream: true });
133
146
  const startOffset = zipDataWriter.offset;
134
- pageData.url = options.url;
135
- pageData.archiveTime = (new Date()).toISOString();
136
- await addPageResources(zipWriter, pageData, { password: options.password, disableCompression: options.disableCompression }, options.createRootDirectory ? String(Date.now()) + "_" + (options.tabId || 0) + "/" : "", options.url);
147
+ await writeEntries(zipWriter);
137
148
  const data = await zipWriter.close(null, { preventClose: true });
138
149
  if (options.selfExtractingArchive) {
139
- const insertionsCRLF = [];
140
- const substitutionsLF = [];
150
+ const lfCodes = [];
151
+ let crc32 = -1;
141
152
  if (options.extractDataFromPage) {
142
- if (!options.extractDataFromPageTags) {
143
- let textContent = "";
144
- data.slice(startOffset).forEach(charCode => textContent += String.fromCharCode(charCode));
145
- const matchCommentTags = textContent.match(/<!--/i) || textContent.match(/-->/i);
146
- if (matchCommentTags) {
147
- return findExtraDataTags(textContent, pageData, options, lastModDate);
153
+ if (!options.extractDataFromPageTags || options.extractDataFromPageTags[0] != "<plaintext>") {
154
+ const textContent = TEXT_DECODER.decode(data.subarray(startOffset));
155
+ if (options.extractDataFromPageTags) {
156
+ const tagIndex = EXTRA_DATA_TAGS.indexOf(options.extractDataFromPageTags);
157
+ const regExpsTag = EXTRA_DATA_REGEXPS[tagIndex];
158
+ if (textContent.match(regExpsTag[0]) || textContent.match(regExpsTag[1])) {
159
+ return findExtraDataTags(textContent, pageData, options, script, writeEntries, lastModDate, tagIndex + 1);
160
+ }
161
+ } else {
162
+ const matchCommentTags = textContent.match(/<!--/i) || textContent.match(/--!?>/i);
163
+ if (matchCommentTags) {
164
+ return findExtraDataTags(textContent, pageData, options, script, writeEntries, lastModDate);
165
+ }
148
166
  }
149
167
  }
150
168
  for (let index = startOffset; index < data.length; index++) {
151
- if (data[index] == 13) {
169
+ const byte = data[index];
170
+ crc32 = (crc32 >>> 8) ^ CRC32_TABLE[(crc32 ^ byte) & 0xff];
171
+ if (byte == 10) {
172
+ lfCodes.push(0);
173
+ } else if (byte == 13) {
152
174
  if (data[index + 1] == 10) {
153
- insertionsCRLF.push(index - startOffset);
175
+ index++;
176
+ crc32 = (crc32 >>> 8) ^ CRC32_TABLE[(crc32 ^ 10) & 0xff];
177
+ lfCodes.push(2);
154
178
  } else {
155
- substitutionsLF.push(index - startOffset);
179
+ lfCodes.push(1);
156
180
  }
157
181
  }
158
182
  }
183
+ crc32 = (crc32 ^ -1) >>> 0;
159
184
  }
160
185
  let pageContent = "";
161
186
  if (!options.preventAppendedData) {
@@ -167,21 +192,22 @@ async function process(pageData, options, lastModDate = new Date()) {
167
192
  }
168
193
  const endTags = options.preventAppendedData || options.embeddedImage ? "" : "</body></html>";
169
194
  if (options.extractDataFromPage) {
170
- const payload = new Uint32Array(insertionsCRLF.length + substitutionsLF.length + 2);
171
- payload.set(new Uint32Array([insertionsCRLF.length]), 0);
172
- payload.set(new Uint32Array(insertionsCRLF), 1);
173
- payload.set(new Uint32Array([substitutionsLF.length]), insertionsCRLF.length + 1);
174
- payload.set(new Uint32Array(substitutionsLF), insertionsCRLF.length + 2);
175
- extraData = "<sfz-extra-data>" + compress(payload.buffer) + "</sfz-extra-data>";
195
+ // payload layout: [crc32, zip data length, LF codes count, 2-bit codes (0=LF, 1=CR, 2=CRLF) packed LSB-first]
196
+ const payload = new Uint32Array(3 + Math.ceil(lfCodes.length / 16));
197
+ payload[0] = crc32;
198
+ payload[1] = data.length - startOffset;
199
+ payload[2] = lfCodes.length;
200
+ lfCodes.forEach((lfCode, indexLFCode) => payload[3 + (indexLFCode >> 4)] |= lfCode << ((indexLFCode & 15) * 2));
201
+ extraData = "<sfz-extra-data>" + base64Encode(deflateRaw(new Uint8Array(payload.buffer))) + "</sfz-extra-data>";
176
202
  if (options.preventAppendedData || extraData.length > 65535 - endTags.length - (options.embeddedImage ? PNG_IEND_LENGTH : 0)) {
177
203
  if (!options.extraDataSize) {
178
204
  options.extraDataSize = Math.floor(extraData.length * 1.001);
179
- return process(pageData, options, lastModDate);
205
+ return createArchive(pageData, options, script, writeEntries, lastModDate);
180
206
  }
181
207
  } else {
182
208
  if (options.extraDataSize) {
183
209
  options.extraDataSize = undefined;
184
- return process(pageData, options, lastModDate);
210
+ return createArchive(pageData, options, script, writeEntries, lastModDate);
185
211
  } else {
186
212
  pageContent += extraData;
187
213
  }
@@ -194,11 +220,11 @@ async function process(pageData, options, lastModDate = new Date()) {
194
220
  const pageContent = await zipDataWriter.getData();
195
221
  if (options.extractDataFromPage && options.extraDataSize !== undefined) {
196
222
  if (options.extraDataSize >= extraData.length) {
197
- pageContent.set(Array.from(extraData).map(character => character.charCodeAt(0)), startOffset - extraDataOffset);
223
+ pageContent.set(new TextEncoder().encode(extraData), startOffset - extraDataOffset);
198
224
  } else {
199
225
  options.extraData = extraData;
200
226
  options.extraDataSize = Math.floor(extraData.length * 1.001);
201
- return process(pageData, options, lastModDate);
227
+ return createArchive(pageData, options, script, writeEntries, lastModDate);
202
228
  }
203
229
  }
204
230
  if (options.embeddedImage) {
@@ -206,7 +232,7 @@ async function process(pageData, options, lastModDate = new Date()) {
206
232
  return new Blob([
207
233
  pageContent,
208
234
  getCRC32(pageContent, embeddedImageDataOffset),
209
- new Uint8Array(options.embeddedImage.slice(options.embeddedImage.length - PNG_IEND_LENGTH))
235
+ options.embeddedImage.slice(options.embeddedImage.length - PNG_IEND_LENGTH)
210
236
  ], { type: "application/octet-stream" });
211
237
  } else {
212
238
  return new Blob([pageContent], { type: "application/octet-stream" });
@@ -243,7 +269,10 @@ async function prependHTMLData(pageData, zipDataWriter, script, options) {
243
269
  await writeData(zipDataWriter.writable, getStartHTMLArray(pageData, options));
244
270
  }
245
271
  pageContent += "<div id=sfz-wait-message>Please wait...</div>";
246
- if (!options.extractDataFromPage) {
272
+ if (options.extractDataFromPage) {
273
+ pageContent += "<div id=sfz-error-message><strong>Error</strong>: Cannot extract the data of the page.";
274
+ pageContent += " The file is still a valid ZIP file, you can rename it with a \"zip\" extension and unzip it to display the page and its resources.</div>";
275
+ } else {
247
276
  pageContent += "<div id=sfz-error-message><strong>Error</strong>: Cannot open the page from the filesystem.";
248
277
  pageContent += "<ul style='line-height:20px;'>";
249
278
  pageContent += "<li style='margin-bottom:10px'><strong>Chrome/Edge/Brave</strong>: Install <a href='https://www.getsinglefile.com'>SingleFile</a> and enable the option \"Allow access to file URLs\" in the details page of the extension.</li>";
@@ -277,7 +306,13 @@ async function prependHTMLData(pageData, zipDataWriter, script, options) {
277
306
  "globalThis.bootstrap=(()=>{let bootstrapStarted;return async content=>{if (bootstrapStarted) return bootstrapStarted; bootstrapStarted = (" +
278
307
  extract.toString().replace(/\n|\t/g, "") + ")(content,{prompt}).then(({docContent}) => " +
279
308
  display.toString().replace(/\n|\t/g, "") + "(document,docContent," + JSON.stringify(displayOptions) + "));return bootstrapStarted;}})();(" +
280
- getContent.toString().replace(/\n|\t/g, "") + ")().then(globalThis.bootstrap).then(() => document.dispatchEvent(new CustomEvent(\"single-file-display-infobar\"))).catch(()=>{});" +
309
+ getContent.toString().replace(/\n|\t/g, "") + ")().then(globalThis.bootstrap).then(() => document.dispatchEvent(new CustomEvent(\"single-file-display-infobar\"))).catch(error => {" +
310
+ "console.error(error);" +
311
+ "const waitMessage = document.getElementById(\"sfz-wait-message\");" +
312
+ "if (waitMessage) { waitMessage.remove(); }" +
313
+ "const errorMessage = document.getElementById(\"sfz-error-message\");" +
314
+ "if (errorMessage) { errorMessage.hidden = false; document.body.hidden = false; }" +
315
+ "});" +
281
316
  "</script>";
282
317
  pageContent += script;
283
318
  let extraData = "";
@@ -287,8 +322,9 @@ async function prependHTMLData(pageData, zipDataWriter, script, options) {
287
322
  }
288
323
  pageContent += extraData;
289
324
  const startTag = options.extractDataFromPageTags ? options.extractDataFromPageTags[0] : "<!--";
290
- pageContent += startTag;
291
- const extraDataOffset = startTag.length + extraData.length;
325
+ // the space guarantees a text node between <sfz-extra-data> and the start tag
326
+ pageContent += (extraData ? " " : "") + startTag;
327
+ const extraDataOffset = startTag.length + extraData.length + (extraData ? 1 : 0);
292
328
  await writeData(zipDataWriter.writable, (new TextEncoder()).encode(pageContent));
293
329
  return extraDataOffset;
294
330
  }
@@ -306,8 +342,8 @@ function getStartHTMLArray(pageData, options, startTag = "") {
306
342
  const htmlHeadData = getHTMLHeadData(pageData, options);
307
343
  let htmlArray;
308
344
  if (options.embeddedPdf) {
309
- const embeddedPdfText = options.embeddedPdf.reduce((text, charCode) => text + String.fromCharCode(charCode), "");
310
- const pdfTagIndex = EMBEDDED_DATA_REGEXPS.findIndex(tests => !embeddedPdfText.match(tests[1]));
345
+ const embeddedPdfText = TEXT_DECODER.decode(new Uint8Array(options.embeddedPdf));
346
+ const pdfTagIndex = EMBEDDED_DATA_REGEXPS.slice(0, -1).findIndex(tests => !embeddedPdfText.match(tests[1]));
311
347
  const [pdfStartTag, pdfEndTag] = pdfTagIndex == -1 ? ["", ""] : EMBEDDED_DATA_TAGS[pdfTagIndex];
312
348
  const htmlArray1 = new TextEncoder().encode(html + pdfStartTag);
313
349
  const htmlArray2 = new TextEncoder().encode(pdfEndTag + htmlHeadData + startTag);
@@ -347,19 +383,24 @@ function getPageTitle(pageData) {
347
383
  return pageData.title.replace(/</g, "&lt;").replace(/>/g, "&gt;") || "";
348
384
  }
349
385
 
350
- function findExtraDataTags(textContent, pageData, options, lastModDate, indexExtractDataFromPageTags = 0) {
386
+ function findExtraDataTags(textContent, pageData, options, script, writeEntries, lastModDate, indexExtractDataFromPageTags = 0) {
351
387
  const regExpsTag = EXTRA_DATA_REGEXPS[indexExtractDataFromPageTags];
352
- const matchTag = textContent.match(regExpsTag[0]) || textContent.match(regExpsTag[1]);
388
+ const plaintextTag = EXTRA_DATA_TAGS[indexExtractDataFromPageTags][0] == "<plaintext>";
389
+ const matchTag = !plaintextTag && (textContent.match(regExpsTag[0]) || textContent.match(regExpsTag[1]));
353
390
  if (matchTag) {
354
391
  if (indexExtractDataFromPageTags < EXTRA_DATA_TAGS.length - 1) {
355
- return findExtraDataTags(textContent, pageData, options, lastModDate, indexExtractDataFromPageTags + 1);
392
+ return findExtraDataTags(textContent, pageData, options, script, writeEntries, lastModDate, indexExtractDataFromPageTags + 1);
356
393
  } else {
357
394
  options.extractDataFromPage = false;
358
- return process(pageData, options, lastModDate);
395
+ return createArchive(pageData, options, script, writeEntries, lastModDate);
359
396
  }
360
397
  } else {
361
398
  options.extractDataFromPageTags = EXTRA_DATA_TAGS[indexExtractDataFromPageTags];
362
- return process(pageData, options, lastModDate);
399
+ if (options.extractDataFromPageTags[0] == "<plaintext>") {
400
+ // <plaintext> cannot be closed, the file must end with the zip data
401
+ options.preventAppendedData = true;
402
+ }
403
+ return createArchive(pageData, options, script, writeEntries, lastModDate);
363
404
  }
364
405
  }
365
406
 
@@ -422,29 +463,45 @@ async function getContent() {
422
463
  [352, 138], [8249, 139], [338, 140], [381, 142], [8216, 145], [8217, 146], [8220, 147], [8221, 148], [8226, 149], [8211, 150],
423
464
  [8212, 151], [732, 152], [8482, 153], [353, 154], [8250, 155], [339, 156], [382, 158], [376, 159]
424
465
  ]);
466
+ const crc32Table = new Uint32Array(256).map((_, indexTable) => {
467
+ let crc = indexTable;
468
+ for (let indexBits = 0; indexBits < 8; indexBits++) {
469
+ crc = crc & 1 ? 0xEDB88320 ^ (crc >>> 1) : crc >>> 1;
470
+ }
471
+ return crc;
472
+ });
425
473
  return new Promise((resolve, reject) => {
426
474
  let aborted = false;
427
- getPageData();
475
+ if (location.protocol == "file:") {
476
+ extractDataFromDocument();
477
+ } else {
478
+ getPageData();
479
+ }
480
+
481
+ async function extractDataFromDocument() {
482
+ try {
483
+ await waitForDocumentReady(document);
484
+ document.body.querySelectorAll("meta, style").forEach(element => document.head.appendChild(element));
485
+ const pageData = extractPageData();
486
+ displayMessage("sfz-wait-message", 2);
487
+ resolve(pageData);
488
+ } catch (error) {
489
+ console.error(error);
490
+ displayMessage("sfz-error-message", 2);
491
+ reject(error);
492
+ }
493
+ }
428
494
 
429
495
  function getPageData() {
430
496
  const xhr = new XMLHttpRequest();
431
497
  xhr.responseType = "blob";
432
498
  xhr.open("GET", "");
433
- xhr.onerror = async () => {
499
+ xhr.onerror = () => {
434
500
  if (aborted) {
435
501
  displayMessage("sfz-error-message", 2);
436
502
  reject();
437
503
  } else {
438
- try {
439
- await waitForDocumentReady(document);
440
- document.body.querySelectorAll("meta, style").forEach(element => document.head.appendChild(element));
441
- const pageData = extractPageData();
442
- displayMessage("sfz-wait-message", 2);
443
- resolve(pageData);
444
- } catch {
445
- displayMessage("sfz-error-message", 2);
446
- reject();
447
- };
504
+ extractDataFromDocument();
448
505
  }
449
506
  };
450
507
  xhr.send();
@@ -485,9 +542,14 @@ async function getContent() {
485
542
  if (element) {
486
543
  Array.from(document.body.childNodes).forEach(node => {
487
544
  if (node.id != elementId) {
488
- node.remove();
545
+ if (node.id == "sfz-wait-message" || node.id == "sfz-error-message") {
546
+ node.hidden = true;
547
+ } else {
548
+ node.remove();
549
+ }
489
550
  }
490
551
  });
552
+ element.hidden = false;
491
553
  document.body.hidden = false;
492
554
  element.style = "opacity: 0; animation: 0s linear " + delay + "s display-wait-message 1 normal forwards";
493
555
  }
@@ -506,59 +568,46 @@ async function getContent() {
506
568
  } else {
507
569
  dataNode = zipDataElement.previousSibling;
508
570
  }
509
- const zipData = [];
510
- let { textContent } = dataNode;
571
+ const inflatedPayload = zip.inflateRaw(base64Decode(zipDataElement.textContent));
572
+ const payload = new Uint32Array(inflatedPayload.buffer, inflatedPayload.byteOffset, inflatedPayload.length >> 2);
573
+ const expectedCRC32 = payload[0];
574
+ const zipDataLength = payload[1];
575
+ const lfCodesLength = payload[2];
576
+ const zipData = new Uint8Array(zipDataLength);
577
+ const { textContent } = dataNode;
578
+ let offset = 0;
579
+ let indexLFCode = 0;
580
+ let crc32 = -1;
511
581
  for (let index = 0; index < textContent.length; index++) {
512
582
  const charCode = textContent.charCodeAt(index);
513
- zipData.push(charCode > 255 ? characterMap.get(charCode) : charCode);
514
- }
515
- const payload = new Uint32Array(decompress(zipDataElement.textContent).buffer);
516
- const insertionsCRLFLength = payload[0];
517
- const insertionsCRLF = payload.slice(1, 1 + insertionsCRLFLength);
518
- const substitutionsLFLength = payload[1 + insertionsCRLFLength];
519
- const substitutionsLF = payload.slice(2 + insertionsCRLFLength, 2 + insertionsCRLFLength + substitutionsLFLength);
520
- insertionsCRLF.forEach(index => zipData.splice(index, 1, 13, 10));
521
- substitutionsLF.forEach(index => zipData[index] = 13);
522
- return new Blob([new Uint8Array(zipData)], { type: "application/octet-stream" });
523
- }
524
- throw new Error("Extra zip data data not found");
525
- }
526
-
527
- function decompress(src) {
528
- src = base64Decode(src);
529
- let out = new Uint8Array(1024);
530
- let outLen = 0;
531
- for (let i = 0; i < src.length;) {
532
- const ctrl = src[i++];
533
- if ((ctrl & 0x80) === 0) {
534
- const L = ctrl;
535
- ensure(outLen + L);
536
- for (let j = 0; j < L && i < src.length; j++) {
537
- out[outLen++] = src[i++];
538
- }
539
- } else {
540
- const L = (ctrl & 0x7f) + 3;
541
- const off = (src[i++] << 8) | src[i++];
542
- const start = outLen - off;
543
- ensure(outLen + L);
544
- for (let k = 0; k < L; k++) {
545
- out[outLen++] = out[start + k];
583
+ if (charCode == 10) {
584
+ const lfCode = (payload[3 + (indexLFCode >> 4)] >>> ((indexLFCode & 15) * 2)) & 3;
585
+ indexLFCode++;
586
+ if (lfCode == 0) {
587
+ writeByte(10);
588
+ } else {
589
+ writeByte(13);
590
+ if (lfCode == 2) {
591
+ writeByte(10);
592
+ }
593
+ }
594
+ } else {
595
+ writeByte(charCode > 255 ? characterMap.get(charCode) : charCode);
546
596
  }
547
597
  }
548
- }
549
- return new Uint8Array(out.buffer.slice(0, outLen));
598
+ crc32 = (crc32 ^ -1) >>> 0;
599
+ if (offset != zipDataLength || indexLFCode != lfCodesLength || crc32 != expectedCRC32) {
600
+ throw new Error("Invalid checksum of the extracted zip data");
601
+ }
602
+ return new Blob([zipData], { type: "application/octet-stream" });
550
603
 
551
- function ensure(n) {
552
- if (out.length < n) {
553
- let nl = out.length * 2;
554
- while (nl < n) {
555
- nl *= 2;
556
- }
557
- const nbuf = new Uint8Array(nl);
558
- nbuf.set(out.subarray(0, outLen));
559
- out = nbuf;
604
+ function writeByte(byte) {
605
+ zipData[offset] = byte;
606
+ crc32 = (crc32 >>> 8) ^ crc32Table[(crc32 ^ byte) & 0xff];
607
+ offset++;
560
608
  }
561
- };
609
+ }
610
+ throw new Error("Extra zip data data not found");
562
611
  }
563
612
 
564
613
  function base64Decode(b64) {
@@ -603,94 +652,3 @@ function base64Encode(bytes) {
603
652
  }
604
653
  return out;
605
654
  }
606
-
607
- function compress(input) {
608
- const src = new Uint8Array(input);
609
- const N = src.length;
610
- const out = [];
611
- const litBuf = [];
612
- const MAX_OFFSET = 0xffff;
613
- const MAX_MATCH = 130;
614
- const MAX_CANDIDATES = 64;
615
- const map = new Map();
616
- let i = 0;
617
- while (i < N) {
618
- let bestLen = 0, bestOff = 0;
619
- if (i + 2 < N) {
620
- const key = (src[i] << 16) | (src[i + 1] << 8) | src[i + 2];
621
- const cand = map.get(key) || [];
622
- for (let c = cand.length - 1; c >= 0; c--) {
623
- const j = cand[c];
624
- const off = i - j;
625
- if (off <= 0 || off > MAX_OFFSET) {
626
- continue;
627
- }
628
- let k = 0;
629
- while (k < MAX_MATCH && i + k < N && src[j + k] === src[i + k]) {
630
- k++;
631
- }
632
- if (k > bestLen && k >= 3) {
633
- bestLen = k; bestOff = off;
634
- if (bestLen === MAX_MATCH) {
635
- break;
636
- }
637
- }
638
- }
639
- }
640
-
641
- if (bestLen >= 3) {
642
- if (litBuf.length) {
643
- flushLiterals();
644
- }
645
- let remain = bestLen;
646
- let produced = 0;
647
- while (remain > 0) {
648
- const take = Math.min(remain, MAX_MATCH);
649
- out.push(0x80 | ((take - 3) & 0x7f));
650
- out.push((bestOff >> 8) & 0xff);
651
- out.push(bestOff & 0xff);
652
- remain -= take;
653
- produced += take;
654
- }
655
- const start = i;
656
- for (let p = start; p < start + produced; p++) {
657
- addPos(p);
658
- }
659
- i += produced;
660
- } else {
661
- litBuf.push(src[i]);
662
- addPos(i);
663
- i++;
664
- if (litBuf.length === 127) {
665
- flushLiterals();
666
- }
667
- }
668
- }
669
- if (litBuf.length) {
670
- flushLiterals();
671
- }
672
- const u8 = new Uint8Array(out);
673
- return base64Encode(u8);
674
-
675
- function flushLiterals() {
676
- while (litBuf.length) {
677
- const take = Math.min(127, litBuf.length);
678
- out.push(take);
679
- for (let t = 0; t < take; t++) {
680
- out.push(litBuf.shift());
681
- }
682
- }
683
- }
684
-
685
- function addPos(pos) {
686
- if (pos + 2 < N) {
687
- const key = (src[pos] << 16) | (src[pos + 1] << 8) | src[pos + 2];
688
- const arr = map.get(key) || [];
689
- arr.push(pos);
690
- if (arr.length > MAX_CANDIDATES) {
691
- arr.shift();
692
- }
693
- map.set(key, arr);
694
- }
695
- }
696
- }