mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 00:36:35 +00:00
* feat(artifacts): read UnityFS AssetBundle containers inspect-artifact and extract-artifact now recognize the UnityFS signature and treat format versions 6 through 9 as containers. Header facts (format version, player/engine strings, declared size, dataflags, blocks-info bounds and the declared 16-byte data hash) are retained alongside a directory of CAB-* and .resS members. Member bytes stream on demand: only the blocks covering the requested member are read and decompressed with bounded expansion and cancellation. Bundles declaring asset-bundle encryption keep header facts and report unknown directory coverage. LZMA blocks-info reports unknown coverage; LZMA member reads fail as unavailable while the directory stays readable. Malformed counts, out-of-stream members and declared sizes beyond the file fail as format errors. UnityFS classifies by bytes in root detection, inventory graph construction and desktop inspection, which emits a unityfs-header fact. Validation: tsc --noEmit, oxlint (no new findings on touched files), 17 reader tests covering LZ4HC/uncompressed tables, blocks-info at end, format 6 without padding alignment, multi-block members, malformed input, unsupported compression, cancellation and abandoned streams; full artifacts/domain/conformance suites pass. Verified against one real YooAsset bundle (Unity 2021.3.32f1, format 8, LZ4HC): the streamed SerializedFile contained its managed assembly byte-identical to a UnityPy extraction. Other producer variants unverified. * fix(unityfs): own bounded member reads and preserve partial evidence * refactor(unityfs): clarify producer revision flag decisions * fix(artifacts): preserve coverage authority and bound UnityFS block work --------- Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
100 lines
3.9 KiB
TypeScript
100 lines
3.9 KiB
TypeScript
import { readFile, rm, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect, it } from "vitest";
|
|
import { parseEvidence } from "../../../src/domain/evidence.js";
|
|
import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js";
|
|
import { artifactExtractionResultSchema } from "../../../src/domain/artifactGraph.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
|
import { buildUnityFs } from "../../fixtures/unityfs.js";
|
|
|
|
it.each(["full", "compact"] as const)(
|
|
"retains UnityFS facts and unsupported content through compiled MCP (%s)",
|
|
async (profile) => {
|
|
const root = await createTestTempDirectory("rea-unity-mcp-");
|
|
const path = join(root, "bundle");
|
|
const payload = Buffer.from("serialized bytes");
|
|
await writeFile(path, buildUnityFs([{ path: "CAB", data: payload }]));
|
|
const { call } = await connectLocalToolsMcp({
|
|
REA_MCP_INPUT_SCHEMA_PROFILE: profile,
|
|
});
|
|
let outputRoot: string | undefined;
|
|
try {
|
|
expect((await call("open_binary", { path })).isError).not.toBe(true);
|
|
const reply = await call("inspect_artifact", {});
|
|
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
|
|
const inspection = artifactInspectionResultSchema.parse(
|
|
parseEvidence(reply.structuredContent).normalized_result,
|
|
);
|
|
expect(
|
|
inspection.substeps[0]!.evidence.normalized_result.occurrences,
|
|
).toContainEqual(
|
|
expect.objectContaining({
|
|
logical_path: "CAB",
|
|
hash_status: "verified",
|
|
source_location: null,
|
|
}),
|
|
);
|
|
const extracted = await call("extract_artifact", {});
|
|
expect(extracted.isError, JSON.stringify(extracted)).not.toBe(true);
|
|
const result = artifactExtractionResultSchema.parse(
|
|
parseEvidence(extracted.structuredContent).normalized_result,
|
|
);
|
|
outputRoot = result.output_root;
|
|
expect((await readFile(join(outputRoot, "CAB"))).equals(payload)).toBe(
|
|
true,
|
|
);
|
|
await call("close_binary", {});
|
|
await writeFile(
|
|
path,
|
|
buildUnityFs([{ path: "CAB", data: payload }], {
|
|
engineVersion: "0.0.0",
|
|
flags: 0x243,
|
|
}),
|
|
);
|
|
await call("open_binary", { path });
|
|
const unknownReply = await call("inspect_artifact", {});
|
|
expect(unknownReply.isError, JSON.stringify(unknownReply)).not.toBe(true);
|
|
const unknown = artifactInspectionResultSchema.parse(
|
|
parseEvidence(unknownReply.structuredContent).normalized_result,
|
|
).substeps[0]!.evidence.normalized_result;
|
|
expect(unknown.coverage).toMatchObject({ status: "unknown" });
|
|
expect(unknown.occurrences).toMatchObject([
|
|
{
|
|
format_metadata: {
|
|
directory_status: "unsupported-layout",
|
|
block_count: null,
|
|
data_start: null,
|
|
},
|
|
},
|
|
]);
|
|
await call("close_binary", {});
|
|
await writeFile(
|
|
path,
|
|
buildUnityFs([{ path: "CAB-lzma", data: payload }], {
|
|
blockCompression: 1,
|
|
}),
|
|
);
|
|
await call("open_binary", { path });
|
|
const partialReply = await call("inspect_artifact", {});
|
|
expect(partialReply.isError, JSON.stringify(partialReply)).not.toBe(true);
|
|
const partial = artifactInspectionResultSchema.parse(
|
|
parseEvidence(partialReply.structuredContent).normalized_result,
|
|
).substeps[0]!.evidence.normalized_result;
|
|
expect(partial.coverage).toMatchObject({ status: "unknown" });
|
|
expect(partial.occurrences).toContainEqual(
|
|
expect.objectContaining({
|
|
logical_path: "CAB-lzma",
|
|
hash_status: "unavailable",
|
|
artifact_id: null,
|
|
}),
|
|
);
|
|
await call("close_binary", {});
|
|
} finally {
|
|
if (outputRoot !== undefined)
|
|
await rm(outputRoot, { recursive: true, force: true });
|
|
}
|
|
},
|
|
60_000,
|
|
);
|