mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 18:52:48 +00:00
* 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
346 lines
10 KiB
TypeScript
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);
|
|
});
|
|
});
|