mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
refactor(artifacts): retain admitted PyInstaller cookie facts
This commit is contained in:
1 parent
9222526fe2
commit
8c8a2752c0
11 files changed
+194
-31
No files matched your search
@@ -13,14 +13,17 @@ import {
|
||||
locateNodeSea,
|
||||
type NodeSeaLocation,
|
||||
} from "./nodeSea/NodeSeaLocation.js";
|
||||
import { containsPyInstallerArchive } from "./pyinstaller/CArchiveLayout.js";
|
||||
import {
|
||||
findPyInstallerCookie,
|
||||
type PyInstallerCookie,
|
||||
} from "./pyinstaller/CArchiveLayout.js";
|
||||
|
||||
/** Completed observations only; null means examined absence, omission means unexamined. */
|
||||
export interface ArtifactEmbeddedFormatProbeFacts {
|
||||
readonly sourceState: RegularFileState;
|
||||
readonly godotPck?: GodotPckLocation | null;
|
||||
readonly nodeSea?: NodeSeaLocation | null;
|
||||
readonly pyInstaller?: boolean;
|
||||
readonly pyInstaller?: PyInstallerCookie | null;
|
||||
}
|
||||
|
||||
/** Share format observations only across admissions of the same unchanged regular file. */
|
||||
@@ -72,17 +75,19 @@ export class ArtifactEmbeddedFormatProbes {
|
||||
return location;
|
||||
}
|
||||
|
||||
async pyInstaller(): Promise<boolean> {
|
||||
async pyInstaller(): Promise<PyInstallerCookie | undefined> {
|
||||
abortIfNeeded(this.signal);
|
||||
if (this.#facts.pyInstaller !== undefined) return this.#facts.pyInstaller;
|
||||
const found = await containsPyInstallerArchive(
|
||||
if (this.#facts.pyInstaller !== undefined)
|
||||
return this.#facts.pyInstaller ?? undefined;
|
||||
const found = await findPyInstallerCookie(
|
||||
this.source.handle,
|
||||
this.source.initial.size,
|
||||
this.signal,
|
||||
);
|
||||
abortIfNeeded(this.signal);
|
||||
this.#facts = { ...this.#facts, pyInstaller: found };
|
||||
return found;
|
||||
const cookie = found?.ok === true ? found.value : undefined;
|
||||
this.#facts = { ...this.#facts, pyInstaller: cookie ?? null };
|
||||
return cookie;
|
||||
}
|
||||
|
||||
completedFacts(): ArtifactEmbeddedFormatProbeFacts {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { PyInstallerAdmission } from "./pyinstaller/PyInstallerAdmission.js";
|
||||
import type { Readable } from "node:stream";
|
||||
import { finished } from "node:stream/promises";
|
||||
import { setImmediate } from "node:timers/promises";
|
||||
@@ -110,6 +111,7 @@ interface LoadedArchive {
|
||||
export class PyInstallerArtifactReader implements ArtifactReader {
|
||||
readonly format = "pyinstaller" as const;
|
||||
readonly #source: ArtifactFileSource;
|
||||
#admission: PyInstallerAdmission | undefined;
|
||||
#closed = false;
|
||||
readonly #scope = new ArtifactResourceScope();
|
||||
readonly #cleanup = new ArtifactResourceScope();
|
||||
@@ -124,8 +126,16 @@ export class PyInstallerArtifactReader implements ArtifactReader {
|
||||
readonly #members = new Map<string, PyInstallerMember>();
|
||||
readonly #openedPyzSizes = new Map<string, number>();
|
||||
|
||||
constructor(path: string, source?: StableRegularFileDescriptor) {
|
||||
this.#source = new ArtifactFileSource(path, "PyInstaller", source);
|
||||
constructor(
|
||||
path: string,
|
||||
source?: StableRegularFileDescriptor | PyInstallerAdmission,
|
||||
) {
|
||||
if (source !== undefined && "kind" in source) {
|
||||
this.#admission = source;
|
||||
this.#source = new ArtifactFileSource(path, "PyInstaller", source.source);
|
||||
} else {
|
||||
this.#source = new ArtifactFileSource(path, "PyInstaller", source);
|
||||
}
|
||||
this.#cleanup.retain({
|
||||
kind: "lease",
|
||||
resource: "PyInstaller private PYZ images",
|
||||
@@ -287,6 +297,7 @@ export class PyInstallerArtifactReader implements ArtifactReader {
|
||||
await this.#cleanup.close();
|
||||
this.#closed = true;
|
||||
this.#loaded = undefined;
|
||||
this.#admission = undefined;
|
||||
this.#members.clear();
|
||||
this.#openedPyzSizes.clear();
|
||||
}
|
||||
@@ -313,7 +324,8 @@ export class PyInstallerArtifactReader implements ArtifactReader {
|
||||
const { handle, initial } = await this.#source.readSelectedPath();
|
||||
const size = initial.size;
|
||||
const pathDialect = await carrierPathDialect(handle, size, signal);
|
||||
const cookie = await this.#cookie(handle, size, signal);
|
||||
const cookie =
|
||||
this.#admission?.cookie ?? (await this.#cookie(handle, size, signal));
|
||||
this.#budget = artifactExpansionBudgetBytes(cookie.packageLength);
|
||||
const toc = await this.#toc(handle, cookie, signal);
|
||||
for (const entry of toc) {
|
||||
@@ -339,6 +351,7 @@ export class PyInstallerArtifactReader implements ArtifactReader {
|
||||
if (member.read?.kind === "carchive")
|
||||
this.#charge(member.read.header?.length ?? 0);
|
||||
await this.#source.readSelectedPath();
|
||||
this.#admission = undefined;
|
||||
this.#metadata = cookieMetadata(cookie, toc, encrypted);
|
||||
let opaqueImages = 0;
|
||||
for (const image of images.values())
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { PyInstallerAdmission } from "./pyinstaller/PyInstallerAdmission.js";
|
||||
import type { NodeSeaAdmission } from "./nodeSea/NodeSeaAdmission.js";
|
||||
import type { AsarHeaderAdmission } from "../config/asarHeaderAdmission.js";
|
||||
import { AsarArtifactReader } from "./AsarArtifactReader.js";
|
||||
@@ -33,6 +34,7 @@ interface ArtifactReaderConstruction {
|
||||
readonly admission?:
|
||||
| StableRegularFileDescriptor
|
||||
| NodeSeaAdmission
|
||||
| PyInstallerAdmission
|
||||
| undefined;
|
||||
readonly asarHeaderAdmission?: AsarHeaderAdmission | undefined;
|
||||
/** Caller-selected root spelling; only ASAR companion lookup consumes this. */
|
||||
@@ -75,9 +77,23 @@ export function createArtifactReader(
|
||||
: { selectedPath: options.selectedPath }),
|
||||
});
|
||||
case "pyinstaller":
|
||||
return new PyInstallerArtifactReader(path, rootSource);
|
||||
return new PyInstallerArtifactReader(
|
||||
path,
|
||||
admission !== undefined &&
|
||||
"kind" in admission &&
|
||||
admission.kind === "pyinstaller"
|
||||
? admission
|
||||
: rootSource,
|
||||
);
|
||||
case "node-sea":
|
||||
return new NodeSeaArtifactReader(path, admission);
|
||||
return new NodeSeaArtifactReader(
|
||||
path,
|
||||
admission !== undefined &&
|
||||
"kind" in admission &&
|
||||
admission.kind === "pyinstaller"
|
||||
? rootSource
|
||||
: admission,
|
||||
);
|
||||
case "godot-pck":
|
||||
return new GodotPckArtifactReader(path, rootSource);
|
||||
case "mach-o-universal":
|
||||
|
||||
@@ -375,7 +375,7 @@ it.each([false, true] as const)(
|
||||
|
||||
vi.mock("../pyinstaller/CArchiveLayout.js", async (importOriginal) => ({
|
||||
...(await importOriginal<object>()),
|
||||
containsPyInstallerArchive: () =>
|
||||
findPyInstallerCookie: () =>
|
||||
Promise.reject(new ArtifactReaderFailure("format", "detection failed")),
|
||||
}));
|
||||
|
||||
|
||||
@@ -603,7 +603,7 @@ const requireExtractableFormat = async (
|
||||
(await probes.godotPck()) !== undefined ||
|
||||
((format === "elf" || format === "pe" || format === "mach-o") &&
|
||||
((await probes.nodeSea()) !== undefined ||
|
||||
(await probes.pyInstaller())));
|
||||
(await probes.pyInstaller()) !== undefined));
|
||||
if (pack && input.expectedRootSha256 !== undefined) {
|
||||
const digest = await hashStableRootArtifactHandle(
|
||||
path,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { PyInstallerAdmission } from "../pyinstaller/PyInstallerAdmission.js";
|
||||
import { assertArtifactSelectedSource } from "../ArtifactSelectedSource.js";
|
||||
import { artifactNameForClassification } from "../../domain/artifactNameSelection.js";
|
||||
import type { NodeSeaAdmission } from "../nodeSea/NodeSeaAdmission.js";
|
||||
@@ -51,6 +52,7 @@ type RootInventoryClassification = RootClassification & {
|
||||
readonly readerAdmission:
|
||||
| StableRegularFileDescriptor
|
||||
| NodeSeaAdmission
|
||||
| PyInstallerAdmission
|
||||
| undefined;
|
||||
};
|
||||
|
||||
@@ -202,7 +204,11 @@ const classifyRootFormat = async (
|
||||
signal?: AbortSignal,
|
||||
asarHeaderAdmission?: AsarHeaderAdmission,
|
||||
selectedPath?: string,
|
||||
): Promise<ArtifactOccurrence["artifact_format"] | NodeSeaAdmission> => {
|
||||
): Promise<
|
||||
| ArtifactOccurrence["artifact_format"]
|
||||
| NodeSeaAdmission
|
||||
| PyInstallerAdmission
|
||||
> => {
|
||||
const { handle } = source;
|
||||
const probe = createArtifactClassificationProbe(asarHeaderAdmission);
|
||||
const chunk = Buffer.alloc(64 * 1024);
|
||||
@@ -292,7 +298,8 @@ const classifyRootFormat = async (
|
||||
}
|
||||
const packaged = await embeddedProbes.pyInstaller();
|
||||
abortIfNeeded(signal);
|
||||
if (packaged) return "pyinstaller";
|
||||
if (packaged !== undefined)
|
||||
return { kind: "pyinstaller", source, cookie: packaged };
|
||||
}
|
||||
return format;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { PyInstallerAdmission } from "../pyinstaller/PyInstallerAdmission.js";
|
||||
import type { NodeSeaAdmission } from "../nodeSea/NodeSeaAdmission.js";
|
||||
import type { AsarHeaderAdmission } from "../../config/asarHeaderAdmission.js";
|
||||
import type {
|
||||
@@ -19,7 +20,10 @@ export const createReader = (
|
||||
path: string,
|
||||
format: ArtifactOccurrence["artifact_format"],
|
||||
environment: Readonly<NodeJS.ProcessEnv>,
|
||||
readerAdmission?: StableRegularFileDescriptor | NodeSeaAdmission,
|
||||
readerAdmission?:
|
||||
| StableRegularFileDescriptor
|
||||
| NodeSeaAdmission
|
||||
| PyInstallerAdmission,
|
||||
asarHeaderAdmission?: AsarHeaderAdmission,
|
||||
selectedPath?: string,
|
||||
): ArtifactReader | undefined => {
|
||||
|
||||
@@ -83,16 +83,6 @@ export const isCArchiveDeclaration = (entry: CArchiveEntry): boolean =>
|
||||
export const isCArchivePythonModule = (entry: CArchiveEntry): boolean =>
|
||||
entry.role === "module" || entry.role === "package";
|
||||
|
||||
/** True when a native executable has a structurally valid CArchive cookie. */
|
||||
export const containsPyInstallerArchive = async (
|
||||
handle: FileHandle,
|
||||
fileSize: number,
|
||||
signal?: AbortSignal,
|
||||
): Promise<boolean> => {
|
||||
const found = await findPyInstallerCookie(handle, fileSize, signal);
|
||||
return found?.ok === true;
|
||||
};
|
||||
|
||||
/** Locate a structurally valid cookie. Invalid magic copies are skipped. */
|
||||
export const findPyInstallerCookie = async (
|
||||
handle: FileHandle,
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { appendFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import { withOwnedRegularFile } from "../../filesystem/OwnedRegularFile.js";
|
||||
import { ArtifactEmbeddedFormatProbes } from "../ArtifactEmbeddedFormatProbes.js";
|
||||
import { ArtifactResourceScope } from "../ArtifactResourceScope.js";
|
||||
import { inventoryArtifact } from "../inventory/ArtifactInventory.js";
|
||||
import { PyInstallerArtifactReader } from "../PyInstallerArtifactReader.js";
|
||||
import { cArchiveExecutable } from "./CArchive.fixture.js";
|
||||
import * as layout from "./CArchiveLayout.js";
|
||||
|
||||
const fixture = () =>
|
||||
cArchiveExecutable({
|
||||
layout: "2.1",
|
||||
pythonVersion: 314,
|
||||
pythonLibrary: "libpython3.14.so",
|
||||
members: [
|
||||
{
|
||||
name: "payload",
|
||||
typeCode: "x",
|
||||
payload: Buffer.from("fixture"),
|
||||
compress: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
it("inventories members using the cookie discovered by root classification", async () => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-pyinstaller-cookie-inventory-",
|
||||
);
|
||||
const path = join(root, "app");
|
||||
await writeFile(path, fixture());
|
||||
const locate = vi.spyOn(layout, "findPyInstallerCookie");
|
||||
const resourceScope = new ArtifactResourceScope();
|
||||
try {
|
||||
const result = await inventoryArtifact(path, {
|
||||
environment: {},
|
||||
resourceScope,
|
||||
});
|
||||
expect(result.manifest.root_format).toBe("pyinstaller");
|
||||
expect(
|
||||
result.occurrences.some(
|
||||
(entry) =>
|
||||
entry.logical_path === "payload" && entry.hash_status === "verified",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(locate).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
await resourceScope.close();
|
||||
locate.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects changed source before consuming an admitted cookie and leaves the borrowed descriptor open", async () => {
|
||||
const root = await createTestTempDirectory("rea-pyinstaller-cookie-change-");
|
||||
const path = join(root, "app");
|
||||
await writeFile(path, fixture());
|
||||
await withOwnedRegularFile(path, {}, async (source) => {
|
||||
const cookie = await new ArtifactEmbeddedFormatProbes(
|
||||
path,
|
||||
source,
|
||||
).pyInstaller();
|
||||
if (cookie === undefined) throw new Error("Fixture cookie was not found");
|
||||
const reader = new PyInstallerArtifactReader(path, {
|
||||
kind: "pyinstaller",
|
||||
source,
|
||||
cookie,
|
||||
});
|
||||
await appendFile(path, "changed");
|
||||
try {
|
||||
await expect(
|
||||
(async () => {
|
||||
for await (const entry of reader.entries())
|
||||
throw new Error(`Changed source exposed member ${entry.path}`);
|
||||
})(),
|
||||
).rejects.toMatchObject({ reason: "integrity" });
|
||||
} finally {
|
||||
await reader.close();
|
||||
}
|
||||
await expect(source.handle.stat()).resolves.toMatchObject({
|
||||
size: fixture().length + 7,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("retains completed cookie facts across unchanged admissions and honors cached cancellation", async () => {
|
||||
const root = await createTestTempDirectory("rea-pyinstaller-cookie-cache-");
|
||||
const path = join(root, "app");
|
||||
await writeFile(path, fixture());
|
||||
const locate = vi.spyOn(layout, "findPyInstallerCookie");
|
||||
try {
|
||||
await withOwnedRegularFile(path, {}, async (source) => {
|
||||
const controller = new AbortController();
|
||||
const probes = new ArtifactEmbeddedFormatProbes(
|
||||
path,
|
||||
source,
|
||||
controller.signal,
|
||||
);
|
||||
const cookie = await probes.pyInstaller();
|
||||
expect(cookie).toMatchObject({ layout: "2.1", pythonVersion: 314 });
|
||||
const resumed = new ArtifactEmbeddedFormatProbes(
|
||||
path,
|
||||
source,
|
||||
controller.signal,
|
||||
probes.completedFacts(),
|
||||
);
|
||||
await expect(resumed.pyInstaller()).resolves.toBe(cookie);
|
||||
controller.abort(new Error("cancel cached cookie"));
|
||||
await expect(resumed.pyInstaller()).rejects.toMatchObject({
|
||||
reason: "cancelled",
|
||||
});
|
||||
expect(locate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
} finally {
|
||||
locate.mockRestore();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { StableRegularFileDescriptor } from "../../filesystem/RegularFile.js";
|
||||
import type { PyInstallerCookie } from "./CArchiveLayout.js";
|
||||
|
||||
/** Validated cookie facts borrowed with the descriptor that produced them. */
|
||||
export interface PyInstallerAdmission {
|
||||
readonly kind: "pyinstaller";
|
||||
readonly source: StableRegularFileDescriptor;
|
||||
readonly cookie: PyInstallerCookie;
|
||||
}
|
||||
@@ -266,10 +266,7 @@ it.each([
|
||||
await writeFile(source, bytes());
|
||||
const pck = vi.spyOn(pckLocation, "locateGodotPck");
|
||||
const sea = vi.spyOn(seaLocation, "locateNodeSea");
|
||||
const pyInstaller = vi.spyOn(
|
||||
pyInstallerLocation,
|
||||
"containsPyInstallerArchive",
|
||||
);
|
||||
const pyInstaller = vi.spyOn(pyInstallerLocation, "findPyInstallerCookie");
|
||||
const hashRoot = rootHash.hashStableRootArtifactHandle;
|
||||
let probesBeforeHash: number[] | undefined;
|
||||
const hash = vi
|
||||
@@ -310,6 +307,10 @@ it.each([
|
||||
expect(extracted.manifest.root_format).toBe(format);
|
||||
expect(extracted.artifacts.length).toBeGreaterThan(0);
|
||||
expect(probesBeforeHash).toEqual(probes);
|
||||
// Preflight's cookie also supplies inventory. Extraction opens its
|
||||
// own reader and establishes that reader's cookie independently.
|
||||
if (format === "pyinstaller")
|
||||
expect(pyInstaller).toHaveBeenCalledTimes(2);
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
|
||||
Reference in new issue
Block a user