@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 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 — no content is read, so no
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. Streaming
486
- * adapters (zip in particular) can't be "rewound" mid-read, so the second
487
- * pass re-invokes `listEntries` from the start rather than reusing entry
488
- * objects collected in the first pass.
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[];
@@ -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/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 { Unzip, UnzipInflate, Zip, ZipDeflate } from "fflate";
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/streamUtils.ts
203
- async function streamToBuffer(stream) {
204
- const chunks = [];
205
- for await (const chunk of stream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
206
- return Buffer.concat(chunks);
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
- /** Bridges a callback-driven push source into an async iterable, in arrival order. */
209
- var AsyncQueue = class {
210
- items = [];
211
- waiter = null;
212
- done = false;
213
- error = null;
214
- push(item) {
215
- this.items.push(item);
216
- this.waiter?.();
217
- }
218
- finish() {
219
- this.done = true;
220
- this.waiter?.();
221
- }
222
- fail(err) {
223
- this.error = err;
224
- this.done = true;
225
- this.waiter?.();
226
- }
227
- async *[Symbol.asyncIterator]() {
228
- for (;;) {
229
- if (this.items.length) {
230
- yield this.items.shift();
231
- continue;
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 (this.error) throw this.error;
234
- if (this.done) return;
235
- await new Promise((resolve) => {
236
- this.waiter = resolve;
237
- });
238
- this.waiter = null;
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
- //#endregion
243
- //#region src/archive/zip.ts
244
- function toEntry(file) {
245
- return {
246
- path: file.name,
247
- size: file.originalSize,
248
- openReadStream() {
249
- const pass = new PassThrough();
250
- file.ondata = (err, data, final) => {
251
- if (err) {
252
- pass.destroy(err);
253
- return;
254
- }
255
- if (data.length) pass.write(Buffer.from(data));
256
- if (final) pass.end();
257
- };
258
- file.start();
259
- return pass;
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 source = openInputReadStream(input);
268
- const queue = new AsyncQueue();
269
- const unzip = new Unzip();
270
- unzip.register(UnzipInflate);
271
- unzip.onfile = (file) => {
272
- if (file.name.endsWith("/")) return;
273
- queue.push(toEntry(file));
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 — no content is read, so no
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. Streaming
1850
- * adapters (zip in particular) can't be "rewound" mid-read, so the second
1851
- * pass re-invokes `listEntries` from the start rather than reusing entry
1852
- * objects collected in the first pass.
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