Files
reverse-engineering-anything/src/apktool/ApktoolWorkspace.lifecycle.test.ts
T
morluto f5fcf5216f fix(apktool): retain decode facts across interrupted read stages
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
2026-10-18 04:36:30 +08:00

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