single-file-core 1.5.92 → 1.5.93

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-core",
3
- "version": "1.5.92",
3
+ "version": "1.5.93",
4
4
  "description": "SingleFile Core",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
@@ -86,6 +86,12 @@ const CRC32_TABLE = new Uint32Array(256).map((_, indexTable) => {
86
86
  const PNG_IEND_LENGTH = 12;
87
87
  const PNG_SIGNATURE_LENGTH = 8;
88
88
  const PNG_IHDR_LENGTH = 25;
89
+ const PDF_ENTRY_FILENAME = "page.pdf";
90
+ const LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50;
91
+ const CENTRAL_FILE_HEADER_SIGNATURE = 0x02014b50;
92
+ const END_OF_CENTRAL_DIR_SIGNATURE = 0x06054b50;
93
+ const ZIP64_END_OF_CENTRAL_DIR_SIGNATURE = 0x06064b50;
94
+ const ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE = 0x07064b50;
89
95
 
90
96
  const browser = globalThis.browser;
91
97
 
@@ -112,7 +118,7 @@ async function process(pageData, options, lastModDate = new Date()) {
112
118
  async function createArchive(pageData, options, script, writeEntries, lastModDate = new Date()) {
113
119
  const zipDataWriter = new Uint8ArrayWriter();
114
120
  zipDataWriter.init();
115
- let extraDataOffset, extraData, embeddedImageDataOffset, endTag;
121
+ let extraDataOffset, extraData, embeddedImageDataOffset, endTag, pdfEntry;
116
122
  if (options.embeddedImage) {
117
123
  options.embeddedImage = new Uint8Array(options.embeddedImage);
118
124
  const embeddedImageData = options.embeddedImage.slice(PNG_SIGNATURE_LENGTH + PNG_IHDR_LENGTH, options.embeddedImage.length - PNG_IEND_LENGTH);
@@ -122,7 +128,13 @@ async function createArchive(pageData, options, script, writeEntries, lastModDat
122
128
  const tagIndex = EMBEDDED_DATA_REGEXPS.slice(0, -1).findIndex(tests => !embeddedImageText.match(tests[1]));
123
129
  let startTag;
124
130
  [startTag, endTag] = tagIndex == -1 ? ["", ""] : EMBEDDED_DATA_TAGS[tagIndex];
125
- const htmlArray = getStartHTMLArray(pageData, options, startTag);
131
+ const startHTMLData = getStartHTMLArray(pageData, options, lastModDate, startTag);
132
+ const htmlArray = startHTMLData.htmlArray;
133
+ if (startHTMLData.pdfEntry) {
134
+ pdfEntry = startHTMLData.pdfEntry;
135
+ // the htmlArray starts after the 4-byte length and the 8-byte type of the tEXt chunk
136
+ pdfEntry.offset += zipDataWriter.offset + 12;
137
+ }
126
138
  const hmtlData = new Uint8Array([...getLength(htmlArray.length + 4), ...[0x74, 0x45, 0x58, 0x74, 0x50, 0x4e, 0x47, 0], ...htmlArray]);
127
139
  await writeData(zipDataWriter.writable, hmtlData);
128
140
  await writeData(zipDataWriter.writable, getCRC32(hmtlData, 4));
@@ -140,7 +152,9 @@ async function createArchive(pageData, options, script, writeEntries, lastModDat
140
152
  }
141
153
  }
142
154
  if (options.selfExtractingArchive) {
143
- extraDataOffset = await prependHTMLData(pageData, zipDataWriter, script, options);
155
+ const prependedData = await prependHTMLData(pageData, zipDataWriter, script, options, lastModDate);
156
+ extraDataOffset = prependedData.extraDataOffset;
157
+ pdfEntry = pdfEntry || prependedData.pdfEntry;
144
158
  } else if (!options.embeddedImage && options.embeddedPdf) {
145
159
  await writeData(zipDataWriter.writable, new Uint8Array(options.embeddedPdf));
146
160
  }
@@ -151,7 +165,17 @@ async function createArchive(pageData, options, script, writeEntries, lastModDat
151
165
  const startOffset = zipDataWriter.offset;
152
166
  const zipWriter = new ZipWriter({ writable: zipDataWriter.writable, size: startOffset }, { bufferedWrite: true, keepOrder: true, lastModDate, useCompressionStream: true });
153
167
  await writeEntries(zipWriter);
168
+ if (pdfEntry) {
169
+ // the record is written where the central directory will start so that the PDF is listed
170
+ // first; the ZipWriter is unaware of these bytes, so the central directory offset it
171
+ // stores in the end of central directory record points here
172
+ new DataView(pdfEntry.centralRecord.buffer).setUint32(42, pdfEntry.offset, true);
173
+ await writeData(zipDataWriter.writable, pdfEntry.centralRecord);
174
+ }
154
175
  await zipWriter.close(undefined, { preventClose: true });
176
+ if (pdfEntry) {
177
+ patchEndOfCentralDirectory(zipDataWriter, pdfEntry.centralRecord.length);
178
+ }
155
179
  const data = zipDataWriter.getData();
156
180
  if (options.selfExtractingArchive) {
157
181
  const lfCodes = [];
@@ -248,13 +272,85 @@ async function createArchive(pageData, options, script, writeEntries, lastModDat
248
272
 
249
273
  function getCRC32(data, indexData = 0) {
250
274
  const crcArray = new Uint8Array(4);
275
+ setUint32(crcArray, getCRC32Value(data, indexData));
276
+ return crcArray;
277
+ }
278
+
279
+ function getCRC32Value(data, indexData = 0) {
251
280
  let crc = -1;
252
281
  for (; indexData < data.length; indexData++) {
253
282
  crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ data[indexData]) & 0xff];
254
283
  }
255
- crc ^= -1;
256
- setUint32(crcArray, crc);
257
- return crcArray;
284
+ return (crc ^ -1) >>> 0;
285
+ }
286
+
287
+ function getPDFEntry(embeddedPdf, lastModDate = new Date()) {
288
+ const filename = new TextEncoder().encode(PDF_ENTRY_FILENAME);
289
+ const crc32 = getCRC32Value(embeddedPdf);
290
+ const dosTime = (lastModDate.getHours() << 11) | (lastModDate.getMinutes() << 5) | (lastModDate.getSeconds() >> 1);
291
+ const dosDate = (Math.max(0, lastModDate.getFullYear() - 1980) << 9) | ((lastModDate.getMonth() + 1) << 5) | lastModDate.getDate();
292
+ const localHeader = new Uint8Array(30 + filename.length);
293
+ const localHeaderView = new DataView(localHeader.buffer);
294
+ localHeaderView.setUint32(0, LOCAL_FILE_HEADER_SIGNATURE, true);
295
+ localHeaderView.setUint16(4, 20, true);
296
+ localHeaderView.setUint16(10, dosTime, true);
297
+ localHeaderView.setUint16(12, dosDate, true);
298
+ localHeaderView.setUint32(14, crc32, true);
299
+ localHeaderView.setUint32(18, embeddedPdf.length, true);
300
+ localHeaderView.setUint32(22, embeddedPdf.length, true);
301
+ localHeaderView.setUint16(26, filename.length, true);
302
+ localHeader.set(filename, 30);
303
+ const centralRecord = new Uint8Array(46 + filename.length);
304
+ const centralRecordView = new DataView(centralRecord.buffer);
305
+ centralRecordView.setUint32(0, CENTRAL_FILE_HEADER_SIGNATURE, true);
306
+ centralRecordView.setUint16(4, 0x0300, true);
307
+ centralRecordView.setUint16(6, 20, true);
308
+ centralRecordView.setUint16(12, dosTime, true);
309
+ centralRecordView.setUint16(14, dosDate, true);
310
+ centralRecordView.setUint32(16, crc32, true);
311
+ centralRecordView.setUint32(20, embeddedPdf.length, true);
312
+ centralRecordView.setUint32(24, embeddedPdf.length, true);
313
+ centralRecordView.setUint16(28, filename.length, true);
314
+ centralRecordView.setUint32(38, 0o100644 << 16, true);
315
+ centralRecord.set(filename, 46);
316
+ return { localHeader, centralRecord };
317
+ }
318
+
319
+ function patchEndOfCentralDirectory(zipDataWriter, centralRecordLength) {
320
+ const view = new DataView(zipDataWriter.array.buffer);
321
+ const offsetEOCD = zipDataWriter.offset - 22;
322
+ if (view.getUint32(offsetEOCD, true) != END_OF_CENTRAL_DIR_SIGNATURE) {
323
+ return;
324
+ }
325
+ const entriesOnDisk = view.getUint16(offsetEOCD + 8, true);
326
+ const totalEntries = view.getUint16(offsetEOCD + 10, true);
327
+ const centralDirectorySize = view.getUint32(offsetEOCD + 12, true);
328
+ const offsetLocator = offsetEOCD - 20;
329
+ let offsetZip64EOCD;
330
+ if (offsetLocator >= 0 && view.getUint32(offsetLocator, true) == ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE) {
331
+ // the offset stored in the locator does not account for the PDF record either
332
+ offsetZip64EOCD = Number(view.getBigUint64(offsetLocator + 8, true)) + centralRecordLength;
333
+ if (view.getUint32(offsetZip64EOCD, true) != ZIP64_END_OF_CENTRAL_DIR_SIGNATURE) {
334
+ return;
335
+ }
336
+ } else if (entriesOnDisk + 1 >= 0xFFFF || totalEntries + 1 >= 0xFFFF || centralDirectorySize + centralRecordLength >= 0xFFFFFFFF) {
337
+ return;
338
+ }
339
+ if (entriesOnDisk != 0xFFFF) {
340
+ view.setUint16(offsetEOCD + 8, entriesOnDisk + 1, true);
341
+ }
342
+ if (totalEntries != 0xFFFF) {
343
+ view.setUint16(offsetEOCD + 10, totalEntries + 1, true);
344
+ }
345
+ if (centralDirectorySize != 0xFFFFFFFF) {
346
+ view.setUint32(offsetEOCD + 12, centralDirectorySize + centralRecordLength, true);
347
+ }
348
+ if (offsetZip64EOCD !== undefined) {
349
+ view.setBigUint64(offsetLocator + 8, BigInt(offsetZip64EOCD), true);
350
+ view.setBigUint64(offsetZip64EOCD + 24, view.getBigUint64(offsetZip64EOCD + 24, true) + 1n, true);
351
+ view.setBigUint64(offsetZip64EOCD + 32, view.getBigUint64(offsetZip64EOCD + 32, true) + 1n, true);
352
+ view.setBigUint64(offsetZip64EOCD + 40, view.getBigUint64(offsetZip64EOCD + 40, true) + BigInt(centralRecordLength), true);
353
+ }
258
354
  }
259
355
 
260
356
  function getLength(length) {
@@ -270,10 +366,16 @@ function setUint32(data, value) {
270
366
  data[3] = value;
271
367
  }
272
368
 
273
- async function prependHTMLData(pageData, zipDataWriter, script, options) {
369
+ async function prependHTMLData(pageData, zipDataWriter, script, options, lastModDate) {
274
370
  let pageContent = "";
371
+ let pdfEntry;
275
372
  if (!options.embeddedImage) {
276
- await writeData(zipDataWriter.writable, getStartHTMLArray(pageData, options));
373
+ const startHTMLData = getStartHTMLArray(pageData, options, lastModDate);
374
+ if (startHTMLData.pdfEntry) {
375
+ pdfEntry = startHTMLData.pdfEntry;
376
+ pdfEntry.offset += zipDataWriter.offset;
377
+ }
378
+ await writeData(zipDataWriter.writable, startHTMLData.htmlArray);
277
379
  }
278
380
  pageContent += "<div id=sfz-wait-message>Please wait...</div>";
279
381
  if (options.extractDataFromPage) {
@@ -341,10 +443,10 @@ async function prependHTMLData(pageData, zipDataWriter, script, options) {
341
443
  pageContent += (extraData ? " " : "") + startTag;
342
444
  const extraDataOffset = startTag.length + extraData.length + (extraData ? 1 : 0);
343
445
  await writeData(zipDataWriter.writable, (new TextEncoder()).encode(pageContent));
344
- return extraDataOffset;
446
+ return { extraDataOffset, pdfEntry };
345
447
  }
346
448
 
347
- function getStartHTMLArray(pageData, options, startTag = "") {
449
+ function getStartHTMLArray(pageData, options, lastModDate, startTag = "") {
348
450
  let html = "";
349
451
  if (options.includeBOM && !options.extractDataFromPage && !options.embeddedImage) {
350
452
  html += "\ufeff";
@@ -355,21 +457,25 @@ function getStartHTMLArray(pageData, options, startTag = "") {
355
457
  const charset = options.extractDataFromPage ? "windows-1252" : "utf-8";
356
458
  html += "<meta charset=" + charset + ">";
357
459
  const htmlHeadData = getHTMLHeadData(pageData, options);
358
- let htmlArray;
460
+ let htmlArray, pdfEntry;
359
461
  if (options.embeddedPdf) {
360
- const embeddedPdfText = TEXT_DECODER.decode(new Uint8Array(options.embeddedPdf));
462
+ const embeddedPdf = new Uint8Array(options.embeddedPdf);
463
+ pdfEntry = getPDFEntry(embeddedPdf, lastModDate);
464
+ const embeddedPdfText = TEXT_DECODER.decode(pdfEntry.localHeader) + TEXT_DECODER.decode(embeddedPdf);
361
465
  const pdfTagIndex = EMBEDDED_DATA_REGEXPS.slice(0, -1).findIndex(tests => !embeddedPdfText.match(tests[1]));
362
466
  const [pdfStartTag, pdfEndTag] = pdfTagIndex == -1 ? ["", ""] : EMBEDDED_DATA_TAGS[pdfTagIndex];
363
467
  const htmlArray1 = new TextEncoder().encode(html + pdfStartTag);
364
468
  const htmlArray2 = new TextEncoder().encode(pdfEndTag + htmlHeadData + startTag);
365
- htmlArray = new Uint8Array(htmlArray1.length + htmlArray2.length + options.embeddedPdf.length);
469
+ htmlArray = new Uint8Array(htmlArray1.length + pdfEntry.localHeader.length + embeddedPdf.length + htmlArray2.length);
366
470
  htmlArray.set(htmlArray1);
367
- htmlArray.set(options.embeddedPdf, htmlArray1.length);
368
- htmlArray.set(htmlArray2, htmlArray1.length + options.embeddedPdf.length);
471
+ htmlArray.set(pdfEntry.localHeader, htmlArray1.length);
472
+ htmlArray.set(embeddedPdf, htmlArray1.length + pdfEntry.localHeader.length);
473
+ htmlArray.set(htmlArray2, htmlArray1.length + pdfEntry.localHeader.length + embeddedPdf.length);
474
+ pdfEntry.offset = htmlArray1.length;
369
475
  } else {
370
476
  htmlArray = new TextEncoder().encode(html + htmlHeadData + startTag);
371
477
  }
372
- return htmlArray;
478
+ return { htmlArray, pdfEntry };
373
479
  }
374
480
 
375
481
  function getHTMLHeadData(pageData, options) {