Something went wrong. Try again.
archived: streaming 7-Zip archive extractor
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460/** * @module * * runs a {@link Folder}'s coder chain over a packed byte stream and yields * the decoded result. supports arbitrary coder chains including * multi-input coders like BCJ2; the only restriction is that each coder * produce exactly one output stream. */
import { InvalidPasswordError, MissingPasswordError, SevenZipError, UnsupportedFeatureError,} from '../errors.ts';import type { Coder, Folder } from '../header.ts';import type { Reader } from '../reader/types.ts';import { Method } from '../constants.ts';import { StreamReader } from '../utils/stream-reader.ts';
import { decodeAesStream } from './aes.ts';import { decodeBcjArm64Stream, decodeBcjArmStream, decodeBcjArmThumbStream, decodeBcjIa64Stream, decodeBcjPowerPcStream, decodeBcjRiscvStream, decodeBcjSparcStream, decodeBcjX86Stream,} from './bcj.ts';import { decodeBcj2Stream } from './bcj2.ts';import { decodeDeltaStream } from './delta.ts';import { type ByteSource, decodeLzma2Stream, decodeLzmaStream, parseLzma2Properties, parseLzmaProperties,} from './lzma.ts';
// #region coder dispatch
const methodIdToNumber = (methodId: Uint8Array): number => { let n = 0; for (const byte of methodId) { n = (n * 0x100) + byte; } return n;};
/** the numeric method id this layer recognises. */export const coderMethod = (coder: Coder): number => methodIdToNumber(coder.methodId);
/** true when the folder's chain includes an AES coder. */export const folderIsEncrypted = (folder: Folder): boolean => folder.coders.some((c) => coderMethod(c) === Method.aes256Sha256);
/** * promotes a decode-time error into {@link InvalidPasswordError} when the * caller supplied a password and the folder is AES-encrypted. wrong keys * decrypt the pack stream to garbage, which then fails downstream (usually * LZMA's range decoder) as `MalformedArchiveError`; without this remapping * the caller can't distinguish a wrong password from a genuinely corrupt * encrypted archive. * * the original error type is preserved for the un-encrypted case so plain * corruption still surfaces correctly. * * @param err the original decode error * @param folder the folder whose decode threw * @param options the dispatch options the decode ran with * @returns the error to throw — either `err` unchanged or a fresh * {@link InvalidPasswordError} */export const rewrapDecodeError = ( err: unknown, folder: Folder, options: DispatchOptions,): unknown => { if ( options.password === undefined || !(err instanceof SevenZipError) || err instanceof InvalidPasswordError || err instanceof MissingPasswordError || !folderIsEncrypted(folder) ) { return err; } return new InvalidPasswordError( `decode failed after AES decryption — password is likely incorrect ` + `(${err.name}: ${err.message})`, );};
/** runtime options forwarded from the public iterator to the codec layer. */export interface DispatchOptions { password?: string;}
/** * runs one coder over its inputs, yielding `unpackSize` bytes of output. * for the 1→1 coders this is essentially `inputs[0]` → coder → stream; * multi-stream coders (BCJ2) pull from several inputs in tandem. */export const dispatchCoder = ( coder: Coder, inputs: readonly ByteSource[], unpackSize: number, options: DispatchOptions,): AsyncGenerator<Uint8Array<ArrayBuffer>> => { const method = methodIdToNumber(coder.methodId);
// 1→1 coders: take inputs[0] if (coder.numInStreams === 1 && coder.numOutStreams === 1) { const input = inputs[0]; switch (method) { case Method.copy: { return streamCopy(input, unpackSize); } case Method.lzma: { const props = parseLzmaProperties(coder.properties); return decodeLzmaStream(input, props, unpackSize); } case Method.lzma2: { const props = parseLzma2Properties(coder.properties); return decodeLzma2Stream(input, props.dictSize, unpackSize); } case Method.deflate: { return decodeDeflateStream(input, unpackSize); } case Method.delta: { return decodeDeltaStream(input, coder.properties, unpackSize); } case Method.bcj: { return decodeBcjX86Stream(input, unpackSize); } case Method.arm: { return decodeBcjArmStream(input, unpackSize); } case Method.arm64: { return decodeBcjArm64Stream(input, unpackSize); } case Method.armthumb: { return decodeBcjArmThumbStream(input, unpackSize); } case Method.powerpc: { return decodeBcjPowerPcStream(input, unpackSize); } case Method.sparc: { return decodeBcjSparcStream(input, unpackSize); } case Method.riscv: { return decodeBcjRiscvStream(input, unpackSize); } case Method.ia64: { return decodeBcjIa64Stream(input, unpackSize); } case Method.aes256Sha256: { return decodeAesStream(input, coder.properties, options.password, unpackSize); } } }
// multi-stream coders if (method === Method.bcj2) { if (coder.numInStreams !== 4 || coder.numOutStreams !== 1) { throw new UnsupportedFeatureError( `BCJ2 must declare 4 inputs and 1 output: got ${coder.numInStreams}/${coder.numOutStreams}`, ); } return decodeBcj2Stream(inputs[0], inputs[1], inputs[2], inputs[3], unpackSize); }
throw new UnsupportedFeatureError( `unsupported coder method: 0x${method.toString(16)}` + ` (in=${coder.numInStreams}, out=${coder.numOutStreams})`, );};
const streamCopy = async function* ( source: ByteSource, size: number,): AsyncGenerator<Uint8Array<ArrayBuffer>> { let produced = 0; while (produced < size) { const chunkSize = Math.min(size - produced, 64 * 1024); await source.require(chunkSize); const chunk = source.take(chunkSize); const owned = new Uint8Array(chunk.length); owned.set(chunk); yield owned; produced += chunkSize; }};
const decodeDeflateStream = async function* ( source: ByteSource, size: number,): AsyncGenerator<Uint8Array<ArrayBuffer>> { const upstream = byteSourceToStream(source); const decompressed = upstream.pipeThrough(new DecompressionStream('deflate-raw')); let produced = 0; const reader = decompressed.getReader(); try { while (produced < size) { const { done, value } = await reader.read(); if (done) { break; } const chunk = value instanceof Uint8Array ? new Uint8Array(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength)) : new Uint8Array(value); produced += chunk.length; yield chunk; } } finally { await reader.cancel(); }};
const byteSourceToStream = (source: ByteSource): ReadableStream<Uint8Array<ArrayBuffer>> => { return new ReadableStream({ async pull(controller) { if (source.buffered === 0) { try { await source.require(16 * 1024); } catch { // stream might have less left; just drain what's there. } } const have = source.buffered; if (have === 0) { controller.close(); return; } const chunk = source.take(have); const owned = new Uint8Array(chunk.length); owned.set(chunk); controller.enqueue(owned); }, });};
// #endregion
// #region byte-source adapters
/** * wraps an async iterable of byte chunks in the {@link ByteSource} shape. * used to feed one coder's output into the next coder's input. */class IteratorByteSource implements ByteSource { readonly #iter: AsyncIterator<Uint8Array<ArrayBuffer>>; #chunks: Uint8Array[] = []; #buffered = 0; #done = false;
constructor(gen: AsyncIterable<Uint8Array<ArrayBuffer>>) { this.#iter = gen[Symbol.asyncIterator](); }
get buffered(): number { return this.#buffered; }
get done(): boolean { return this.#done; }
async require(size: number): Promise<void> { while (this.#buffered < size) { if (this.#done) { return; } const { done, value } = await this.#iter.next(); if (done) { this.#done = true; return; } this.#chunks.push(value); this.#buffered += value.length; } }
take(size: number): Uint8Array { if (size === 0) { return new Uint8Array(0); } const out = new Uint8Array(size); let written = 0; while (written < size) { const chunk = this.#chunks[0]; const left = size - written; if (chunk.length <= left) { out.set(chunk, written); written += chunk.length; this.#chunks.shift(); } else { out.set(chunk.subarray(0, left), written); this.#chunks[0] = chunk.subarray(left); written += left; } } this.#buffered -= size; return out; }}
// #endregion
// #region folder runner
/** * decodes a folder's coder chain over its pack streams, yielding * `unpackedSize` bytes of plaintext. * * the folder may declare multiple pack streams (typical for chains that * include BCJ2); each is opened against `reader` at its corresponding * offset+size. coders are run in dataflow order, and the terminal * coder's output is yielded. * * @param reader random-access archive source * @param folder the folder description * @param packStreamOffsets absolute offsets of each pack stream, in pack-stream order * @param packStreamSizes byte counts of each pack stream, in pack-stream order * @param unpackedSize byte count of the final plaintext output * @param options runtime options forwarded to each coder */export async function* runFolder( reader: Reader, folder: Folder, packStreamOffsets: readonly number[], packStreamSizes: readonly number[], unpackedSize: number, options: DispatchOptions = {},): AsyncGenerator<Uint8Array<ArrayBuffer>> { for (const c of folder.coders) { if (c.numOutStreams !== 1) { throw new UnsupportedFeatureError( `unsupported coder output arity: ${c.numOutStreams}`, ); } }
// per-coder accumulated input/output stream offsets in the flat global frame. const inputStart: number[] = new Array(folder.coders.length); const outputStart: number[] = new Array(folder.coders.length); let inAcc = 0; let outAcc = 0; for (let i = 0; i < folder.coders.length; i++) { inputStart[i] = inAcc; outputStart[i] = outAcc; inAcc += folder.coders[i].numInStreams; outAcc += folder.coders[i].numOutStreams; }
// for each global input stream, determine its source. type Source = { kind: 'pack'; index: number } | { kind: 'bind'; producerCoderIdx: number }; const inputSources: Source[] = new Array(inAcc); for (let p = 0; p < folder.packedStreams.length; p++) { inputSources[folder.packedStreams[p]] = { index: p, kind: 'pack' }; } for (const bp of folder.bindPairs) { // `bp.out` is a global output stream index; find its owning coder. let producer = -1; for (let i = folder.coders.length - 1; i >= 0; i--) { if (bp.out >= outputStart[i]) { producer = i; break; } } if (producer < 0) { throw new UnsupportedFeatureError(`bindPair references non-existent output: ${bp.out}`); } inputSources[bp.in] = { kind: 'bind', producerCoderIdx: producer }; } for (let i = 0; i < inAcc; i++) { if (inputSources[i] === undefined) { throw new UnsupportedFeatureError(`folder input stream has no source: stream ${i}`); } }
// topological order: a coder runs after all of its bind-source coders. const order: number[] = []; const visited = new Set<number>(); const visit = (idx: number): void => { if (visited.has(idx)) { return; } visited.add(idx); const inStart = inputStart[idx]; for (let k = 0; k < folder.coders[idx].numInStreams; k++) { const src = inputSources[inStart + k]; if (src.kind === 'bind') { visit(src.producerCoderIdx); } } order.push(idx); }; for (let i = 0; i < folder.coders.length; i++) { visit(i); }
// find the terminal coder: its output stream has no bind-pair consumer. const consumedOutputs = new Set(folder.bindPairs.map((bp) => bp.out)); let terminal = -1; for (let i = 0; i < folder.coders.length; i++) { if (!consumedOutputs.has(outputStart[i])) { terminal = i; break; } } if (terminal < 0) { throw new UnsupportedFeatureError('folder has no terminal coder output'); }
// open StreamReaders for each pack stream so they can be disposed at // generator exit. const packReaders: StreamReader[] = []; const cleanup = async (): Promise<void> => { for (const sr of packReaders) { await sr[Symbol.asyncDispose](); } }; try { for (let p = 0; p < packStreamOffsets.length; p++) { const stream = await reader.read(packStreamOffsets[p], packStreamSizes[p]); packReaders.push(new StreamReader(stream)); }
// per-coder cached output ByteSource (filled when the coder runs). const coderOutputs: ByteSource[] = new Array(folder.coders.length);
for (const coderIdx of order) { const coder = folder.coders[coderIdx]; const inputs: ByteSource[] = []; for (let k = 0; k < coder.numInStreams; k++) { const src = inputSources[inputStart[coderIdx] + k]; if (src.kind === 'pack') { inputs.push(packReaders[src.index]); } else { inputs.push(coderOutputs[src.producerCoderIdx]); } } const unpack = coderIdx === terminal ? unpackedSize : folder.unpackSizes[coderIdx]; const gen = dispatchCoder(coder, inputs, unpack, options); if (coderIdx === terminal) { yield* gen; return; } coderOutputs[coderIdx] = new IteratorByteSource(gen); } } finally { await cleanup(); }}
// #endregion