Something went wrong. Try again.
archived: streaming zip archiver/extractor jsr.io/@mary/zip
typescript jsr
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107/** * @module * * internal {@link Reader} that glues an ordered list of volume readers into * a single flat offset space — used by `unzip()` to support multi-volume * archives (PKZIP multi-disk and Info-ZIP `zip -s` splits, which share the * same on-disk format). callers see one logical reader whose length is the * sum of the volumes; reads that cross a volume boundary concatenate across * the underlying readers transparently. * * not part of the public API — `lib/mod.ts` does not re-export it. consumers * pass the original {@link Reader} array straight to `unzip()` instead. */
import type { Reader } from './types.ts';
/** * a {@link Reader} composed of an ordered list of underlying volume readers. * exposes the flat length and a per-volume cumulative-start table so * `walkArchive()` can translate disk-number + local-offset pairs (as stored * in central directory records) into flat offsets. */export class VolumeReader implements Reader { readonly length: number; readonly volumes: readonly Reader[]; /** flat offset of each volume's first byte: `starts[i]` = sum of every * preceding volume's length. `starts[0]` is always 0. */ readonly starts: readonly number[];
constructor(volumes: readonly Reader[]) { const starts: number[] = []; let total = 0; for (const volume of volumes) { starts.push(total); total += volume.length; } this.volumes = volumes; this.starts = starts; this.length = total; }
async read(offset: number, length?: number): Promise<ReadableStream<Uint8Array<ArrayBuffer>>> { const requested = length ?? this.length - offset; const startDisk = this.#diskAt(offset); const startLocal = offset - this.starts[startDisk]; const fromFirstDisk = Math.min(requested, this.volumes[startDisk].length - startLocal);
// fast path: the read lives entirely within one volume — defer to its // reader directly and skip the concatenation wrapper. if (fromFirstDisk === requested) { return await this.volumes[startDisk].read(startLocal, requested); }
const volumes = this.volumes; let disk = startDisk; let local = startLocal; let remaining = requested; let active: ReadableStreamDefaultReader<Uint8Array<ArrayBuffer>> | null = null;
return new ReadableStream<Uint8Array<ArrayBuffer>>({ async pull(controller) { for (;;) { if (active === null) { if (remaining <= 0 || disk >= volumes.length) { controller.close(); return; } const diskLength = volumes[disk].length; const fromThisDisk = Math.min(remaining, diskLength - local); const stream = await volumes[disk].read(local, fromThisDisk); active = stream.getReader(); }
const { done, value } = await active.read(); if (done) { active.releaseLock(); active = null; disk++; local = 0; continue; }
remaining -= value.byteLength; controller.enqueue(value); return; } }, async cancel() { if (active !== null) { await active.cancel(); } }, }); }
/** the disk index whose flat range contains the given offset. */ #diskAt(flatOffset: number): number { const starts = this.starts; for (let idx = starts.length - 1; idx >= 0; idx--) { if (flatOffset >= starts[idx]) { return idx; } } return 0; }}