mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 02:32:54 +00:00
* feat(artifacts): read Godot PCK packs Add a Godot Engine PCK artifact reader for standalone packs and packs embedded behind an executable trailer (formats 2 and 3). Members are uncompressed byte ranges verified against their declared MD5 on read. Encrypted directories, sparse bundles, patch removal entries, unknown flags and format 1 fail closed as unavailable; encrypted members are reported without being read. inspect-artifact and extract-artifact recognize standalone packs by their GDPC magic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(artifacts): bound Godot PCK metadata and own member streams Cap member paths at 4096 bytes and the retained directory at 64 MiB before allocation. Read members with positional chunk reads through pipeline so cancellation is typed, abandoned consumers release their source, and the shared descriptor stays reader-owned. Retain the root descriptor for godot-pck inventory, as for ZIP and universal Mach-O. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(artifacts): complete Godot PCK facts, integrity and selected views * fix(artifacts): preserve atomic-root inventory Evidence * fix(artifacts): bound aggregate PCK member expansion --------- Co-authored-by: Samucahub <151155051+Samucahub@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
205 lines
6.6 KiB
TypeScript
205 lines
6.6 KiB
TypeScript
import { locateGodotPck } from "../GodotPckLocation.js";
|
|
import { sameRegularFileState } from "../../filesystem/RegularFile.js";
|
|
import type { FileHandle } from "node:fs/promises";
|
|
import type { Stats } from "node:fs";
|
|
import type { StableRegularFileDescriptor } from "../../filesystem/RegularFile.js";
|
|
import type { OwnedFileHandle } from "../../filesystem/OwnedFileHandle.js";
|
|
|
|
import { classifyArtifactContent } from "./ArtifactGraphConstruction.js";
|
|
import { abortIfNeeded } from "../ArtifactHash.js";
|
|
import { createArtifactClassificationProbe } from "../ArtifactClassificationProbe.js";
|
|
import type { ArtifactOccurrence } from "../../domain/artifactGraph.js";
|
|
import {
|
|
hasZipSignature,
|
|
isZipPackageFormat,
|
|
zipPackageFormatForPath,
|
|
type ZipPackageFormat,
|
|
} from "../../domain/zipPackageFormat.js";
|
|
import { ArtifactReaderFailure } from "../ArtifactReader.js";
|
|
import type { ArtifactResourceOwner } from "../ArtifactResourceScope.js";
|
|
import type { HashResult } from "../ArtifactHash.js";
|
|
import {
|
|
hashStableRootArtifactHandle,
|
|
openRootArtifact,
|
|
} from "./hashStableRootArtifact.js";
|
|
import type { ArtifactInventoryOptions } from "./types.js";
|
|
|
|
interface RootClassification {
|
|
readonly format: ArtifactOccurrence["artifact_format"];
|
|
readonly digest: HashResult | null;
|
|
}
|
|
|
|
type RootInventoryClassification = RootClassification & {
|
|
readonly rootSource: RootInventorySource | undefined;
|
|
};
|
|
|
|
/** The admitted descriptor and the single owner that can confirm its close. */
|
|
export interface RootInventorySource extends StableRegularFileDescriptor {
|
|
readonly owner: OwnedFileHandle;
|
|
}
|
|
|
|
/** Classify and hash one file root through the same stable open descriptor. */
|
|
export const classifyAndHashRoot = async (
|
|
path: string,
|
|
directory: boolean,
|
|
expectedMetadata: Stats,
|
|
options: Pick<ArtifactInventoryOptions, "resourceScope" | "signal">,
|
|
): Promise<RootClassification> => {
|
|
return options.resourceScope.run(async () => {
|
|
const result = await classifyAndHashRootForInventory(
|
|
path,
|
|
directory,
|
|
expectedMetadata,
|
|
options,
|
|
);
|
|
const rootSource = result.rootSource;
|
|
if (rootSource !== undefined) {
|
|
const owner: ArtifactResourceOwner = {
|
|
kind: "file-handle",
|
|
handle: rootSource.owner,
|
|
resource: `root artifact descriptor for ${path}`,
|
|
};
|
|
const cleanupAttempt = await options.resourceScope.release(owner);
|
|
if (cleanupAttempt.kind === "failed")
|
|
throw options.resourceScope.cleanupFailure({ kind: "completed" }, [
|
|
cleanupAttempt,
|
|
]);
|
|
}
|
|
return { format: result.format, digest: result.digest };
|
|
});
|
|
};
|
|
|
|
/** Classify a root and retain its admitted ZIP descriptor for child inventory. */
|
|
export const classifyAndHashRootForInventory = async (
|
|
path: string,
|
|
directory: boolean,
|
|
expectedMetadata: Stats,
|
|
options: Pick<ArtifactInventoryOptions, "resourceScope" | "signal">,
|
|
): Promise<RootInventoryClassification> => {
|
|
if (directory)
|
|
return { format: "directory", digest: null, rootSource: undefined };
|
|
const ownedHandle = await openRootArtifact(
|
|
path,
|
|
options.resourceScope,
|
|
options.signal,
|
|
);
|
|
const handle = ownedHandle.handle;
|
|
const owner: ArtifactResourceOwner = {
|
|
kind: "file-handle",
|
|
handle: ownedHandle,
|
|
resource: `root artifact descriptor for ${path}`,
|
|
};
|
|
let outcome:
|
|
| {
|
|
readonly kind: "completed";
|
|
readonly value: RootInventoryClassification;
|
|
}
|
|
| { readonly kind: "failed"; readonly cause: unknown };
|
|
try {
|
|
const initial = await handle.stat();
|
|
if (!sameRegularFileState(expectedMetadata, initial))
|
|
throw new ArtifactReaderFailure(
|
|
"integrity",
|
|
`Root artifact changed before inventory: ${path}`,
|
|
);
|
|
const format = await classifyRootFormat(path, handle, options.signal);
|
|
const digest = await hashStableRootArtifactHandle(
|
|
path,
|
|
handle,
|
|
initial,
|
|
options.signal,
|
|
);
|
|
const retainSource =
|
|
isZipPackageFormat(format) ||
|
|
format === "mach-o-universal" ||
|
|
format === "godot-pck";
|
|
outcome = {
|
|
kind: "completed",
|
|
value: {
|
|
format,
|
|
digest,
|
|
rootSource: retainSource
|
|
? { handle, initial, owner: ownedHandle }
|
|
: undefined,
|
|
},
|
|
};
|
|
} catch (cause: unknown) {
|
|
outcome = { kind: "failed", cause };
|
|
}
|
|
if (outcome.kind === "failed" || outcome.value.rootSource === undefined) {
|
|
const cleanupAttempt = await options.resourceScope.release(owner);
|
|
if (cleanupAttempt.kind === "failed")
|
|
throw options.resourceScope.cleanupFailure(outcome, [cleanupAttempt]);
|
|
}
|
|
if (outcome.kind === "failed") throw outcome.cause;
|
|
return outcome.value;
|
|
};
|
|
|
|
const classifyRootFormat = async (
|
|
path: string,
|
|
handle: FileHandle,
|
|
signal?: AbortSignal,
|
|
): Promise<ArtifactOccurrence["artifact_format"]> => {
|
|
const probe = createArtifactClassificationProbe();
|
|
const chunk = Buffer.alloc(64 * 1024);
|
|
while (probe.length < probe.limit) {
|
|
abortIfNeeded(signal);
|
|
const read = await handle.read(
|
|
chunk,
|
|
0,
|
|
Math.min(chunk.length, probe.limit - probe.length),
|
|
probe.length,
|
|
);
|
|
abortIfNeeded(signal);
|
|
if (read.bytesRead === 0) break;
|
|
probe.write(chunk.subarray(0, read.bytesRead));
|
|
}
|
|
const archiveBytes = (await handle.stat()).size;
|
|
const evidence = probe.finish(archiveBytes);
|
|
abortIfNeeded(signal);
|
|
const lower = path.toLowerCase();
|
|
if (hasZipSignature(evidence.prefix)) return zipRootPackageFormat(lower);
|
|
if (
|
|
lower.endsWith(".pkg") &&
|
|
evidence.prefix.subarray(0, 4).equals(Buffer.from("xar!"))
|
|
)
|
|
return "pkg";
|
|
if (
|
|
lower.endsWith(".dmg") &&
|
|
(await hasKolyTrailer(handle, archiveBytes, signal))
|
|
)
|
|
return "dmg";
|
|
if (await locateGodotPck(handle, archiveBytes, signal, evidence.prefix))
|
|
return "godot-pck";
|
|
return classifyArtifactContent(path, evidence, archiveBytes).format;
|
|
};
|
|
|
|
/** App Pack is a verified ZIP root role; nested .app members retain their observed format. */
|
|
const zipRootPackageFormat = (lower: string): ZipPackageFormat =>
|
|
lower.endsWith(".app")
|
|
? "app-pack"
|
|
: (zipPackageFormatForPath(lower) ?? "zip");
|
|
|
|
const hasKolyTrailer = async (
|
|
handle: FileHandle,
|
|
size: number,
|
|
signal?: AbortSignal,
|
|
): Promise<boolean> => {
|
|
if (size < 512) return false;
|
|
const trailer = Buffer.alloc(4);
|
|
let bytes = 0;
|
|
while (bytes < trailer.length) {
|
|
abortIfNeeded(signal);
|
|
const read = await handle.read(
|
|
trailer,
|
|
bytes,
|
|
trailer.length - bytes,
|
|
size - 512 + bytes,
|
|
);
|
|
abortIfNeeded(signal);
|
|
if (read.bytesRead === 0) return false;
|
|
bytes += read.bytesRead;
|
|
}
|
|
return trailer.equals(Buffer.from("koly"));
|
|
};
|