Files
reverse-engineering-anything/src/artifacts/pyinstaller/PyInstallerMemberStream.ts
T
morluto f8f52f205a fix(python): preserve marshal names and packaged bytecode
Use one marshal surrogatepass decoder for PYZ directories and Python bytecode.
Validate ordinary UTF-8 spans with the native fatal decoder, preserving BOMs,
lone surrogates, raw bytes and marshal reference slots.

Distinguish legacy full-pyc CArchive modules from modern marshal-only modules
before reconstructing headers. Share typed source/codec failures between stream
output and completion while retaining native-read drain and cleanup ownership.

Validated focused producer, reader, lifecycle, CLI and MCP regressions, plus
public inventory/extraction of CPython 3.14-generated marshal tables and code.
No packaged application was executed. Document original versus derived bytes.

Fixes #6800
Fixes #6789
2026-10-24 01:24:43 +08:00

194 lines
6.4 KiB
TypeScript

import { Readable, Transform } from "node:stream";
import { pipeline } from "node:stream/promises";
import { createInflate } from "node:zlib";
import { ArtifactReaderFailure } from "../ArtifactReader.js";
import { abortIfNeeded } from "../ArtifactHash.js";
interface MemberStreamOptions {
readonly sourceBytes: number;
readonly compressed: boolean;
readonly header: Buffer | undefined;
readonly expectedBytes: number | undefined;
readonly maxBytes: number;
readonly signal: AbortSignal;
readonly charge?: (bytes: number) => void;
readonly complete?: (bytes: number) => Promise<void>;
}
/** Observe a bounded plaintext prefix, then drain the existing stream owner. */
export const readPyInstallerMemberPrefix = async (
openSource: (signal: AbortSignal) => Promise<Readable>,
options: Omit<MemberStreamOptions, "header" | "charge" | "complete">,
length: number,
): Promise<Buffer> => {
const stopped = new AbortController();
const prefixComplete = new Error("PyInstaller member prefix observed");
const signal = AbortSignal.any([options.signal, stopped.signal]);
const source = await openSource(signal);
const stream = pyInstallerMemberStream(source, {
...options,
header: undefined,
signal,
});
const prefix = Buffer.alloc(length);
let copied = 0;
stream.output.on("data", (chunk: Buffer) => {
copied += chunk.copy(prefix, copied, 0, length - copied);
if (copied === length) stopped.abort(prefixComplete);
});
try {
await stream.completion;
} catch (cause: unknown) {
// Only our accepted early stop is successful. Native I/O, malformed zlib,
// declaration failures, and caller cancellation keep their original reason.
if (
!(cause instanceof ArtifactReaderFailure) ||
cause.reason !== "cancelled" ||
cause.cause !== prefixComplete
)
throw cause;
}
abortIfNeeded(options.signal);
return prefix.subarray(0, copied);
};
/** Backpressure bounds plaintext independently of the untrusted declared size. */
export const pyInstallerMemberStream = (
source: Readable,
options: MemberStreamOptions,
): { readonly output: Readable; readonly completion: Promise<void> } => {
let bytes = 0;
let headerSent = false;
let completing = false;
let failure: ArtifactReaderFailure | undefined;
const prependHeader = (): void => {
if (!headerSent && options.header !== undefined)
output.push(options.header);
headerSent = true;
};
const output = new Transform({
transform(chunk: Buffer, _encoding, callback) {
try {
abortIfNeeded(options.signal);
bytes += chunk.length;
if (bytes > options.maxBytes)
throw new ArtifactReaderFailure(
"limit",
"PyInstaller member exceeds its plaintext budget",
);
options.charge?.(chunk.length);
prependHeader();
callback(null, chunk);
} catch (cause: unknown) {
callback(asError(cause));
}
},
flush(callback) {
completing = true;
void (async () => {
abortIfNeeded(options.signal);
if (
inflate !== undefined &&
inflate.bytesWritten !== options.sourceBytes
)
throw new ArtifactReaderFailure(
"integrity",
"Compressed PyInstaller member did not consume its complete declared source span",
);
if (
options.expectedBytes !== undefined &&
bytes !== options.expectedBytes
)
throw new ArtifactReaderFailure(
"integrity",
"PyInstaller member size disagrees with its declaration or previous observation",
);
await options.complete?.(bytes);
abortIfNeeded(options.signal);
prependHeader();
})().then(
() => {
completing = false;
try {
abortIfNeeded(options.signal);
} catch (cause: unknown) {
callback(asError(cause));
return;
}
callback();
},
(cause: unknown) => {
completing = false;
callback(asError(cause));
},
);
},
});
// A consumer can retire its reader before attaching its stream error handler.
output.on("error", () => undefined);
const onAbort = (): void => {
// Verification and private-file cleanup own their accepted native I/O.
if (completing) return;
// The descriptor source owns cancellation until its native read settles.
// Retiring it downstream earlier would hide a native I/O rejection.
if (!source.readableEnded && !source.destroyed) return;
output.destroy(
new ArtifactReaderFailure(
"cancelled",
"PyInstaller member read cancelled",
{ cause: options.signal.reason },
),
);
};
options.signal.addEventListener("abort", onAbort, { once: true });
const onSourceFailure = (cause: unknown): void => {
failure ??=
cause instanceof ArtifactReaderFailure
? cause
: new ArtifactReaderFailure(
"io",
"PyInstaller member source could not be read",
{ cause },
);
output.destroy(failure);
};
source.on("error", onSourceFailure);
const inflate = options.compressed ? createInflate() : undefined;
inflate?.on("error", (cause: unknown) => {
failure ??=
cause instanceof ArtifactReaderFailure
? cause
: new ArtifactReaderFailure(
"format",
"Compressed PyInstaller member is not a complete zlib stream",
{ cause },
);
output.destroy(failure);
});
const completion = (
inflate === undefined
? pipeline(source, output)
: pipeline(source, inflate, output)
)
.catch((cause: unknown) => {
// Pipeline can reject with the native error that caused a mapped output
// failure. Completion and output must expose the same boundary reason.
throw failure ?? cause;
})
.finally(() => {
options.signal.removeEventListener("abort", onAbort);
source.removeListener("error", onSourceFailure);
});
// Preserve rejection for the owning scope without an early unhandled promise.
void completion.catch(() => undefined);
if (options.signal.aborted) onAbort();
return { output, completion };
};
const asError = (cause: unknown): Error =>
cause instanceof Error
? cause
: new ArtifactReaderFailure("format", "PyInstaller member read failed", {
cause,
});