@clearmist-labs/comic-archive-handler 1.1.1 → 1.2.0
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/dist/index.d.mts +27 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +193 -76
- package/dist/index.mjs.map +1 -1
- package/docs/API.md +16 -0
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -480,12 +480,13 @@ export declare function sha256Archive(input: ArchiveInput): Promise<string>;
|
|
|
480
480
|
* ComicInfo.xml/MetronInfo.xml) untouched.
|
|
481
481
|
*
|
|
482
482
|
* This requires two passes over the archive: `listEntries` is called once to
|
|
483
|
-
* collect image paths (metadata only
|
|
483
|
+
* collect image paths (metadata only, no content is read, so no
|
|
484
484
|
* decompression work is wasted) to compute the sort-order rename map, then
|
|
485
|
-
* called again to stream entries out under their new names.
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
485
|
+
* called again to stream entries out under their new names. `listEntries` is
|
|
486
|
+
* re-invoked from the start for the second pass rather than reusing entry
|
|
487
|
+
* objects collected in the first, since the sequential/CLI-driven adapters
|
|
488
|
+
* (rar, 7z, ace, tar) can't be "rewound" mid-read even though zip and asar
|
|
489
|
+
* could serve both passes from the entries collected the first time round.
|
|
489
490
|
*/
|
|
490
491
|
export declare function renameArchiveImagesSequentially(input: ArchiveInput, options?: RenameOptions): Promise<Buffer | void>;
|
|
491
492
|
//#endregion
|
|
@@ -498,6 +499,26 @@ export declare function listArchiveFiles(input: ArchiveInput): Promise<string[]>
|
|
|
498
499
|
//#region src/readArchiveEntry.d.ts
|
|
499
500
|
export declare function readArchiveEntry(input: ArchiveInput, entryPath: string): Promise<Buffer>;
|
|
500
501
|
//#endregion
|
|
502
|
+
//#region src/readArchiveEntries.d.ts
|
|
503
|
+
export interface ReadArchiveEntriesResult {
|
|
504
|
+
path: string;
|
|
505
|
+
size?: number;
|
|
506
|
+
buffer: Buffer;
|
|
507
|
+
sha256: string;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Reads every entry's bytes and SHA256 in a single pass over the archive.
|
|
511
|
+
* Calling `readArchiveEntry`/`sha256ArchiveEntry` once per entry instead
|
|
512
|
+
* means re-running `listEntries` and re-locating the target entry on every
|
|
513
|
+
* call — cheap for zip (backed by real central-directory random access) and
|
|
514
|
+
* tar/asar, but for the sequential/CLI-driven formats (rar, 7z, ace) each
|
|
515
|
+
* call re-reads the archive from the start, so reading every entry that way
|
|
516
|
+
* costs O(n^2). This also does half the I/O of calling both
|
|
517
|
+
* `sha256ArchiveEntry` and `readArchiveEntry` per entry, since it computes
|
|
518
|
+
* the hash and buffer from the same read instead of two.
|
|
519
|
+
*/
|
|
520
|
+
export declare function readArchiveEntries(input: ArchiveInput): AsyncGenerator<ReadArchiveEntriesResult>;
|
|
521
|
+
//#endregion
|
|
501
522
|
//#region src/benchmark/benchmarkArchive.d.ts
|
|
502
523
|
/**
|
|
503
524
|
* Extracts a comic archive, validates it contains image files, then
|
|
@@ -537,6 +558,7 @@ declare const comicArchiveHandler: {
|
|
|
537
558
|
stripNonEssentialFiles: typeof stripNonEssentialFiles;
|
|
538
559
|
listArchiveFiles: typeof listArchiveFiles;
|
|
539
560
|
readArchiveEntry: typeof readArchiveEntry;
|
|
561
|
+
readArchiveEntries: typeof readArchiveEntries;
|
|
540
562
|
benchmarkArchive: typeof benchmarkArchive;
|
|
541
563
|
renderBenchmarkReportMarkdown: typeof renderBenchmarkReportMarkdown;
|
|
542
564
|
WRITABLE_ARCHIVE_TYPES: readonly WritableArchiveType[];
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/benchmark/types.ts","../src/errors.ts","../src/detect.ts","../src/convertArchive.ts","../src/extractArchive.ts","../src/metadata/comicInfo.ts","../src/metadata/metronInfo.ts","../src/metadata/validate.ts","../src/metadata/schema.ts","../src/metadata/index.ts","../src/images/convert.ts","../src/images/phash.ts","../src/images/isImage.ts","../src/hashing/sha256.ts","../src/rename.ts","../src/strip.ts","../src/listFiles.ts","../src/readArchiveEntry.ts","../src/benchmark/benchmarkArchive.ts","../src/benchmark/report.ts","../src/index.ts"],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/benchmark/types.ts","../src/errors.ts","../src/detect.ts","../src/convertArchive.ts","../src/extractArchive.ts","../src/metadata/comicInfo.ts","../src/metadata/metronInfo.ts","../src/metadata/validate.ts","../src/metadata/schema.ts","../src/metadata/index.ts","../src/images/convert.ts","../src/images/phash.ts","../src/images/isImage.ts","../src/hashing/sha256.ts","../src/rename.ts","../src/strip.ts","../src/listFiles.ts","../src/readArchiveEntry.ts","../src/readArchiveEntries.ts","../src/benchmark/benchmarkArchive.ts","../src/benchmark/report.ts","../src/index.ts"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import * as fs from "node:fs";
|
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import * as fsp from "node:fs/promises";
|
|
6
6
|
import { PassThrough, Readable } from "node:stream";
|
|
7
|
-
import {
|
|
7
|
+
import { Zip, ZipDeflate, inflateSync } from "fflate";
|
|
8
8
|
import * as tarStream from "tar-stream";
|
|
9
9
|
import { createExtractorFromData } from "node-unrar-js";
|
|
10
10
|
import * as path from "node:path";
|
|
@@ -199,86 +199,163 @@ async function isAce(input) {
|
|
|
199
199
|
return await detectArchiveType(input) === "ace";
|
|
200
200
|
}
|
|
201
201
|
//#endregion
|
|
202
|
-
//#region src/internal/
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
202
|
+
//#region src/internal/zipCentralDirectory.ts
|
|
203
|
+
const EOCD_SIGNATURE = 101010256;
|
|
204
|
+
const ZIP64_EOCD_LOCATOR_SIGNATURE = 117853008;
|
|
205
|
+
const ZIP64_EOCD_SIGNATURE = 101075792;
|
|
206
|
+
const CENTRAL_DIRECTORY_SIGNATURE = 33639248;
|
|
207
|
+
const LOCAL_FILE_HEADER_SIGNATURE = 67324752;
|
|
208
|
+
const ZIP64_EXTRA_FIELD_ID = 1;
|
|
209
|
+
const EOCD_FIXED_SIZE = 22;
|
|
210
|
+
const EOCD_MAX_COMMENT_SIZE = 65535;
|
|
211
|
+
const ZIP64_EOCD_LOCATOR_SIZE = 20;
|
|
212
|
+
const UINT32_MAX = 4294967295;
|
|
213
|
+
const UINT16_MAX = 65535;
|
|
214
|
+
/**
|
|
215
|
+
* The end-of-central-directory record sits at the end of the file, after an
|
|
216
|
+
* optional variable-length comment, so it's found by scanning backward from
|
|
217
|
+
* the end for its signature rather than reading forward from the start.
|
|
218
|
+
*/
|
|
219
|
+
async function findEndOfCentralDirectory(input) {
|
|
220
|
+
const size = await inputSize(input);
|
|
221
|
+
const searchStart = Math.max(0, size - EOCD_FIXED_SIZE - EOCD_MAX_COMMENT_SIZE);
|
|
222
|
+
const tail = await readInputRange(input, searchStart, size);
|
|
223
|
+
for (let i = tail.length - EOCD_FIXED_SIZE; i >= 0; i--) if (tail.readUInt32LE(i) === EOCD_SIGNATURE) return {
|
|
224
|
+
record: tail.subarray(i),
|
|
225
|
+
offset: searchStart + i
|
|
226
|
+
};
|
|
227
|
+
throw new ArchiveFormatError("Not a valid zip archive: no end-of-central-directory record found.");
|
|
207
228
|
}
|
|
208
|
-
/**
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Zip64 stores the real (64-bit) totals in its own record, pointed to by a
|
|
231
|
+
* fixed-size locator that sits immediately before the regular
|
|
232
|
+
* end-of-central-directory record.
|
|
233
|
+
*/
|
|
234
|
+
async function readZip64EndOfCentralDirectory(input, eocdOffset) {
|
|
235
|
+
const locatorOffset = eocdOffset - ZIP64_EOCD_LOCATOR_SIZE;
|
|
236
|
+
if (locatorOffset < 0) throw new ArchiveFormatError("Not a valid zip64 archive: missing end-of-central-directory locator.");
|
|
237
|
+
const locator = await readInputRange(input, locatorOffset, locatorOffset + ZIP64_EOCD_LOCATOR_SIZE);
|
|
238
|
+
if (locator.length < ZIP64_EOCD_LOCATOR_SIZE || locator.readUInt32LE(0) !== ZIP64_EOCD_LOCATOR_SIGNATURE) throw new ArchiveFormatError("Not a valid zip64 archive: missing end-of-central-directory locator.");
|
|
239
|
+
const zip64EocdOffset = Number(locator.readBigUInt64LE(8));
|
|
240
|
+
const record = await readInputRange(input, zip64EocdOffset, zip64EocdOffset + 56);
|
|
241
|
+
if (record.length < 56 || record.readUInt32LE(0) !== ZIP64_EOCD_SIGNATURE) throw new ArchiveFormatError("Not a valid zip64 archive: malformed end-of-central-directory record.");
|
|
242
|
+
return {
|
|
243
|
+
totalEntries: Number(record.readBigUInt64LE(32)),
|
|
244
|
+
centralDirectorySize: Number(record.readBigUInt64LE(40)),
|
|
245
|
+
centralDirectoryOffset: Number(record.readBigUInt64LE(48))
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/** Reads the zip64 extra field's 8-byte values for whichever fixed-header fields overflowed 32 bits, in the fixed order the spec requires: uncompressed size, compressed size, local header offset. */
|
|
249
|
+
function readZip64ExtraField(extra, overflowed) {
|
|
250
|
+
let pos = 0;
|
|
251
|
+
while (pos + 4 <= extra.length) {
|
|
252
|
+
const fieldId = extra.readUInt16LE(pos);
|
|
253
|
+
const fieldSize = extra.readUInt16LE(pos + 2);
|
|
254
|
+
if (fieldId === ZIP64_EXTRA_FIELD_ID) {
|
|
255
|
+
let valuePos = pos + 4;
|
|
256
|
+
const result = {};
|
|
257
|
+
if (overflowed.uncompressedSize && valuePos + 8 <= extra.length) {
|
|
258
|
+
result.uncompressedSize = Number(extra.readBigUInt64LE(valuePos));
|
|
259
|
+
valuePos += 8;
|
|
232
260
|
}
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
261
|
+
if (overflowed.compressedSize && valuePos + 8 <= extra.length) {
|
|
262
|
+
result.compressedSize = Number(extra.readBigUInt64LE(valuePos));
|
|
263
|
+
valuePos += 8;
|
|
264
|
+
}
|
|
265
|
+
if (overflowed.localHeaderOffset && valuePos + 8 <= extra.length) {
|
|
266
|
+
result.localHeaderOffset = Number(extra.readBigUInt64LE(valuePos));
|
|
267
|
+
valuePos += 8;
|
|
268
|
+
}
|
|
269
|
+
return result;
|
|
239
270
|
}
|
|
271
|
+
pos += 4 + fieldSize;
|
|
240
272
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
273
|
+
throw new ArchiveFormatError("Not a valid zip64 archive: an oversized entry is missing its zip64 extra field.");
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Parses a zip's central directory — the entry index at the end of the
|
|
277
|
+
* file — into per-entry metadata (name, sizes, compression method, and the
|
|
278
|
+
* offset of its local file header). Reading this index costs a couple of
|
|
279
|
+
* small range reads regardless of archive size; it never reads or
|
|
280
|
+
* decompresses entry data itself, which is why entries can then be read in
|
|
281
|
+
* any order at O(1) cost each rather than needing a forward scan through
|
|
282
|
+
* every entry that precedes the one wanted.
|
|
283
|
+
*/
|
|
284
|
+
async function parseZipCentralDirectory(input) {
|
|
285
|
+
const { record: eocd, offset: eocdOffset } = await findEndOfCentralDirectory(input);
|
|
286
|
+
let totalEntries = eocd.readUInt16LE(10);
|
|
287
|
+
let centralDirectorySize = eocd.readUInt32LE(12);
|
|
288
|
+
let centralDirectoryOffset = eocd.readUInt32LE(16);
|
|
289
|
+
if (totalEntries === UINT16_MAX || centralDirectorySize === UINT32_MAX || centralDirectoryOffset === UINT32_MAX) ({totalEntries, centralDirectorySize, centralDirectoryOffset} = await readZip64EndOfCentralDirectory(input, eocdOffset));
|
|
290
|
+
const centralDirectory = await readInputRange(input, centralDirectoryOffset, centralDirectoryOffset + centralDirectorySize);
|
|
291
|
+
const entries = [];
|
|
292
|
+
let pos = 0;
|
|
293
|
+
for (let i = 0; i < totalEntries; i++) {
|
|
294
|
+
if (pos + 46 > centralDirectory.length || centralDirectory.readUInt32LE(pos) !== CENTRAL_DIRECTORY_SIGNATURE) throw new ArchiveFormatError("Not a valid zip archive: malformed central directory entry.");
|
|
295
|
+
const compressionMethod = centralDirectory.readUInt16LE(pos + 10);
|
|
296
|
+
let compressedSize = centralDirectory.readUInt32LE(pos + 20);
|
|
297
|
+
let uncompressedSize = centralDirectory.readUInt32LE(pos + 24);
|
|
298
|
+
const nameLength = centralDirectory.readUInt16LE(pos + 28);
|
|
299
|
+
const extraLength = centralDirectory.readUInt16LE(pos + 30);
|
|
300
|
+
const commentLength = centralDirectory.readUInt16LE(pos + 32);
|
|
301
|
+
let localHeaderOffset = centralDirectory.readUInt32LE(pos + 42);
|
|
302
|
+
const name = centralDirectory.toString("utf8", pos + 46, pos + 46 + nameLength);
|
|
303
|
+
const overflowed = {
|
|
304
|
+
uncompressedSize: uncompressedSize === UINT32_MAX,
|
|
305
|
+
compressedSize: compressedSize === UINT32_MAX,
|
|
306
|
+
localHeaderOffset: localHeaderOffset === UINT32_MAX
|
|
307
|
+
};
|
|
308
|
+
if (overflowed.uncompressedSize || overflowed.compressedSize || overflowed.localHeaderOffset) {
|
|
309
|
+
const zip64Values = readZip64ExtraField(centralDirectory.subarray(pos + 46 + nameLength, pos + 46 + nameLength + extraLength), overflowed);
|
|
310
|
+
uncompressedSize = zip64Values.uncompressedSize ?? uncompressedSize;
|
|
311
|
+
compressedSize = zip64Values.compressedSize ?? compressedSize;
|
|
312
|
+
localHeaderOffset = zip64Values.localHeaderOffset ?? localHeaderOffset;
|
|
260
313
|
}
|
|
261
|
-
|
|
314
|
+
if (!name.endsWith("/")) entries.push({
|
|
315
|
+
path: name,
|
|
316
|
+
compressedSize,
|
|
317
|
+
uncompressedSize,
|
|
318
|
+
compressionMethod,
|
|
319
|
+
localHeaderOffset
|
|
320
|
+
});
|
|
321
|
+
pos += 46 + nameLength + extraLength + commentLength;
|
|
322
|
+
}
|
|
323
|
+
return entries;
|
|
262
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* Reads and decompresses one entry's data, using its central directory
|
|
327
|
+
* metadata to locate the bytes directly — no scan through other entries.
|
|
328
|
+
* The local file header still has to be read first because its name/extra
|
|
329
|
+
* field lengths (which can differ from the central directory's) are what
|
|
330
|
+
* determine where the entry's actual data starts.
|
|
331
|
+
*/
|
|
332
|
+
async function readZipEntryData(input, entry) {
|
|
333
|
+
const localHeader = await readInputRange(input, entry.localHeaderOffset, entry.localHeaderOffset + 30);
|
|
334
|
+
if (localHeader.length < 30 || localHeader.readUInt32LE(0) !== LOCAL_FILE_HEADER_SIGNATURE) throw new ArchiveFormatError(`Not a valid zip archive: malformed local file header for "${entry.path}".`);
|
|
335
|
+
const nameLength = localHeader.readUInt16LE(26);
|
|
336
|
+
const extraLength = localHeader.readUInt16LE(28);
|
|
337
|
+
const dataStart = entry.localHeaderOffset + 30 + nameLength + extraLength;
|
|
338
|
+
const compressed = await readInputRange(input, dataStart, dataStart + entry.compressedSize);
|
|
339
|
+
if (entry.compressionMethod === 0) return compressed;
|
|
340
|
+
if (entry.compressionMethod === 8) return Buffer.from(inflateSync(compressed));
|
|
341
|
+
throw new ArchiveFormatError(`Unsupported zip compression method (${entry.compressionMethod}) for "${entry.path}".`);
|
|
342
|
+
}
|
|
343
|
+
//#endregion
|
|
344
|
+
//#region src/archive/zip.ts
|
|
263
345
|
const zipAdapter = {
|
|
264
346
|
type: "zip",
|
|
265
347
|
canWrite: true,
|
|
266
348
|
async *listEntries(input) {
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
349
|
+
const entries = await parseZipCentralDirectory(input);
|
|
350
|
+
for (const entry of entries) yield {
|
|
351
|
+
path: entry.path,
|
|
352
|
+
size: entry.uncompressedSize,
|
|
353
|
+
openReadStream() {
|
|
354
|
+
return Readable.from((async function* () {
|
|
355
|
+
yield await readZipEntryData(input, entry);
|
|
356
|
+
})());
|
|
357
|
+
}
|
|
274
358
|
};
|
|
275
|
-
source.on("data", (chunk) => unzip.push(new Uint8Array(chunk), false));
|
|
276
|
-
source.on("end", () => {
|
|
277
|
-
unzip.push(/* @__PURE__ */ new Uint8Array(0), true);
|
|
278
|
-
queue.finish();
|
|
279
|
-
});
|
|
280
|
-
source.on("error", (err) => queue.fail(err));
|
|
281
|
-
yield* queue;
|
|
282
359
|
},
|
|
283
360
|
async write(entries, destination) {
|
|
284
361
|
await new Promise((resolve, reject) => {
|
|
@@ -307,6 +384,13 @@ const zipAdapter = {
|
|
|
307
384
|
}
|
|
308
385
|
};
|
|
309
386
|
//#endregion
|
|
387
|
+
//#region src/internal/streamUtils.ts
|
|
388
|
+
async function streamToBuffer(stream) {
|
|
389
|
+
const chunks = [];
|
|
390
|
+
for await (const chunk of stream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
391
|
+
return Buffer.concat(chunks);
|
|
392
|
+
}
|
|
393
|
+
//#endregion
|
|
310
394
|
//#region src/archive/tar.ts
|
|
311
395
|
/**
|
|
312
396
|
* The tar-stream v3 package is built on `streamx`, not Node's native streams;
|
|
@@ -1844,12 +1928,13 @@ function naturalCompare(a, b) {
|
|
|
1844
1928
|
* ComicInfo.xml/MetronInfo.xml) untouched.
|
|
1845
1929
|
*
|
|
1846
1930
|
* This requires two passes over the archive: `listEntries` is called once to
|
|
1847
|
-
* collect image paths (metadata only
|
|
1931
|
+
* collect image paths (metadata only, no content is read, so no
|
|
1848
1932
|
* decompression work is wasted) to compute the sort-order rename map, then
|
|
1849
|
-
* called again to stream entries out under their new names.
|
|
1850
|
-
*
|
|
1851
|
-
*
|
|
1852
|
-
*
|
|
1933
|
+
* called again to stream entries out under their new names. `listEntries` is
|
|
1934
|
+
* re-invoked from the start for the second pass rather than reusing entry
|
|
1935
|
+
* objects collected in the first, since the sequential/CLI-driven adapters
|
|
1936
|
+
* (rar, 7z, ace, tar) can't be "rewound" mid-read even though zip and asar
|
|
1937
|
+
* could serve both passes from the entries collected the first time round.
|
|
1853
1938
|
*/
|
|
1854
1939
|
async function renameArchiveImagesSequentially(input, options = {}) {
|
|
1855
1940
|
const adapter = getAdapter(await detectArchiveType(input));
|
|
@@ -1909,6 +1994,37 @@ async function readArchiveEntry(input, entryPath) {
|
|
|
1909
1994
|
throw new MetadataNotFoundError(`No entry named "${entryPath}" was found in the archive.`);
|
|
1910
1995
|
}
|
|
1911
1996
|
//#endregion
|
|
1997
|
+
//#region src/readArchiveEntries.ts
|
|
1998
|
+
/**
|
|
1999
|
+
* Reads every entry's bytes and SHA256 in a single pass over the archive.
|
|
2000
|
+
* Calling `readArchiveEntry`/`sha256ArchiveEntry` once per entry instead
|
|
2001
|
+
* means re-running `listEntries` and re-locating the target entry on every
|
|
2002
|
+
* call — cheap for zip (backed by real central-directory random access) and
|
|
2003
|
+
* tar/asar, but for the sequential/CLI-driven formats (rar, 7z, ace) each
|
|
2004
|
+
* call re-reads the archive from the start, so reading every entry that way
|
|
2005
|
+
* costs O(n^2). This also does half the I/O of calling both
|
|
2006
|
+
* `sha256ArchiveEntry` and `readArchiveEntry` per entry, since it computes
|
|
2007
|
+
* the hash and buffer from the same read instead of two.
|
|
2008
|
+
*/
|
|
2009
|
+
async function* readArchiveEntries(input) {
|
|
2010
|
+
const adapter = getAdapter(await detectArchiveType(input));
|
|
2011
|
+
for await (const entry of adapter.listEntries(input)) {
|
|
2012
|
+
const hash = createHash("sha256");
|
|
2013
|
+
const chunks = [];
|
|
2014
|
+
for await (const chunk of entry.openReadStream()) {
|
|
2015
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
2016
|
+
chunks.push(buffer);
|
|
2017
|
+
hash.update(buffer);
|
|
2018
|
+
}
|
|
2019
|
+
yield {
|
|
2020
|
+
path: entry.path,
|
|
2021
|
+
size: entry.size,
|
|
2022
|
+
buffer: Buffer.concat(chunks),
|
|
2023
|
+
sha256: hash.digest("hex")
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
//#endregion
|
|
1912
2028
|
//#region src/benchmark/timing.ts
|
|
1913
2029
|
/** Runs `run` `iterations` times and returns the average duration in milliseconds. */
|
|
1914
2030
|
async function averageDuration(iterations, run) {
|
|
@@ -2153,6 +2269,7 @@ const comicArchiveHandler = {
|
|
|
2153
2269
|
stripNonEssentialFiles,
|
|
2154
2270
|
listArchiveFiles,
|
|
2155
2271
|
readArchiveEntry,
|
|
2272
|
+
readArchiveEntries,
|
|
2156
2273
|
benchmarkArchive,
|
|
2157
2274
|
renderBenchmarkReportMarkdown,
|
|
2158
2275
|
WRITABLE_ARCHIVE_TYPES,
|
|
@@ -2160,6 +2277,6 @@ const comicArchiveHandler = {
|
|
|
2160
2277
|
ARCHIVE_TYPE_EXTENSIONS
|
|
2161
2278
|
};
|
|
2162
2279
|
//#endregion
|
|
2163
|
-
export { ARCHIVE_TYPE_EXTENSIONS, ArchiveFormatError, BENCHMARK_IMAGE_FORMATS, COMIC_INFO_AGE_RATING_VALUES, COMIC_INFO_CREDIT_ROLES, COMIC_INFO_MANGA_VALUES, COMIC_INFO_PAGE_TYPE_VALUES, COMIC_INFO_YES_NO_VALUES, FilesystemAccessError, IMAGE_EXTENSIONS, METRON_AGE_RATING_VALUES, METRON_FORMAT_VALUES, METRON_INFORMATION_SOURCE_VALUES, METRON_ROLE_VALUES, MetadataNotFoundError, NoImagesFoundError, SevenZipUnavailableError, UnsupportedOperationError, WRITABLE_ARCHIVE_TYPES, addMetadataToArchive, benchmarkArchive, comicInfoXmlToMetadata, computeArchiveImagePHash, computeImagePHash, convertArchive, convertArchiveImages, convertImageBuffer, comicArchiveHandler as default, detectArchiveType, extractArchive, getExtension, hammingDistance, hasComicMetadata, is7z, isAce, isAsar, isImagePath, isRar, isTar, isZip, joinCommaList, joinResourceNames, listArchiveFiles, metadataToComicInfoXml, metadataToMetronInfoXml, metadataToXml, metronInfoXmlToMetadata, phashToHex, readArchiveEntry, readArchiveMetadata, renameArchiveImagesSequentially, renderBenchmarkReportMarkdown, resourceId, resourceName, sha256Archive, sha256ArchiveEntry, splitCommaList, stripNonEssentialFiles, validateMetadataXml, xmlToMetadata };
|
|
2280
|
+
export { ARCHIVE_TYPE_EXTENSIONS, ArchiveFormatError, BENCHMARK_IMAGE_FORMATS, COMIC_INFO_AGE_RATING_VALUES, COMIC_INFO_CREDIT_ROLES, COMIC_INFO_MANGA_VALUES, COMIC_INFO_PAGE_TYPE_VALUES, COMIC_INFO_YES_NO_VALUES, FilesystemAccessError, IMAGE_EXTENSIONS, METRON_AGE_RATING_VALUES, METRON_FORMAT_VALUES, METRON_INFORMATION_SOURCE_VALUES, METRON_ROLE_VALUES, MetadataNotFoundError, NoImagesFoundError, SevenZipUnavailableError, UnsupportedOperationError, WRITABLE_ARCHIVE_TYPES, addMetadataToArchive, benchmarkArchive, comicInfoXmlToMetadata, computeArchiveImagePHash, computeImagePHash, convertArchive, convertArchiveImages, convertImageBuffer, comicArchiveHandler as default, detectArchiveType, extractArchive, getExtension, hammingDistance, hasComicMetadata, is7z, isAce, isAsar, isImagePath, isRar, isTar, isZip, joinCommaList, joinResourceNames, listArchiveFiles, metadataToComicInfoXml, metadataToMetronInfoXml, metadataToXml, metronInfoXmlToMetadata, phashToHex, readArchiveEntries, readArchiveEntry, readArchiveMetadata, renameArchiveImagesSequentially, renderBenchmarkReportMarkdown, resourceId, resourceName, sha256Archive, sha256ArchiveEntry, splitCommaList, stripNonEssentialFiles, validateMetadataXml, xmlToMetadata };
|
|
2164
2281
|
|
|
2165
2282
|
//# sourceMappingURL=index.mjs.map
|