mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 06:15:59 +00:00
Keep one typed accumulation of observed decode facts instead of constructing partial Evidence only after string projection. Record decoded client/target before the post-command cancellation checkpoint, parsed metadata before manifest reads, raw manifest before interpretation, and inventory prefix before rethrowing its exact original cause. Omit unexamined fields. Return inventory success or exact failure plus completed prefix through Result. Keep resource-cap stops distinct from physical failures. Attach truncation and inventory limitations at their producing stages. Validate complete results with the named domain schema; centralize partial Evidence assembly in the existing failure/cleanup boundary. Validation: six authored stage controls fail unchanged34a88d6c1; 131 focused checks across7files, check:fast, docs:check and discovery1556 pass. Covers manifest bytes/XML, interrupted inventory, late cleanup cancellation, denied removal/retry and existing table interruption. Two source reviewers clear after moving the command handoff checkpoint. No separate runtime qualification of that precise handoff race. Public CLI manifest failure retains metadata/raw manifest with unknown fields omitted and original failure framing unchanged. Twelve small normal CLI tasks retain exact replies. No speedup or memory-saving claim; failure reply estimate189 to610cl100k tokens preserves completed Evidence. No real Apktool, large-case memory or whole-agent qualification. Heavy tests remain stopped. Fixes #4882
402 lines
13 KiB
TypeScript
402 lines
13 KiB
TypeScript
import { countDecodedFiles } from "./ApktoolDecodedInventory.js";
|
|
import * as fs from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { execFile } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
import { afterEach, expect, it, vi } from "vitest";
|
|
import {
|
|
artifactFilePrimaryFailure,
|
|
ArtifactFileCleanupFailure,
|
|
ArtifactFileCancelled,
|
|
} from "../artifacts/ArtifactFileOperation.js";
|
|
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
|
import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
|
|
import { ApktoolCommandCleanup } from "./ApktoolCommandCleanup.js";
|
|
import { ApktoolWorkspace } from "./ApktoolWorkspace.js";
|
|
|
|
vi.mock("node:fs/promises", async (original) => {
|
|
const actual = await original<typeof import("node:fs/promises")>();
|
|
return {
|
|
...actual,
|
|
open: vi.fn(actual.open),
|
|
opendir: vi.fn(actual.opendir),
|
|
};
|
|
});
|
|
const actual =
|
|
await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises");
|
|
afterEach(() => {
|
|
vi.mocked(fs.open).mockImplementation(actual.open);
|
|
vi.mocked(fs.opendir).mockImplementation(actual.opendir);
|
|
});
|
|
|
|
it.each(["source", "snapshot", "output"] as const)(
|
|
"retains the exact %s descriptor after a denied close and fences root removal",
|
|
async (kind) => {
|
|
const root = await PrivateRuntimeRoot.create({
|
|
prefix: "rea-apktool-owned-test-",
|
|
});
|
|
const commands = new ApktoolCommandCleanup();
|
|
const workspace = new ApktoolWorkspace(root, commands);
|
|
const input = join(root.path, "source.apk");
|
|
const output = join(root.path, "output.xml");
|
|
await fs.writeFile(input, "small-authored-apk");
|
|
await fs.writeFile(output, "<resources/>");
|
|
const selected =
|
|
kind === "source"
|
|
? input
|
|
: kind === "snapshot"
|
|
? join(root.path, "input.apk")
|
|
: output;
|
|
let denied = true;
|
|
const closes = vi.fn();
|
|
vi.mocked(fs.open).mockImplementation(async (path, flags, mode) => {
|
|
const handle = await actual.open(path, flags, mode);
|
|
if (String(path) === selected) {
|
|
const close = handle.close.bind(handle);
|
|
handle.close = async () => {
|
|
closes();
|
|
if (denied) throw new Error("authored close denial");
|
|
await close();
|
|
};
|
|
}
|
|
return handle;
|
|
});
|
|
try {
|
|
const failure = await (
|
|
kind === "output"
|
|
? workspace.readText(output, 100)
|
|
: workspace.snapshotInput(input, "decode_android_resources")
|
|
).catch((cause: unknown) => cause);
|
|
expect(failure).toBeInstanceOf(ArtifactFileCleanupFailure);
|
|
if (!(failure instanceof ArtifactFileCleanupFailure))
|
|
throw new Error("Expected owned descriptor failure");
|
|
expect(failure.cleanupLease.released).toBe(false);
|
|
await expect(workspace.close()).rejects.toBeInstanceOf(
|
|
ArtifactReaderFailure,
|
|
);
|
|
await expect(fs.stat(root.path)).resolves.toBeDefined();
|
|
denied = false;
|
|
await workspace.close();
|
|
expect(failure.cleanupLease.released).toBe(true);
|
|
const count = closes.mock.calls.length;
|
|
await workspace.close();
|
|
expect(closes).toHaveBeenCalledTimes(count);
|
|
await expect(fs.stat(root.path)).rejects.toMatchObject({
|
|
code: "ENOENT",
|
|
});
|
|
} finally {
|
|
denied = false;
|
|
await workspace.close();
|
|
await commands.close();
|
|
}
|
|
},
|
|
);
|
|
|
|
it("retains a physical decode-output failure when cancellation arrives during descriptor close", async () => {
|
|
const root = await PrivateRuntimeRoot.create({
|
|
prefix: "rea-apktool-owned-test-",
|
|
});
|
|
const commands = new ApktoolCommandCleanup();
|
|
const workspace = new ApktoolWorkspace(root, commands);
|
|
const output = join(root.path, "invalid.xml");
|
|
const controller = new AbortController();
|
|
await fs.writeFile(output, Buffer.from([0xff]));
|
|
vi.mocked(fs.open).mockImplementation(async (path, flags, mode) => {
|
|
const handle = await actual.open(path, flags, mode);
|
|
const close = handle.close.bind(handle);
|
|
handle.close = async () => {
|
|
controller.abort({ caller: "after malformed bytes" });
|
|
await close();
|
|
};
|
|
return handle;
|
|
});
|
|
try {
|
|
await expect(
|
|
workspace.readText(output, 100, controller.signal),
|
|
).rejects.toMatchObject({ _tag: "AnalysisProtocolError" });
|
|
} finally {
|
|
await workspace.close();
|
|
await commands.close();
|
|
}
|
|
});
|
|
|
|
it("records cancellation observed during a successful output descriptor close", async () => {
|
|
const root = await PrivateRuntimeRoot.create({
|
|
prefix: "rea-apktool-owned-test-",
|
|
});
|
|
const commands = new ApktoolCommandCleanup();
|
|
const workspace = new ApktoolWorkspace(root, commands);
|
|
const output = join(root.path, "output.xml");
|
|
const controller = new AbortController();
|
|
const reason = { caller: "during descriptor close" };
|
|
await fs.writeFile(output, "<resources/>");
|
|
vi.mocked(fs.open).mockImplementation(async (path, flags, mode) => {
|
|
const handle = await actual.open(path, flags, mode);
|
|
const close = handle.close.bind(handle);
|
|
handle.close = async () => {
|
|
controller.abort(reason);
|
|
await close();
|
|
};
|
|
return handle;
|
|
});
|
|
try {
|
|
const failure = await workspace
|
|
.readText(output, 100, controller.signal)
|
|
.catch((cause: unknown) => cause);
|
|
expect(failure).toBeInstanceOf(ArtifactFileCancelled);
|
|
if (!(failure instanceof ArtifactFileCancelled))
|
|
throw new Error("Expected close cancellation");
|
|
expect(failure.fileCancellation.reason).toBe(reason);
|
|
expect(failure.fileCancellation.outcome).toEqual({
|
|
kind: "completed",
|
|
value: { text: "<resources/>", truncated: false, sourceBytes: 12 },
|
|
});
|
|
} finally {
|
|
await workspace.close();
|
|
await commands.close();
|
|
}
|
|
});
|
|
|
|
it.skipIf(process.platform === "win32")(
|
|
"rejects a decoded FIFO without waiting for a writer",
|
|
async () => {
|
|
const root = await PrivateRuntimeRoot.create({
|
|
prefix: "rea-apktool-owned-test-",
|
|
});
|
|
const commands = new ApktoolCommandCleanup();
|
|
const workspace = new ApktoolWorkspace(root, commands);
|
|
const fifo = join(root.path, "strings.xml");
|
|
try {
|
|
await promisify(execFile)("mkfifo", [fifo]);
|
|
await expect(workspace.readText(fifo, 100)).rejects.toMatchObject({
|
|
code: "ENOTFILE",
|
|
});
|
|
} finally {
|
|
await workspace.close();
|
|
await commands.close();
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each([false, true])(
|
|
"keeps malformed UTF-8 primary when final stat observes cancellation (close denied=%s)",
|
|
async (deniedInitially) => {
|
|
const root = await PrivateRuntimeRoot.create({
|
|
prefix: "rea-apktool-owned-test-",
|
|
});
|
|
const commands = new ApktoolCommandCleanup();
|
|
const workspace = new ApktoolWorkspace(root, commands);
|
|
const path = join(root.path, "malformed.xml");
|
|
const controller = new AbortController();
|
|
let denied = deniedInitially;
|
|
await fs.writeFile(path, Buffer.from([0xff]));
|
|
vi.mocked(fs.open).mockImplementation(async (name, flags, mode) => {
|
|
const handle = await actual.open(name, flags, mode);
|
|
const stat = handle.stat.bind(handle);
|
|
let calls = 0;
|
|
handle.stat = ((...args: Parameters<typeof handle.stat>) =>
|
|
stat(...args).then((value) => {
|
|
if (++calls === 3)
|
|
controller.abort({ caller: "after bytes before interpretation" });
|
|
return value;
|
|
})) as typeof handle.stat;
|
|
const close = handle.close.bind(handle);
|
|
handle.close = async () => {
|
|
if (denied) throw new Error("authored denial");
|
|
await close();
|
|
};
|
|
return handle;
|
|
});
|
|
try {
|
|
const failure = await workspace
|
|
.readText(path, 100, controller.signal)
|
|
.catch((cause: unknown) => cause);
|
|
expect(controller.signal.aborted).toBe(true);
|
|
expect(artifactFilePrimaryFailure(failure)).toMatchObject({
|
|
_tag: "AnalysisProtocolError",
|
|
});
|
|
if (denied)
|
|
await expect(workspace.close()).rejects.toBeInstanceOf(
|
|
ArtifactReaderFailure,
|
|
);
|
|
denied = false;
|
|
await workspace.close();
|
|
} finally {
|
|
denied = false;
|
|
await workspace.close();
|
|
await commands.close();
|
|
}
|
|
},
|
|
);
|
|
|
|
it.skipIf(process.platform === "win32")(
|
|
"counts omitted special entries toward the decoded inventory budget",
|
|
async () => {
|
|
const root = await PrivateRuntimeRoot.create({
|
|
prefix: "rea-apktool-owned-test-",
|
|
});
|
|
const commands = new ApktoolCommandCleanup();
|
|
const workspace = new ApktoolWorkspace(root, commands);
|
|
try {
|
|
await fs.writeFile(join(root.path, "one.xml"), "one");
|
|
await fs.writeFile(join(root.path, "two.xml"), "two");
|
|
for (let index = 0; index < 4; index++)
|
|
await promisify(execFile)("mkfifo", [
|
|
join(root.path, `special-${index}`),
|
|
]);
|
|
const result = await countDecodedFiles(
|
|
workspace,
|
|
root.path,
|
|
undefined,
|
|
3,
|
|
);
|
|
if (!result.ok) throw result.error.cause;
|
|
expect(result.value.capped).toBe(true);
|
|
expect(result.value.count).toBeLessThanOrEqual(2);
|
|
const complete = await countDecodedFiles(
|
|
workspace,
|
|
root.path,
|
|
undefined,
|
|
6,
|
|
);
|
|
if (!complete.ok) throw complete.error.cause;
|
|
expect(complete.value).toEqual({
|
|
count: 2,
|
|
capped: false,
|
|
locales: [],
|
|
otherStringsTables: [],
|
|
unreadableStringsTables: [],
|
|
});
|
|
} finally {
|
|
await workspace.close();
|
|
await commands.close();
|
|
}
|
|
},
|
|
);
|
|
|
|
it.skipIf(process.platform === "win32")(
|
|
"inventories only observed regular tables and discloses non-regular and unknown entries",
|
|
async () => {
|
|
const root = await PrivateRuntimeRoot.create({
|
|
prefix: "rea-apktool-tables-test-",
|
|
});
|
|
const commands = new ApktoolCommandCleanup();
|
|
const workspace = new ApktoolWorkspace(root, commands);
|
|
const regular = ["de", "en-rUS-v21", "mcc310-fr", "night", "pt-BR", ""];
|
|
const nonRegular = ["fr", "it", "es"];
|
|
try {
|
|
for (const suffix of [...regular, ...nonRegular, "ja"])
|
|
await fs.mkdir(join(root.path, "res", `values-${suffix}`), {
|
|
recursive: true,
|
|
});
|
|
for (const suffix of regular)
|
|
await fs.writeFile(
|
|
join(root.path, "res", `values-${suffix}`, "strings.xml"),
|
|
"<resources/>",
|
|
);
|
|
await fs.symlink(
|
|
join(root.path, "res/values-de/strings.xml"),
|
|
join(root.path, "res/values-fr/strings.xml"),
|
|
);
|
|
await promisify(execFile)("mkfifo", [
|
|
join(root.path, "res/values-it/strings.xml"),
|
|
]);
|
|
await fs.mkdir(join(root.path, "res/values-es/strings.xml"));
|
|
const result = await countDecodedFiles(
|
|
workspace,
|
|
root.path,
|
|
undefined,
|
|
100,
|
|
);
|
|
if (!result.ok) throw result.error.cause;
|
|
expect(result.value).toEqual({
|
|
count: regular.length,
|
|
capped: false,
|
|
locales: ["de"],
|
|
otherStringsTables: [
|
|
"values-",
|
|
"values-en-rUS-v21",
|
|
"values-mcc310-fr",
|
|
"values-night",
|
|
"values-pt-BR",
|
|
],
|
|
unreadableStringsTables: [
|
|
{ path: "res/values-es/strings.xml", kind: "directory" },
|
|
{ path: "res/values-fr/strings.xml", kind: "symlink" },
|
|
{ path: "res/values-it/strings.xml", kind: "fifo" },
|
|
],
|
|
});
|
|
} finally {
|
|
await workspace.close();
|
|
await commands.close();
|
|
}
|
|
},
|
|
);
|
|
|
|
it("preserves a directory inspection failure instead of treating its locale table as absent", async () => {
|
|
const root = await PrivateRuntimeRoot.create({
|
|
prefix: "rea-apktool-tables-test-",
|
|
});
|
|
const commands = new ApktoolCommandCleanup();
|
|
const workspace = new ApktoolWorkspace(root, commands);
|
|
const blocked = join(root.path, "res/values-fr");
|
|
try {
|
|
await fs.mkdir(blocked, { recursive: true });
|
|
await fs.writeFile(join(root.path, "known.txt"), "completed");
|
|
await fs.writeFile(join(blocked, "strings.xml"), "<resources/>");
|
|
vi.mocked(fs.opendir).mockImplementation(async (path, options) => {
|
|
if (String(path) === blocked)
|
|
throw Object.assign(new Error("authored directory inspection denial"), {
|
|
code: "EACCES",
|
|
});
|
|
return actual.opendir(path, options);
|
|
});
|
|
await expect(
|
|
countDecodedFiles(workspace, root.path, undefined, 100),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
partial: { count: 1 },
|
|
cause: {
|
|
reason: "io",
|
|
message: expect.stringContaining(
|
|
"authored directory inspection denial",
|
|
),
|
|
},
|
|
},
|
|
});
|
|
} finally {
|
|
await workspace.close();
|
|
await commands.close();
|
|
}
|
|
});
|
|
|
|
it("reports raw retained-prefix bytes when UTF-8 decoding removes a BOM", async () => {
|
|
const root = await PrivateRuntimeRoot.create({
|
|
prefix: "rea-apktool-source-bytes-",
|
|
});
|
|
const commands = new ApktoolCommandCleanup();
|
|
const workspace = new ApktoolWorkspace(root, commands);
|
|
const output = join(root.path, "strings.xml");
|
|
const bytes = Buffer.from("\uFEFF<resources/>", "utf8");
|
|
try {
|
|
await fs.writeFile(output, bytes);
|
|
await expect(workspace.readText(output, 100)).resolves.toEqual({
|
|
text: "<resources/>",
|
|
truncated: false,
|
|
sourceBytes: bytes.length,
|
|
});
|
|
await expect(workspace.readText(output, 5)).resolves.toEqual({
|
|
text: "<r",
|
|
truncated: true,
|
|
sourceBytes: 5,
|
|
});
|
|
await expect(
|
|
workspace.readText(join(root.path, "missing.xml"), 100),
|
|
).resolves.toEqual({ text: null, truncated: false, sourceBytes: 0 });
|
|
} finally {
|
|
await workspace.close();
|
|
await commands.close();
|
|
}
|
|
});
|