Files
reverse-engineering-anything/src/artifacts/inventory/classify.ts
T
5fa4defcbc feat(artifacts): inspect and extract Godot PCK with retained facts (#3634)
* 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>
2026-10-20 09:53:53 +08:00

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"));
};