Files
reverse-engineering-anything/src/apktool/ApktoolDecodedInventory.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

84 lines
2.7 KiB
TypeScript

import { ok, err, type Result } from "../domain/result.js";
import type {
ArtifactEntry,
ArtifactTraversalOmission,
} from "../artifacts/ArtifactReader.js";
import { DirectoryArtifactTraversalLimit } from "../artifacts/DirectoryArtifactReader.js";
import { assertArtifactReadActive } from "../artifacts/ArtifactFileOperation.js";
import { classifyAndroidStringsTable } from "../domain/apktool/apktoolResourceAnalysis.js";
import type { ApktoolWorkspace } from "./ApktoolWorkspace.js";
type TableEntryKind = ArtifactEntry["kind"] | ArtifactTraversalOmission["kind"];
interface StringsTableOmission {
readonly path: string;
readonly kind: Exclude<TableEntryKind, "file">;
}
interface DecodedInventory {
count: number;
capped: boolean;
locales: readonly string[];
otherStringsTables: readonly string[];
unreadableStringsTables: readonly StringsTableOmission[];
}
/** Count files over bounded examined coverage, including omitted special entries. */
export const countDecodedFiles = async (
workspace: ApktoolWorkspace,
root: string,
signal: AbortSignal | undefined,
maximumEntries: number,
): Promise<
Result<
DecodedInventory,
{ readonly cause: unknown; readonly partial: DecodedInventory }
>
> => {
let count = 0;
let capped = false;
const locales: string[] = [];
const otherStringsTables: string[] = [];
const unreadableStringsTables: StringsTableOmission[] = [];
const observeTable = (path: string, kind: TableEntryKind): void => {
const match = /^res\/(values(?:-([^/]*))?)\/strings\.xml$/u.exec(path);
if (match === null) return;
if (kind !== "file") {
unreadableStringsTables.push({ path, kind });
return;
}
const suffix = match[2];
if (suffix === undefined) return;
const table = classifyAndroidStringsTable(suffix);
if (table.kind === "locale") locales.push(table.locale);
else otherStringsTables.push(match[1]!);
};
const observation = (): DecodedInventory => {
return {
count,
capped,
locales: locales.sort(),
otherStringsTables: otherStringsTables.sort(),
unreadableStringsTables: unreadableStringsTables.sort((a, b) =>
a.path < b.path ? -1 : a.path > b.path ? 1 : 0,
),
};
};
try {
for await (const entry of workspace.entries(
root,
signal,
maximumEntries,
(omission) => observeTable(omission.path, omission.kind),
)) {
assertArtifactReadActive(signal);
observeTable(entry.path, entry.kind);
if (entry.kind === "file") count += 1;
}
} catch (cause) {
if (!(cause instanceof DirectoryArtifactTraversalLimit))
return err({ cause, partial: observation() });
capped = true;
}
return ok(observation());
};