fix(artifacts): distinguish incomplete PyInstaller probes

This commit is contained in:
morluto committed 2026-10-27 08:27:08 +08:00
1 parent 0ddde829b2
commit abc1351506
7 files changed
+254 -24

No files matched your search

+12 -8
View File
@@ -14,16 +14,17 @@ import {
type NodeSeaLocation,
} from "./nodeSea/NodeSeaLocation.js";
import {
findPyInstallerCookie,
observePyInstallerCookie,
type PyInstallerCookie,
type PyInstallerCookieObservation,
} from "./pyinstaller/CArchiveLayout.js";
/** Completed observations only; null means examined absence, omission means unexamined. */
/** Completed observations only; omission means unexamined, and null is examined absence. */
export interface ArtifactEmbeddedFormatProbeFacts {
readonly sourceState: RegularFileState;
readonly godotPck?: GodotPckLocation | null;
readonly nodeSea?: NodeSeaLocation | null;
readonly pyInstaller?: PyInstallerCookie | null;
readonly pyInstaller?: PyInstallerCookieObservation;
}
/** Share format observations only across admissions of the same unchanged regular file. */
@@ -78,16 +79,19 @@ export class ArtifactEmbeddedFormatProbes {
async pyInstaller(): Promise<PyInstallerCookie | undefined> {
abortIfNeeded(this.signal);
if (this.#facts.pyInstaller !== undefined)
return this.#facts.pyInstaller ?? undefined;
const found = await findPyInstallerCookie(
return this.#facts.pyInstaller.state === "located"
? this.#facts.pyInstaller.cookie
: undefined;
const found = await observePyInstallerCookie(
this.source.handle,
this.source.initial.size,
this.signal,
);
abortIfNeeded(this.signal);
const cookie = found?.ok === true ? found.value : undefined;
this.#facts = { ...this.#facts, pyInstaller: cookie ?? null };
return cookie;
if (!found.ok)
throw new ArtifactReaderFailure(found.error.reason, found.error.message);
this.#facts = { ...this.#facts, pyInstaller: found.value };
return found.value.state === "located" ? found.value.cookie : undefined;
}
completedFacts(): ArtifactEmbeddedFormatProbeFacts {
+8 -5
View File
@@ -34,7 +34,7 @@ import { ARTIFACT_CLASSIFICATION_PREFIX_BYTES } from "./ArtifactClassificationPr
import { PyInstallerPyzStore } from "./pyinstaller/PyInstallerPyzStore.js";
import { pyInstallerMemberStream } from "./pyinstaller/PyInstallerMemberStream.js";
import {
findPyInstallerCookie,
observePyInstallerCookie,
readCArchiveToc,
type CArchiveEntry,
type PyInstallerCookie,
@@ -406,14 +406,17 @@ export class PyInstallerArtifactReader implements ArtifactReader {
size: number,
signal?: AbortSignal,
): Promise<PyInstallerCookie> {
const found = await findPyInstallerCookie(handle, size, signal);
if (found === undefined)
const found = await observePyInstallerCookie(handle, size, signal);
if (!found.ok) throw failure(found.error);
const observed = found.value;
if (observed.state === "absent")
throw new ArtifactReaderFailure(
"format",
"PyInstaller CArchive cookie was not found",
);
if (!found.ok) throw failure(found.error);
return found.value;
if (observed.state === "rejected-candidates")
throw failure(observed.failure);
return observed.cookie;
}
async #toc(
@@ -375,7 +375,7 @@ it.each([false, true] as const)(
vi.mock("../pyinstaller/CArchiveLayout.js", async (importOriginal) => ({
...(await importOriginal<object>()),
findPyInstallerCookie: () =>
observePyInstallerCookie: () =>
Promise.reject(new ArtifactReaderFailure("format", "detection failed")),
}));
@@ -46,7 +46,7 @@ const cases = [
],
}),
member: "pkg/payload.txt",
observe: () => vi.spyOn(cArchive, "findPyInstallerCookie"),
observe: () => vi.spyOn(cArchive, "observePyInstallerCookie"),
},
{
format: "node-sea" as const,
+19 -6
View File
@@ -83,17 +83,26 @@ export const isCArchiveDeclaration = (entry: CArchiveEntry): boolean =>
export const isCArchivePythonModule = (entry: CArchiveEntry): boolean =>
entry.role === "module" || entry.role === "package";
/** Locate a structurally valid cookie. Invalid magic copies are skipped. */
export const findPyInstallerCookie = async (
/** Completed search facts; a rejected magic copy does not establish an archive. */
export type PyInstallerCookieObservation =
| { readonly state: "absent" }
| {
readonly state: "rejected-candidates";
readonly failure: PyInstallerFailure;
}
| { readonly state: "located"; readonly cookie: PyInstallerCookie };
/** Search the bounded tail completely; acquisition failures are not negative observations. */
export const observePyInstallerCookie = async (
handle: FileHandle,
fileSize: number,
signal?: AbortSignal,
): Promise<Result<PyInstallerCookie, PyInstallerFailure> | undefined> => {
): Promise<Result<PyInstallerCookieObservation, PyInstallerFailure>> => {
if (
!Number.isSafeInteger(fileSize) ||
fileSize < PYINSTALLER_COOKIE_V20_BYTES
)
return undefined;
return ok({ state: "absent" });
const tailSize = Math.min(fileSize, PYINSTALLER_COOKIE_SEARCH_BYTES);
const read = await readPyInstallerBytes(handle, {
position: fileSize - tailSize,
@@ -112,11 +121,15 @@ export const findPyInstallerCookie = async (
if (relative < 0) break;
const cookieOffset = fileSize - tailSize + relative;
const parsed = parseCookieAt(observed, relative, cookieOffset, fileSize);
if (parsed.ok) return parsed;
if (parsed.ok) return ok({ state: "located", cookie: parsed.value });
failure = parsed.error;
from = relative;
}
return failure === undefined ? undefined : err(failure);
return ok(
failure === undefined
? { state: "absent" }
: { state: "rejected-candidates", failure },
);
};
/** Read and parse the TOC that a validated cookie names. */
@@ -136,7 +136,7 @@ it("inventories members using the cookie discovered by root classification", asy
);
const path = join(root, "app");
await writeFile(path, fixture());
const locate = vi.spyOn(layout, "findPyInstallerCookie");
const locate = vi.spyOn(layout, "observePyInstallerCookie");
const resourceScope = new ArtifactResourceScope();
try {
const result = await inventoryArtifact(path, {
@@ -194,7 +194,7 @@ it("retains completed cookie facts across unchanged admissions and honors cached
const root = await createTestTempDirectory("rea-pyinstaller-cookie-cache-");
const path = join(root, "app");
await writeFile(path, fixture());
const locate = vi.spyOn(layout, "findPyInstallerCookie");
const locate = vi.spyOn(layout, "observePyInstallerCookie");
try {
await withOwnedRegularFile(path, {}, async (source) => {
const controller = new AbortController();
@@ -222,3 +222,108 @@ it("retains completed cookie facts across unchanged admissions and honors cached
locate.mockRestore();
}
});
it("retains rejected magic candidates without classifying their native carrier as PyInstaller", async () => {
const root = await createTestTempDirectory("rea-pyinstaller-candidate-");
const path = join(root, "app");
await writeFile(
path,
Buffer.concat([
fixture().subarray(0, 64),
layout.PYINSTALLER_COOKIE_MAGIC,
Buffer.alloc(88),
]),
);
await withOwnedRegularFile(path, {}, async (source) => {
const probes = new ArtifactEmbeddedFormatProbes(path, source);
await expect(probes.pyInstaller()).resolves.toBeUndefined();
const observed = probes.completedFacts().pyInstaller;
expect(observed).toMatchObject({
state: "rejected-candidates",
failure: { reason: "format" },
});
const resumed = new ArtifactEmbeddedFormatProbes(
path,
source,
undefined,
probes.completedFacts(),
);
await expect(resumed.pyInstaller()).resolves.toBeUndefined();
expect(resumed.completedFacts().pyInstaller).toBe(observed);
});
const resourceScope = new ArtifactResourceScope();
try {
const result = await inventoryArtifact(path, {
environment: {},
resourceScope,
});
expect(result.manifest.root_format).toBe("elf");
} finally {
await resourceScope.close();
}
});
it("continues past an invalid trailing magic copy to inventory the valid cookie", async () => {
const root = await createTestTempDirectory(
"rea-pyinstaller-candidate-valid-",
);
const path = join(root, "app");
await writeFile(
path,
Buffer.concat([
fixture(),
layout.PYINSTALLER_COOKIE_MAGIC,
Buffer.alloc(88),
]),
);
const resourceScope = new ArtifactResourceScope();
try {
const result = await inventoryArtifact(path, {
environment: {},
resourceScope,
});
expect(result.manifest.root_format).toBe("pyinstaller");
expect(result.occurrences).toContainEqual(
expect.objectContaining({
logical_path: "payload",
hash_status: "verified",
}),
);
} finally {
await resourceScope.close();
}
});
it("does not cache an incomplete tail read as an absent cookie and can retry", async () => {
const root = await createTestTempDirectory("rea-pyinstaller-tail-short-");
const path = join(root, "app");
await writeFile(path, fixture());
await withOwnedRegularFile(path, {}, async (source) => {
const probes = new ArtifactEmbeddedFormatProbes(path, source);
const read = source.handle.read.bind(source.handle);
let calls = 0;
const short = vi
.spyOn(source.handle, "read")
.mockImplementation((...args: unknown[]) => {
calls += 1;
if (calls > 1)
return Promise.resolve({ bytesRead: 0, buffer: args[0] });
args[2] = 3;
return Reflect.apply(read, source.handle, args);
});
try {
await expect(probes.pyInstaller()).rejects.toMatchObject({
reason: "format",
message: "CArchive tail is truncated",
});
expect(calls).toBe(2);
expect(Object.hasOwn(probes.completedFacts(), "pyInstaller")).toBe(false);
} finally {
short.mockRestore();
}
await expect(probes.pyInstaller()).resolves.toMatchObject({
layout: "2.1",
});
expect(probes.completedFacts().pyInstaller?.state).toBe("located");
});
});
@@ -6,6 +6,7 @@ import {
readFile,
rename,
rm,
truncate,
writeFile,
} from "node:fs/promises";
import { join } from "node:path";
@@ -266,7 +267,10 @@ it.each([
await writeFile(source, bytes());
const pck = vi.spyOn(pckLocation, "locateGodotPck");
const sea = vi.spyOn(seaLocation, "locateNodeSea");
const pyInstaller = vi.spyOn(pyInstallerLocation, "findPyInstallerCookie");
const pyInstaller = vi.spyOn(
pyInstallerLocation,
"observePyInstallerCookie",
);
const hashRoot = rootHash.hashStableRootArtifactHandle;
let probesBeforeHash: number[] | undefined;
const hash = vi
@@ -321,3 +325,104 @@ it.each([
}
},
);
it("refuses an incomplete PyInstaller detection tail without reporting unsupported format", async () => {
const root = await createTestTempDirectory(
"rea-extraction-pyinstaller-tail-",
);
const source = join(root, "app");
const output = join(root, "output");
const bytes = cArchiveExecutable({
layout: "2.1",
pythonVersion: 314,
members: [],
});
await writeFile(source, bytes);
const observe = pyInstallerLocation.observePyInstallerCookie;
const detect = vi
.spyOn(pyInstallerLocation, "observePyInstallerCookie")
.mockImplementation(async (...arguments_) => {
// The admitted size is now larger than the bytes the descriptor can read.
await truncate(source, 64);
return observe(...arguments_);
});
const close = vi.spyOn(OwnedFileHandle.prototype, "close");
const hash = vi.spyOn(rootHash, "hashStableRootArtifactHandle");
try {
await withArtifactClient(
{
path: source,
sourcePath: source,
sha256: createHash("sha256").update(bytes).digest("hex"),
kind: "executable",
format: "elf",
architecture: "x86_64",
availableArchitectures: ["x86_64"],
},
async (client) => {
expect(
await client.execute("extract_artifact", { output_root: output }),
).toMatchObject({
ok: false,
error: {
_tag: "ArtifactOperationError",
reason: "format",
detail: "CArchive tail is truncated",
},
});
expect(detect).toHaveBeenCalledTimes(1);
expect(close).toHaveBeenCalled();
expect(hash).not.toHaveBeenCalled();
await expect(access(output)).rejects.toMatchObject({ code: "ENOENT" });
},
);
} finally {
detect.mockRestore();
close.mockRestore();
hash.mockRestore();
}
});
it("keeps incidental rejected cookie signatures unsupported during native extraction", async () => {
const root = await createTestTempDirectory(
"rea-extraction-pyinstaller-candidate-",
);
const source = join(root, "app");
const output = join(root, "output");
const bytes = Buffer.concat([
cArchiveExecutable({
layout: "2.1",
pythonVersion: 314,
members: [],
}).subarray(0, 64),
pyInstallerLocation.PYINSTALLER_COOKIE_MAGIC,
Buffer.alloc(88),
]);
await writeFile(source, bytes);
const hash = vi.spyOn(rootHash, "hashStableRootArtifactHandle");
try {
await withArtifactClient(
{
path: source,
sourcePath: source,
sha256: createHash("sha256").update(bytes).digest("hex"),
kind: "executable",
format: "elf",
architecture: "x86_64",
availableArchitectures: ["x86_64"],
},
async (client) => {
expect(
await client.execute("extract_artifact", { output_root: output }),
).toMatchObject({
ok: false,
error: { _tag: "AnalysisUnsupportedTargetError", path: source },
});
expect(hash).not.toHaveBeenCalled();
await expect(access(output)).rejects.toMatchObject({ code: "ENOENT" });
},
);
} finally {
hash.mockRestore();
}
});