Files
rea/src/artifacts/AsarEntryStream.ts
T

118 lines
4.0 KiB
TypeScript

import type { FileHandleChunkStream } from "../filesystem/readFileHandleChunks.js";
import { Readable } from "node:stream";
import { ArtifactReaderFailure } from "./ArtifactReader.js";
import {
assertAsarReadActive,
asarCancellationFailure,
asarFailure,
} from "./AsarFailure.js";
/** Close an ASAR file handle before honoring cancellation of an empty member. */
export const closeAsarHandle = async (
close: () => Promise<void>,
signal?: AbortSignal,
): Promise<void> => {
await close();
assertAsarReadActive(signal);
};
/** Validate and contextualize bytes read from one ASAR member source. */
export const readValidatedAsarEntry = (
source: FileHandleChunkStream,
expectedBytes: number | undefined,
entryPath: string,
archivePath: string,
signal?: AbortSignal,
): Readable => {
const expectedExtent =
expectedBytes === undefined ? undefined : BigInt(expectedBytes);
const completionFailure = (): ArtifactReaderFailure | undefined => {
const coverage = source.readCoverage();
if (coverage.kind !== "complete")
return new ArtifactReaderFailure(
"integrity",
`ASAR entry read coverage is incomplete: ${entryPath}`,
);
if (expectedExtent !== undefined && coverage.bytes !== expectedExtent)
return new ArtifactReaderFailure(
"integrity",
`ASAR entry size disagrees with its header: ${entryPath}`,
);
if (signal?.aborted) return asarCancellationFailure(signal.reason);
return undefined;
};
const output = Readable.from(
(async function* () {
const iterator = source[Symbol.asyncIterator]();
try {
while (true) {
const next = await iterator.next();
if (next.done) break;
const chunk: unknown = next.value;
if (!Buffer.isBuffer(chunk) && !(chunk instanceof Uint8Array))
throw new ArtifactReaderFailure(
"format",
`ASAR entry did not return bytes: ${entryPath}`,
);
const coverage = source.readCoverage();
if (expectedExtent !== undefined && coverage.bytes > expectedExtent)
throw new ArtifactReaderFailure(
"integrity",
`ASAR entry exceeded its declared size: ${entryPath}`,
);
if (coverage.kind === "complete" && signal?.aborted) {
const failure = completionFailure();
if (failure !== undefined) throw failure;
}
yield chunk;
}
const failure = completionFailure();
if (failure !== undefined) throw failure;
} catch (cause: unknown) {
throw asarFailure(archivePath, `read ${entryPath}`, cause);
} finally {
try {
await iterator.return?.();
} catch {
// Preserve the read failure; source destruction below owns cleanup.
}
if (!source.destroyed) source.destroy();
}
})(),
);
const onSourceError = (cause: unknown): void => {
const completedCancellation =
cause instanceof ArtifactReaderFailure &&
cause.reason === "cancelled" &&
source.readCoverage().kind === "complete"
? completionFailure()
: undefined;
output.destroy(
completedCancellation ??
asarFailure(archivePath, `read ${entryPath}`, cause),
);
};
source.on("error", onSourceError);
source.once("close", () => source.removeListener("error", onSourceError));
const onAbort = (): void => {
if (
source.readCoverage().kind === "complete" &&
!output.destroyed &&
(source.errored === null ||
(source.errored instanceof ArtifactReaderFailure &&
source.errored.reason === "cancelled"))
) {
const failure = completionFailure();
if (failure !== undefined) output.destroy(failure);
}
};
signal?.addEventListener("abort", onAbort, { once: true });
output.once("close", () => {
signal?.removeEventListener("abort", onAbort);
if (!source.destroyed) source.destroy();
});
if (signal?.aborted) onAbort();
return output;
};