mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(artifacts): distinguish incomplete PyInstaller probes
This commit is contained in:
1 parent
0ddde829b2
commit
abc1351506
7 files changed
+254
-24
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user