Files
reverse-engineering-anything/src/apktool/ApktoolStringTables.test.ts
T
morluto cb4e5cfbff feat(analysis): make resource budgets recoverable across CLI and MCP (#9706)
* refactor(analysis): centralize resource budgets and recovery facts

* feat(analysis): expose ASAR and Ghidra runtime admission controls

* feat(artifacts): expose reader admission and preserve limited recognition

* feat(javascript): expose recoverable semantic precision budgets

* feat(web): add recoverable source-map budgets and compact retained views

* feat(web): make historical capture decoding budgets recoverable

* fix(desktop): enforce selected view policies across capture and export

* feat(runtime): make Frida and LLDB retention budgets selectable

* feat(native): expose diagnostic budgets and preserve exhaustion causes

* feat(managed): expose recoverable snapshot and representation budgets

* feat(metadata): make Go and Rust inspection budgets recoverable

* feat(wasm): expose representation and producer admissions

* feat(evm): expose per-call worker resource budgets

* feat(android): make resource and string-pool omissions recoverable

* feat(producers): expose command and recorded-capture budgets

* docs(analysis): record recovery contracts and integrated validation

* fix(docs): link calibration oracle from published verification pages

* fix(docs): restore desktop task anchors and receipt links

* test(analysis): align fixtures with selectable budget contracts

* fix(analysis): remove source-location projector dependency cycle

* test(analysis): refresh budget recovery and verifier regressions

* test(analysis): preserve format and publication recovery expectations

* test(windows): align conformance with owned runtime admission

* fix(desktop): preserve selected alias kinds in exported state

* test(analysis): align provider fixtures with recoverable budgets

* test(desktop): validate emitted alias metadata structurally

* fix(ghidra): classify bounded Windows endpoint refusals

* test(cli): assert selectable artifact admission recovery

* fix(cli): preserve capture provenance in environment builds
2026-11-10 05:11:20 -05:00

346 lines
10 KiB
TypeScript

import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
DEFAULT_APKTOOL_TOTAL_STRINGS,
DEFAULT_APKTOOL_STRING_LIMIT,
} from "../domain/apktool/apktoolResourceAnalysis.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { projectApktoolStringTables } from "./ApktoolStringTables.js";
const xml = (count: number) =>
`<resources>${Array.from({ length: count }, (_, index) => `<string name="s_${String(index)}">value</string>`).join("")}</resources>`;
const textRead = (text: string) => ({
text,
truncated: false,
sourceBytes: Buffer.byteLength(text, "utf8"),
});
it("charges table-only output without reserving a duplicate primary copy", async () => {
const paths: string[] = [];
const outcome = await projectApktoolStringTables(
{
async readText(path) {
paths.push(path);
return textRead(xml(DEFAULT_APKTOOL_STRING_LIMIT));
},
},
"decoded",
{
include_strings: true,
locales: ["de", "es", "it", "fr"],
string_tables_only: true,
},
);
if (!outcome.ok) throw outcome.error.cause;
expect(paths).toHaveLength(5);
expect(outcome.value.strings).toEqual([]);
expect(outcome.value.tables.map((table) => table.status)).toEqual(
Array.from({ length: 5 }, () => "present"),
);
expect(
outcome.value.tables.reduce((sum, table) => sum + table.strings.length, 0),
).toBe(DEFAULT_APKTOOL_TOTAL_STRINGS);
expect(outcome.value.coverage).toBe("complete");
});
describe("Apktool locale alias table selection", () => {
it.each([false, true])(
"reads one physical table for two caller-selected aliases (table-only: %s)",
async (tablesOnly) => {
const paths: string[] = [];
const outcome = await projectApktoolStringTables(
{
async readText(path) {
paths.push(path);
return textRead(xml(1));
},
},
"decoded",
{
include_strings: true,
locale: "pt-BR",
locales: ["pt-rBR"],
string_tables_only: tablesOnly,
},
);
if (!outcome.ok) throw outcome.error.cause;
const result = outcome.value;
expect(paths).toEqual([
join("decoded", "res", "values-pt-rBR", "strings.xml"),
]);
expect(result.tables.map((table) => table.locale)).toEqual([
"pt-BR",
"pt-rBR",
]);
expect(result.tables.map((table) => table.total_count)).toEqual([1, 1]);
},
);
it.each([
["es-419", "b+es+419"],
["en-001", "b+en+001"],
["ar-015", "b+ar+015"],
])(
"reads the three-digit region %s from the b+ table Apktool writes",
async (locale, table) => {
const paths: string[] = [];
const outcome = await projectApktoolStringTables(
{
async readText(path) {
paths.push(path);
return textRead(xml(1));
},
},
"decoded",
{ include_strings: true, locale, locales: [table] },
);
if (!outcome.ok) throw outcome.error.cause;
const result = outcome.value;
expect(paths).toEqual([
join("decoded", "res", `values-${table}`, "strings.xml"),
]);
expect(result.tables.map((entry) => entry.locale)).toEqual([
locale,
table,
]);
expect(result.tables.map((entry) => entry.status)).toEqual([
"present",
"present",
]);
},
);
});
describe("physical Apktool string facts", () => {
it("applies the selected page before aggregate allocation while preserving alias counts", async () => {
const result = await projectApktoolStringTables(
{ readText: async () => textRead(xml(4)) },
"decoded",
{
include_strings: true,
locale: "pt-BR",
locales: ["pt-rBR"],
string_tables_only: true,
string_offset: 1,
string_limit: 2,
max_total_strings: 3,
},
);
if (!result.ok) throw result.error.cause;
expect(result.value.tables).toMatchObject([
{
strings: [{ name: "s_1" }, { name: "s_2" }],
total_count: 4,
page: { offset: 1, returned_count: 2, next_offset: 3 },
coverage: "partial",
},
{
strings: [{ name: "s_1" }],
total_count: 4,
page: { offset: 1, returned_count: 1, next_offset: 2 },
coverage: "partial",
},
]);
});
it("counts complete source entries beyond the retained table prefix", async () => {
const outcome = await projectApktoolStringTables(
{
async readText() {
return textRead(
xml(DEFAULT_APKTOOL_STRING_LIMIT + 1).replace(
"</resources>",
'<string name="styled"><b>omitted</b></string></resources>',
),
);
},
},
"decoded",
{ include_strings: true },
);
if (!outcome.ok) throw outcome.error.cause;
const result = outcome.value;
expect(result.tables[0]).toMatchObject({
total_count: DEFAULT_APKTOOL_STRING_LIMIT + 1,
unparsed_count: 1,
coverage: "partial",
});
expect(result.strings).toHaveLength(DEFAULT_APKTOOL_STRING_LIMIT);
expect(result.limitations).toContain(
`strings reports the first ${String(DEFAULT_APKTOOL_STRING_LIMIT)} of ${String(DEFAULT_APKTOOL_STRING_LIMIT + 1)} entries`,
);
});
it("does not read unexamined tables after output exhaustion", async () => {
const paths: string[] = [];
const outcome = await projectApktoolStringTables(
{
async readText(path) {
paths.push(path);
if (path.includes("values-fr"))
throw new Error("Unexamined table was read");
return textRead(xml(DEFAULT_APKTOOL_STRING_LIMIT));
},
},
"decoded",
{ include_strings: true, locales: ["de", "es", "it", "fr"] },
);
if (!outcome.ok) throw outcome.error.cause;
const result = outcome.value;
expect(paths).toHaveLength(4);
expect(result.tables.at(-1)).toEqual({
locale: "fr",
status: "skipped",
strings: [],
total_count: null,
unparsed_count: null,
coverage: "partial",
});
expect(
result.strings.length +
result.tables.reduce((count, table) => count + table.strings.length, 0),
).toBe(DEFAULT_APKTOOL_TOTAL_STRINGS);
});
it("keeps a known empty alias present after the output budget is spent", async () => {
const paths: string[] = [];
const outcome = await projectApktoolStringTables(
{
async readText(path) {
paths.push(path);
return textRead(
xml(
path.includes("values-pt-rBR") ? 0 : DEFAULT_APKTOOL_STRING_LIMIT,
),
);
},
},
"decoded",
{
include_strings: true,
locale: "pt-BR",
locales: ["de", "es", "it", "fr", "en", "pt-rBR"],
},
);
if (!outcome.ok) throw outcome.error.cause;
const result = outcome.value;
expect(paths.filter((path) => path.includes("values-pt-rBR"))).toHaveLength(
1,
);
expect(result.tables.at(-1)).toEqual({
locale: "pt-rBR",
status: "present",
strings: [],
total_count: 0,
unparsed_count: 0,
coverage: "complete",
});
expect(result.coverage).toBe("complete");
});
});
describe("Apktool string projection admission", () => {
it.each([false, true])(
"retains completed tables when cancellation arrives before the next selection (table-only: %s)",
async (tablesOnly) => {
const controller = new AbortController();
const reason = { selected: "caller" };
const paths: string[] = [];
const result = await projectApktoolStringTables(
{
async readText(path) {
paths.push(path);
controller.abort(reason);
return textRead(xml(1));
},
},
"decoded",
{
include_strings: true,
locales: ["de", "fr"],
string_tables_only: tablesOnly,
},
controller.signal,
);
if (result.ok) throw new Error("Expected cancellation");
expect(result.error.cause).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toHaveProperty("cause", reason);
expect(paths).toHaveLength(1);
expect(result.error.partial).toMatchObject({
coverage: "partial",
tables: [{ locale: null, total_count: 1 }],
});
expect(result.error.partial.strings).toHaveLength(tablesOnly ? 0 : 1);
expect(result.error.partial.limitations).toContain(
'String projection stopped at locale "de"; this selection and 1 remaining selections were not projected',
);
},
);
it.each([false, true])(
"honors omitted strings without reading any selected table (table-only: %s)",
async (tablesOnly) => {
const outcome = await projectApktoolStringTables(
{
async readText() {
throw new Error("Unexpected read");
},
},
"decoded",
{
include_strings: false,
locale: "de",
locales: ["fr"],
string_tables_only: tablesOnly,
},
);
expect(outcome).toEqual({
ok: true,
value: {
strings: [],
tables: [],
limitations: [],
coverage: "complete",
},
});
},
);
it("preserves the exact physical read failure", async () => {
const cause = new Error("Physical read and close failed");
const result = await projectApktoolStringTables(
{
async readText() {
throw cause;
},
},
"decoded",
{ include_strings: true },
);
if (result.ok) throw new Error("Expected read failure");
expect(result.error.cause).toBe(cause);
expect(result.error.partial.coverage).toBe("partial");
});
it("preserves opaque cancellation before any table read", async () => {
const reason = { selected: "caller" };
const pending = projectApktoolStringTables(
{
async readText() {
throw new Error("Unexpected read");
},
},
"decoded",
{ include_strings: true },
AbortSignal.abort(reason),
);
const result = await pending;
if (result.ok) throw new Error("Expected cancellation");
expect(result.error.cause).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toHaveProperty("cause", reason);
});
});