single-file-core 1.5.83 → 1.5.85

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.
@@ -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) => {
@@ -91,7 +95,7 @@ async function process(pageData, options, lastModDate = new Date()) {
91
95
  if (options.zipScript) {
92
96
  script = options.zipScript;
93
97
  } else if (browser && browser.runtime && browser.runtime.getURL) {
94
- configure({ workerScripts: { deflate: ["/lib/single-file-z-worker.js"] } });
98
+ configure({ workerURI: "/lib/single-file-z-worker.js" });
95
99
  script = await (await fetch(browser.runtime.getURL(SCRIPT_PATH))).text();
96
100
  }
97
101
  const zipDataWriter = new Uint8ArrayWriter();
@@ -103,8 +107,8 @@ async function process(pageData, options, lastModDate = new Date()) {
103
107
  const embeddedImageData = options.embeddedImage.slice(PNG_SIGNATURE_LENGTH + PNG_IHDR_LENGTH, options.embeddedImage.length - PNG_IEND_LENGTH);
104
108
  await writeData(zipDataWriter.writable, options.embeddedImage.slice(0, PNG_SIGNATURE_LENGTH + PNG_IHDR_LENGTH));
105
109
  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]));
110
+ const embeddedImageText = TEXT_DECODER.decode(new Uint8Array(embeddedImageData));
111
+ const tagIndex = EMBEDDED_DATA_REGEXPS.slice(0, -1).findIndex(tests => !embeddedImageText.match(tests[1]));
108
112
  let startTag;
109
113
  [startTag, endTag] = tagIndex == -1 ? ["", ""] : EMBEDDED_DATA_TAGS[tagIndex];
110
114
  const htmlArray = getStartHTMLArray(pageData, options, startTag);
@@ -129,33 +133,47 @@ async function process(pageData, options, lastModDate = new Date()) {
129
133
  } else if (!options.embeddedImage && options.embeddedPdf) {
130
134
  await writeData(zipDataWriter.writable, new Uint8Array(options.embeddedPdf));
131
135
  }
132
- const zipWriter = new ZipWriter(zipDataWriter, { bufferedWrite: true, keepOrder: false, lastModDate });
136
+ const zipWriter = new ZipWriter(zipDataWriter, { bufferedWrite: true, keepOrder: true, lastModDate, useCompressionStream: true });
133
137
  const startOffset = zipDataWriter.offset;
134
138
  pageData.url = options.url;
135
139
  pageData.archiveTime = (new Date()).toISOString();
136
140
  await addPageResources(zipWriter, pageData, { password: options.password, disableCompression: options.disableCompression }, options.createRootDirectory ? String(Date.now()) + "_" + (options.tabId || 0) + "/" : "", options.url);
137
141
  const data = await zipWriter.close(null, { preventClose: true });
138
142
  if (options.selfExtractingArchive) {
139
- const insertionsCRLF = [];
140
- const substitutionsLF = [];
143
+ const lfCodes = [];
144
+ let crc32 = -1;
141
145
  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);
146
+ if (!options.extractDataFromPageTags || options.extractDataFromPageTags[0] != "<plaintext>") {
147
+ const textContent = TEXT_DECODER.decode(data.subarray(startOffset));
148
+ if (options.extractDataFromPageTags) {
149
+ const tagIndex = EXTRA_DATA_TAGS.indexOf(options.extractDataFromPageTags);
150
+ const regExpsTag = EXTRA_DATA_REGEXPS[tagIndex];
151
+ if (textContent.match(regExpsTag[0]) || textContent.match(regExpsTag[1])) {
152
+ return findExtraDataTags(textContent, pageData, options, lastModDate, tagIndex + 1);
153
+ }
154
+ } else {
155
+ const matchCommentTags = textContent.match(/<!--/i) || textContent.match(/--!?>/i);
156
+ if (matchCommentTags) {
157
+ return findExtraDataTags(textContent, pageData, options, lastModDate);
158
+ }
148
159
  }
149
160
  }
