@clearmist-labs/comic-archive-handler 1.1.1 → 1.3.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/README.md +1 -1
- package/dist/index.d.mts +32 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +216 -76
- package/dist/index.mjs.map +1 -1
- package/docs/API.md +34 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ A toolkit for detecting, converting, and manipulating comic book archives (CBAS/
|
|
|
11
11
|
- **Re-encode** page images to different formats
|
|
12
12
|
- **Hash** archives perceptually and by content
|
|
13
13
|
- **Rename** pages sequentially
|
|
14
|
-
- **Strip** non-essential files from archives
|
|
14
|
+
- **Strip** non-essential files from archives, or **remove** a single named entry
|
|
15
15
|
- **Extract** archives to a plain directory of files
|
|
16
16
|
- **Benchmark** archive/image format combinations for creation speed, random read speed, and size
|
|
17
17
|
|
package/dist/index.d.mts
CHANGED
|
@@ -61,6 +61,7 @@ export interface RenameOptions extends ArchiveWriteOptions {
|
|
|
61
61
|
export interface StripOptions extends ArchiveWriteOptions {
|
|
62
62
|
extraKeepExtensions?: string[];
|
|
63
63
|
}
|
|
64
|
+
export type RemoveEntryOptions = ArchiveWriteOptions;
|
|
64
65
|
export interface ComicCredit {
|
|
65
66
|
name: string;
|
|
66
67
|
role: string;
|
|
@@ -480,12 +481,13 @@ export declare function sha256Archive(input: ArchiveInput): Promise<string>;
|
|
|
480
481
|
* ComicInfo.xml/MetronInfo.xml) untouched.
|
|
481
482
|
*
|
|
482
483
|
* This requires two passes over the archive: `listEntries` is called once to
|
|
483
|
-
* collect image paths (metadata only
|
|
484
|
+
* collect image paths (metadata only, no content is read, so no
|
|
484
485
|
* 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
|
-
*
|
|
486
|
+
* called again to stream entries out under their new names. `listEntries` is
|
|
487
|
+
* re-invoked from the start for the second pass rather than reusing entry
|
|
488
|
+
* objects collected in the first, since the sequential/CLI-driven adapters
|
|
489
|
+
* (rar, 7z, ace, tar) can't be "rewound" mid-read even though zip and asar
|
|
490
|
+
* could serve both passes from the entries collected the first time round.
|
|
489
491
|
*/
|
|
490
492
|
export declare function renameArchiveImagesSequentially(input: ArchiveInput, options?: RenameOptions): Promise<Buffer | void>;
|
|
491
493
|
//#endregion
|
|
@@ -498,6 +500,29 @@ export declare function listArchiveFiles(input: ArchiveInput): Promise<string[]>
|
|
|
498
500
|
//#region src/readArchiveEntry.d.ts
|
|
499
501
|
export declare function readArchiveEntry(input: ArchiveInput, entryPath: string): Promise<Buffer>;
|
|
500
502
|
//#endregion
|
|
503
|
+
//#region src/readArchiveEntries.d.ts
|
|
504
|
+
export interface ReadArchiveEntriesResult {
|
|
505
|
+
path: string;
|
|
506
|
+
size?: number;
|
|
507
|
+
buffer: Buffer;
|
|
508
|
+
sha256: string;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Reads every entry's bytes and SHA256 in a single pass over the archive.
|
|
512
|
+
* Calling `readArchiveEntry`/`sha256ArchiveEntry` once per entry instead
|
|
513
|
+
* means re-running `listEntries` and re-locating the target entry on every
|
|
514
|
+
* call — cheap for zip (backed by real central-directory random access) and
|
|
515
|
+
* tar/asar, but for the sequential/CLI-driven formats (rar, 7z, ace) each
|
|
516
|
+
* call re-reads the archive from the start, so reading every entry that way
|
|
517
|
+
* costs O(n^2). This also does half the I/O of calling both
|
|
518
|
+
* `sha256ArchiveEntry` and `readArchiveEntry` per entry, since it computes
|
|
519
|
+
* the hash and buffer from the same read instead of two.
|
|
520
|
+
*/
|
|
521
|
+
export declare function readArchiveEntries(input: ArchiveInput): AsyncGenerator<ReadArchiveEntriesResult>;
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region src/removeArchiveEntry.d.ts
|
|
524
|
+
export declare function removeArchiveEntry(input: ArchiveInput, entryPath: string, options?: RemoveEntryOptions): Promise<Buffer | void>;
|
|
525
|
+
//#endregion
|
|
501
526
|
//#region src/benchmark/benchmarkArchive.d.ts
|
|
502
527
|
/**
|
|
503
528
|
* Extracts a comic archive, validates it contains image files, then
|
|
@@ -537,6 +562,8 @@ declare const comicArchiveHandler: {
|
|
|
537
562
|
stripNonEssentialFiles: typeof stripNonEssentialFiles;
|
|
538
563
|
listArchiveFiles: typeof listArchiveFiles;
|
|
539
564
|
readArchiveEntry: typeof readArchiveEntry;
|
|
565
|
+
readArchiveEntries: typeof readArchiveEntries;
|
|
566
|
+
removeArchiveEntry: typeof removeArchiveEntry;
|
|
540
567
|
benchmarkArchive: typeof benchmarkArchive;
|
|
541
568
|
renderBenchmarkReportMarkdown: typeof renderBenchmarkReportMarkdown;
|
|
542
569
|
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":";;YAEY;;YAGA,eAAe;YAEf;YAEA;iBAEK;;EAEf;;EAEA;;EAEA;;iBAGe;;EAEf;;iBAGe;;EAEf;;EAEA;;EAEA;;iBAGe;EACf,OAAO;EACP,OAAO;EACP,MAAM;;;iBAIS;;;;;;;EAOf;;;;;EAKA,kBAAkB;;iBAGH,8BAA8B;EAC7C;IAAU,QAAQ;IAAmB,UAAU;;;iBAGhC,2BAA2B;EAC1C;;iBAGe,sBAAsB;EACrC;EACA;;iBAGe,qBAAqB;EACpC;;
|
|
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/removeArchiveEntry.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;
|
|
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}".`);
|
|
262
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,59 @@ 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
|
|
2028
|
+
//#region src/removeArchiveEntry.ts
|
|
2029
|
+
async function removeArchiveEntry(input, entryPath, options = {}) {
|
|
2030
|
+
const adapter = getAdapter(await detectArchiveType(input));
|
|
2031
|
+
let found = false;
|
|
2032
|
+
for await (const entry of adapter.listEntries(input, { tempDir: options.tempDir })) if (entry.path === entryPath) {
|
|
2033
|
+
found = true;
|
|
2034
|
+
break;
|
|
2035
|
+
}
|
|
2036
|
+
if (!found) throw new MetadataNotFoundError(`No entry named "${entryPath}" was found in the archive.`);
|
|
2037
|
+
async function* output() {
|
|
2038
|
+
for await (const entry of adapter.listEntries(input, { tempDir: options.tempDir })) {
|
|
2039
|
+
if (entry.path === entryPath) continue;
|
|
2040
|
+
yield {
|
|
2041
|
+
path: entry.path,
|
|
2042
|
+
size: entry.size,
|
|
2043
|
+
content: entry.openReadStream()
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
return withOutput(options.output, (destination) => adapter.write(output(), destination, { tempDir: options.tempDir }));
|
|
2048
|
+
}
|
|
2049
|
+
//#endregion
|
|
1912
2050
|
//#region src/benchmark/timing.ts
|
|
1913
2051
|
/** Runs `run` `iterations` times and returns the average duration in milliseconds. */
|
|
1914
2052
|
async function averageDuration(iterations, run) {
|
|
@@ -2153,6 +2291,8 @@ const comicArchiveHandler = {
|
|
|
2153
2291
|
stripNonEssentialFiles,
|
|
2154
2292
|
listArchiveFiles,
|
|
2155
2293
|
readArchiveEntry,
|
|
2294
|
+
readArchiveEntries,
|
|
2295
|
+
removeArchiveEntry,
|
|
2156
2296
|
benchmarkArchive,
|
|
2157
2297
|
renderBenchmarkReportMarkdown,
|
|
2158
2298
|
WRITABLE_ARCHIVE_TYPES,
|
|
@@ -2160,6 +2300,6 @@ const comicArchiveHandler = {
|
|
|
2160
2300
|
ARCHIVE_TYPE_EXTENSIONS
|
|
2161
2301
|
};
|
|
2162
2302
|
//#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 };
|
|
2303
|
+
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, removeArchiveEntry, renameArchiveImagesSequentially, renderBenchmarkReportMarkdown, resourceId, resourceName, sha256Archive, sha256ArchiveEntry, splitCommaList, stripNonEssentialFiles, validateMetadataXml, xmlToMetadata };
|
|
2164
2304
|
|
|
2165
2305
|
//# sourceMappingURL=index.mjs.map
|