150
161
  for (let index = startOffset; index < data.length; index++) {
151
- if (data[index] == 13) {
162
+ const byte = data[index];
163
+ crc32 = (crc32 >>> 8) ^ CRC32_TABLE[(crc32 ^ byte) & 0xff];
164
+ if (byte == 10) {
165
+ lfCodes.push(0);
166
+ } else if (byte == 13) {
152
167
  if (data[index + 1] == 10) {
153
- insertionsCRLF.push(index - startOffset);
168
+ index++;
169
+ crc32 = (crc32 >>> 8) ^ CRC32_TABLE[(crc32 ^ 10) & 0xff];
170
+ lfCodes.push(2);
154
171
  } else {
155
- substitutionsLF.push(index - startOffset);
172
+ lfCodes.push(1);
156
173
  }
157
174
  }
158
175
  }
176
+ crc32 = (crc32 ^ -1) >>> 0;
159
177
  }
160
178
  let pageContent = "";
161
179
  if (!options.preventAppendedData) {
@@ -167,12 +185,13 @@ async function process(pageData, options, lastModDate = new Date()) {
167
185
  }
168
186
  const endTags = options.preventAppendedData || options.embeddedImage ? "" : "</body></html>";
169
187
  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>";
188
+ // payload layout: [crc32, zip data length, LF codes count, 2-bit codes (0=LF, 1=CR, 2=CRLF) packed LSB-first]
189
+ const payload = new Uint32Array(3 + Math.ceil(lfCodes.length / 16));
190
+ payload[0] = crc32;
191
+ payload[1] = data.length - startOffset;
192
+ payload[2] = lfCodes.length;
193
+ lfCodes.forEach((lfCode, indexLFCode) => payload[3 + (indexLFCode >> 4)] |= lfCode << ((indexLFCode & 15) * 2));
194
+ extraData = "<sfz-extra-data>" + base64Encode(deflateRaw(new Uint8Array(payload.buffer))) + "</sfz-extra-data>";
176
195
  if (options.preventAppendedData || extraData.length > 65535 - endTags.length - (options.embeddedImage ? PNG_IEND_LENGTH : 0)) {
177
196
  if (!options.extraDataSize) {
178
197
  options.extraDataSize = Math.floor(extraData.length * 1.001);
@@ -194,7 +213,7 @@ async function process(pageData, options, lastModDate = new Date()) {
194
213
  const pageContent = await zipDataWriter.getData();
195
214
  if (options.extractDataFromPage && options.extraDataSize !== undefined) {
196
215
  if (options.extraDataSize >= extraData.length) {
197
- pageContent.set(Array.from(extraData).map(character => character.charCodeAt(0)), startOffset - extraDataOffset);
216
+ pageContent.set(new TextEncoder().encode(extraData), startOffset - extraDataOffset);
198
217
  } else {
199
218
  options.extraData = extraData;
200
219
  options.extraDataSize = Math.floor(extraData.length * 1.001);
@@ -243,11 +262,13 @@ async function prependHTMLData(pageData, zipDataWriter, script, options) {
243
262
  await writeData(zipDataWriter.writable, getStartHTMLArray(pageData, options));
244
263
  }
245
264
  pageContent += "<div id=sfz-wait-message>Please wait...</div>";
246
- if (!options.extractDataFromPage) {
265
+ if (options.extractDataFromPage) {
266
+ pageContent += "<div id=sfz-error-message><strong>Error</strong>: Cannot extract the data of the page.";
267
+ 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>";
268
+ } else {
247
269
  pageContent += "<div id=sfz-error-message><strong>Error</strong>: Cannot open the page from the filesystem.";
248
270
  pageContent += "<ul style='line-height:20px;'>";
249
- pageContent += "<li style='margin-bottom:10px'><strong>Chrome</strong>: Install <a href='https://chrome.google.com/webstore/detail/singlefile/mpiodijhokgodhhofbcjdecpffjipkle'>SingleFile</a> and enable the option \"Allow access to file URLs\" in the details page of the extension (chrome://extensions/?id=mpiodijhokgodhhofbcjdecpffjipkle).</li>";
250
- pageContent += "<li style='margin-bottom:10px'><strong>Microsoft Edge</strong>: Install <a href='https://microsoftedge.microsoft.com/addons/detail/singlefile/efnbkdcfmcmnhlkaijjjmhjjgladedno'>SingleFile</a> and enable the option \"Allow access to file URLs\" in the details page of the extension (edge://extensions/?id=efnbkdcfmcmnhlkaijjjmhjjgladedno).</li>";
271
+ 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>";
251
272
  pageContent += "<li><strong>Safari</strong>: Select \"Security > Disable Local File Restrictions\" in the \"Develop > Developer settings\" menu.</li></ul></div>";
252
273
  }
253
274
  if (options.insertTextBody) {
@@ -275,11 +296,15 @@ async function prependHTMLData(pageData, zipDataWriter, script, options) {
275
296
  script = "<script>" +
276
297
  script +
277
298
  "document.currentScript.remove();" +
278
- "globalThis.addEventListener('load', () => {" +
279
299
  "globalThis.bootstrap=(()=>{let bootstrapStarted;return async content=>{if (bootstrapStarted) return bootstrapStarted; bootstrapStarted = (" +
280
300
  extract.toString().replace(/\n|\t/g, "") + ")(content,{prompt}).then(({docContent}) => " +
281
301
  display.toString().replace(/\n|\t/g, "") + "(document,docContent," + JSON.stringify(displayOptions) + "));return bootstrapStarted;}})();(" +
282
- getContent.toString().replace(/\n|\t/g, "") + ")().then(globalThis.bootstrap).then(() => document.dispatchEvent(new CustomEvent(\"single-file-display-infobar\"))).catch(()=>{});" +
302
+ getContent.toString().replace(/\n|\t/g, "") + ")().then(globalThis.bootstrap).then(() => document.dispatchEvent(new CustomEvent(\"single-file-display-infobar\"))).catch(error => {" +
303
+ "console.error(error);" +
304
+ "const waitMessage = document.getElementById(\"sfz-wait-message\");" +
305
+ "if (waitMessage) { waitMessage.remove(); }" +
306
+ "const errorMessage = document.getElementById(\"sfz-error-message\");" +
307
+ "if (errorMessage) { errorMessage.hidden = false; document.body.hidden = false; }" +
283
308
  "});" +
284
309
  "</script>";
285
310
  pageContent += script;
@@ -290,8 +315,9 @@ async function prependHTMLData(pageData, zipDataWriter, script, options) {
290
315
  }
291
316
  pageContent += extraData;
292
317
  const startTag = options.extractDataFromPageTags ? options.extractDataFromPageTags[0] : "<!--";
293
- pageContent += startTag;
294
- const extraDataOffset = startTag.length + extraData.length;
318
+ // the space guarantees a text node between <sfz-extra-data> and the start tag
319
+ pageContent += (extraData ? " " : "") + startTag;
320
+ const extraDataOffset = startTag.length + extraData.length + (extraData ? 1 : 0);
295
321
  await writeData(zipDataWriter.writable, (new TextEncoder()).encode(pageContent));
296
322
  return extraDataOffset;
297
323
  }
@@ -309,8 +335,8 @@ function getStartHTMLArray(pageData, options, startTag = "") {
309
335
  const htmlHeadData = getHTMLHeadData(pageData, options);
310
336
  let htmlArray;
311
337
  if (options.embeddedPdf) {
312
- const embeddedPdfText = options.embeddedPdf.reduce((text, charCode) => text + String.fromCharCode(charCode), "");
313
- const pdfTagIndex = EMBEDDED_DATA_REGEXPS.findIndex(tests => !embeddedPdfText.match(tests[1]));
338
+ const embeddedPdfText = TEXT_DECODER.decode(new Uint8Array(options.embeddedPdf));
339
+ const pdfTagIndex = EMBEDDED_DATA_REGEXPS.slice(0, -1).findIndex(tests => !embeddedPdfText.match(tests[1]));
314
340
  const [pdfStartTag, pdfEndTag] = pdfTagIndex == -1 ? ["", ""] : EMBEDDED_DATA_TAGS[pdfTagIndex];
315
341
  const htmlArray1 = new TextEncoder().encode(html + pdfStartTag);
316
342
  const htmlArray2 = new TextEncoder().encode(pdfEndTag + htmlHeadData + startTag);
@@ -341,7 +367,7 @@ function getHTMLHeadData(pageData, options) {
341
367
  const cspContent = "default-src 'none';connect-src 'self' data: blob:;font-src 'self' data: blob:;img-src 'self' data: blob:;style-src 'self' 'unsafe-inline' data: blob:;frame-src 'self' data: blob:;media-src 'self' data: blob:;script-src 'self' 'unsafe-inline' data: blob:;object-src 'self' data: blob:";
342
368
  pageContent += `<meta http-equiv=content-security-policy content=${JSON.stringify(cspContent)}>`;
343
369
  }
344
- pageContent += "<style>@keyframes display-wait-message{0%{opacity:0}100%{opacity:1}};body{color:transparent};div{color:initial}</style>";
370
+ pageContent += "<style>@keyframes display-wait-message{0%{opacity:0}100%{opacity:1}};body{color:transparent};div{color:initial}body>:not(#sfz-wait-message,#sfz-error-message){display:none}</style>";
345
371
  pageContent += "<body hidden>";
346
372
  return pageContent;
347
373
  }
@@ -352,7 +378,8 @@ function getPageTitle(pageData) {
352
378
 
353
379
  function findExtraDataTags(textContent, pageData, options, lastModDate, indexExtractDataFromPageTags = 0) {
354
380
  const regExpsTag = EXTRA_DATA_REGEXPS[indexExtractDataFromPageTags];
355
- const matchTag = textContent.match(regExpsTag[0]) || textContent.match(regExpsTag[1]);
381
+ const plaintextTag = EXTRA_DATA_TAGS[indexExtractDataFromPageTags][0] == "<plaintext>";
382
+ const matchTag = !plaintextTag && (textContent.match(regExpsTag[0]) || textContent.match(regExpsTag[1]));
356
383
  if (matchTag) {
357
384
  if (indexExtractDataFromPageTags < EXTRA_DATA_TAGS.length - 1) {
358
385
  return findExtraDataTags(textContent, pageData, options, lastModDate, indexExtractDataFromPageTags + 1);
@@ -362,6 +389,10 @@ function findExtraDataTags(textContent, pageData, options, lastModDate, indexExt
362
389
  }
363
390
  } else {
364
391
  options.extractDataFromPageTags = EXTRA_DATA_TAGS[indexExtractDataFromPageTags];
392
+ if (options.extractDataFromPageTags[0] == "<plaintext>") {
393
+ // <plaintext> cannot be closed, the file must end with the zip data
394
+ options.preventAppendedData = true;
395
+ }
365
396
  return process(pageData, options, lastModDate);
366
397
  }
367
398
  }
@@ -419,45 +450,105 @@ async function addFile(zipWriter, prefixName, data, disableCompresson) {
419
450
 
420
451
  async function getContent() {
421
452
  const BASE64_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
422
- const { Blob, XMLHttpRequest, document, stop } = globalThis;
453
+ const { Blob, XMLHttpRequest, document, zip, location } = globalThis;
423
454
  const characterMap = new Map([
424
455
  [65533, 0], [8364, 128], [8218, 130], [402, 131], [8222, 132], [8230, 133], [8224, 134], [8225, 135], [710, 136], [8240, 137],
425
456
  [352, 138], [8249, 139], [338, 140], [381, 142], [8216, 145], [8217, 146], [8220, 147], [8221, 148], [8226, 149], [8211, 150],
426
457
  [8212, 151], [732, 152], [8482, 153], [353, 154], [8250, 155], [339, 156], [382, 158], [376, 159]
427
458
  ]);
428
- const xhr = new XMLHttpRequest();
429
- document.body.querySelectorAll("meta, style").forEach(element => document.head.appendChild(element));
430
- xhr.responseType = "blob";
431
- xhr.open("GET", "");
459
+ const crc32Table = new Uint32Array(256).map((_, indexTable) => {
460
+ let crc = indexTable;
461
+ for (let indexBits = 0; indexBits < 8; indexBits++) {
462
+ crc = crc & 1 ? 0xEDB88320 ^ (crc >>> 1) : crc >>> 1;
463
+ }
464
+ return crc;
465
+ });
432
466
  return new Promise((resolve, reject) => {
433
- xhr.onerror = () => {
434
- extractPageData().then(resolve).catch(() => {
467
+ let aborted = false;
468
+ if (location.protocol == "file:") {
469
+ extractDataFromDocument();
470
+ } else {
471
+ getPageData();
472
+ }
473
+
474
+ async function extractDataFromDocument() {
475
+ try {
476
+ await waitForDocumentReady(document);
477
+ document.body.querySelectorAll("meta, style").forEach(element => document.head.appendChild(element));
478
+ const pageData = extractPageData();
479
+ displayMessage("sfz-wait-message", 2);
480
+ resolve(pageData);
481
+ } catch (error) {
482
+ console.error(error);
435
483
  displayMessage("sfz-error-message", 2);
436
- reject();
437
- });
438
- };
439
- xhr.send();
440
- xhr.onload = () => {
441
- stop();
442
- displayMessage("sfz-wait-message", 2);
443
- resolve(xhr.response);
444
- };
484
+ reject(error);
485
+ }
486
+ }
487
+
488
+ function getPageData() {
489
+ const xhr = new XMLHttpRequest();
490
+ xhr.responseType = "blob";
491
+ xhr.open("GET", "");
492
+ xhr.onerror = () => {
493
+ if (aborted) {
494
+ displayMessage("sfz-error-message", 2);
495
+ reject();
496
+ } else {
497
+ extractDataFromDocument();
498
+ }
499
+ };
500
+ xhr.send();
501
+ xhr.onreadystatechange = () => {
502
+ if (xhr.readyState === 2 && xhr.status === 200 && !aborted) {
503
+ aborted = true;
504
+ const httpRangeSupport = xhr.getResponseHeader("Accept-Ranges") === "bytes";
505
+ xhr.abort();
506
+ displayMessage("sfz-wait-message", 2);
507
+ if (httpRangeSupport) {
508
+ resolve(new zip.HttpRangeReader(location.href, {
509
+ useXHR: true,
510
+ combineSizeEocd: true
511
+ }));
512
+ } else {
513
+ getPageData();
514
+ }
515
+ }
516
+ };
517
+ if (aborted) {
518
+ xhr.onload = () => resolve(xhr.response);
519
+ }
520
+ }
445
521
  });
446
522
 
523
+ function waitForDocumentReady(document) {
524
+ return new Promise(resolve => {
525
+ if (document.readyState === "complete" || document.readyState === "interactive") {
526
+ resolve();
527
+ } else {
528
+ document.addEventListener("DOMContentLoaded", () => resolve());
529
+ }
530
+ });
531
+ }
532
+
447
533
  function displayMessage(elementId, delay = 0) {
448
534
  const element = document.getElementById(elementId);
449
535
  if (element) {
450
536
  Array.from(document.body.childNodes).forEach(node => {
451
537
  if (node.id != elementId) {
452
- node.remove();
538
+ if (node.id == "sfz-wait-message" || node.id == "sfz-error-message") {
539
+ node.hidden = true;
540
+ } else {
541
+ node.remove();
542
+ }
453
543
  }
454
544
  });
545
+ element.hidden = false;
455
546
  document.body.hidden = false;
456
547
  element.style = "opacity: 0; animation: 0s linear " + delay + "s display-wait-message 1 normal forwards";
457
548
  }
458
549
  }
459
550
 
460
- async function extractPageData() {
551
+ function extractPageData() {
461
552
  const zipDataElement = document.querySelector("sfz-extra-data");
462
553
  if (zipDataElement) {
463
554
  let dataNode = zipDataElement.nextSibling;
@@ -470,60 +561,46 @@ async function getContent() {
470
561
  } else {
471
562
  dataNode = zipDataElement.previousSibling;
472
563
  }
473
- const zipData = [];
474
- let { textContent } = dataNode;
475
- displayMessage("sfz-wait-message", 2);
564
+ const inflatedPayload = zip.inflateRaw(base64Decode(zipDataElement.textContent));
565
+ const payload = new Uint32Array(inflatedPayload.buffer, inflatedPayload.byteOffset, inflatedPayload.length >> 2);
566
+ const expectedCRC32 = payload[0];
567
+ const zipDataLength = payload[1];
568
+ const lfCodesLength = payload[2];
569
+ const zipData = new Uint8Array(zipDataLength);
570
+ const { textContent } = dataNode;
571
+ let offset = 0;
572
+ let indexLFCode = 0;
573
+ let crc32 = -1;
476
574
  for (let index = 0; index < textContent.length; index++) {
477
575
  const charCode = textContent.charCodeAt(index);
478
- zipData.push(charCode > 255 ? characterMap.get(charCode) : charCode);
479
- }
480
- const payload = new Uint32Array(decompress(zipDataElement.textContent).buffer);
481
- const insertionsCRLFLength = payload[0];
482
- const insertionsCRLF = payload.slice(1, 1 + insertionsCRLFLength);
483
- const substitutionsLFLength = payload[1 + insertionsCRLFLength];
484
- const substitutionsLF = payload.slice(2 + insertionsCRLFLength, 2 + insertionsCRLFLength + substitutionsLFLength);
485
- insertionsCRLF.forEach(index => zipData.splice(index, 1, 13, 10));
486
- substitutionsLF.forEach(index => zipData[index] = 13);
487
- return new Blob([new Uint8Array(zipData)], { type: "application/octet-stream" });
488
- }
489
- throw new Error("Extra zip data data not found");
490
- }
491
-
492
- function decompress(src) {
493
- src = base64Decode(src);
494
- let out = new Uint8Array(1024);
495
- let outLen = 0;
496
- for (let i = 0; i < src.length;) {
497
- const ctrl = src[i++];
498
- if ((ctrl & 0x80) === 0) {
499
- const L = ctrl;
500
- ensure(outLen + L);
501
- for (let j = 0; j < L && i < src.length; j++) {
502
- out[outLen++] = src[i++];
503
- }
504
- } else {
505
- const L = (ctrl & 0x7f) + 3;
506
- const off = (src[i++] << 8) | src[i++];
507
- const start = outLen - off;
508
- ensure(outLen + L);
509
- for (let k = 0; k < L; k++) {
510
- out[outLen++] = out[start + k];
576
+ if (charCode == 10) {
577
+ const lfCode = (payload[3 + (indexLFCode >> 4)] >>> ((indexLFCode & 15) * 2)) & 3;
578
+ indexLFCode++;
579
+ if (lfCode == 0) {
580
+ writeByte(10);
581
+ } else {
582
+ writeByte(13);
583
+ if (lfCode == 2) {
584
+ writeByte(10);
585
+ }
586
+ }
587
+ } else {
588
+ writeByte(charCode > 255 ? characterMap.get(charCode) : charCode);
511
589
  }
512
590
  }
513
- }
514
- return new Uint8Array(out.buffer.slice(0, outLen));
591
+ crc32 = (crc32 ^ -1) >>> 0;
592
+ if (offset != zipDataLength || indexLFCode != lfCodesLength || crc32 != expectedCRC32) {
593
+ throw new Error("Invalid checksum of the extracted zip data");
594
+ }
595
+ return new Blob([zipData], { type: "application/octet-stream" });
515
596
 
516
- function ensure(n) {
517
- if (out.length < n) {
518
- let nl = out.length * 2;
519
- while (nl < n) {
520
- nl *= 2;
521
- }
522
- const nbuf = new Uint8Array(nl);
523
- nbuf.set(out.subarray(0, outLen));
524
- out = nbuf;
597
+ function writeByte(byte) {
598
+ zipData[offset] = byte;
599
+ crc32 = (crc32 >>> 8) ^ crc32Table[(crc32 ^ byte) & 0xff];
600
+ offset++;
525
601
  }
526
- };
602
+ }
603
+ throw new Error("Extra zip data data not found");
527
604
  }
528
605
 
529
606
  function base64Decode(b64) {
@@ -568,94 +645,3 @@ function base64Encode(bytes) {
568
645
  }
569
646
  return out;
570
647
  }
571
-
572
- function compress(input) {
573
- const src = new Uint8Array(input);
574
- const N = src.length;
575
- const out = [];
576
- const litBuf = [];
577
- const MAX_OFFSET = 0xffff;
578
- const MAX_MATCH = 130;
579
- const MAX_CANDIDATES = 64;
580
- const map = new Map();
581
- let i = 0;
582
- while (i < N) {
583
- let bestLen = 0, bestOff = 0;
584
- if (i + 2 < N) {
585
- const key = (src[i] << 16) | (src[i + 1] << 8) | src[i + 2];
586
- const cand = map.get(key) || [];
587
- for (let c = cand.length - 1; c >= 0; c--) {
588
- const j = cand[c];
589
- const off = i - j;
590
- if (off <= 0 || off > MAX_OFFSET) {
591
- continue;
592
- }
593
- let k = 0;
594
- while (k < MAX_MATCH && i + k < N && src[j + k] === src[i + k]) {
595
- k++;
596
- }
597
- if (k > bestLen && k >= 3) {
598
- bestLen = k; bestOff = off;
599
- if (bestLen === MAX_MATCH) {
600
- break;
601
- }
602
- }
603
- }
604
- }
605
-
606
- if (bestLen >= 3) {
607
- if (litBuf.length) {
608
- flushLiterals();
609
- }
610
- let remain = bestLen;
611
- let produced = 0;
612
- while (remain > 0) {
613
- const take = Math.min(remain, MAX_MATCH);
614
- out.push(0x80 | ((take - 3) & 0x7f));
615
- out.push((bestOff >> 8) & 0xff);
616
- out.push(bestOff & 0xff);
617
- remain -= take;
618
- produced += take;
619
- }
620
- const start = i;
621
- for (let p = start; p < start + produced; p++) {
622
- addPos(p);
623
- }
624
- i += produced;
625
- } else {
626
- litBuf.push(src[i]);
627
- addPos(i);
628
- i++;
629
- if (litBuf.length === 127) {
630
- flushLiterals();
631
- }
632
- }
633
- }
634
- if (litBuf.length) {
635
- flushLiterals();
636
- }
637
- const u8 = new Uint8Array(out);
638
- return base64Encode(u8);
639
-
640
- function flushLiterals() {
641
- while (litBuf.length) {
642
- const take = Math.min(127, litBuf.length);
643
- out.push(take);
644
- for (let t = 0; t < take; t++) {
645
- out.push(litBuf.shift());
646
- }
647
- }
648
- }
649
-
650
- function addPos(pos) {
651
- if (pos + 2 < N) {
652
- const key = (src[pos] << 16) | (src[pos + 1] << 8) | src[pos + 2];
653
- const arr = map.get(key) || [];
654
- arr.push(pos);
655
- if (arr.length > MAX_CANDIDATES) {
656
- arr.shift();
657
- }
658
- map.set(key, arr);
659
- }
660
- }
661
- }