fix(apktool): preserve decoded APK Evidence subject (#4771)

Bind shared CLI/MCP Evidence to the digest already computed from the owned
APK snapshot. Exercise source mutation during decoding through the shared
application workflow and verify subject identity in the real Apktool lane.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
This commit is contained in:
Ysopwnable
2026-10-17 19:36:21 +08:00
committed by GitHub
parent 969aafcf28
commit e5ee3ac658
4 changed files with 37 additions and 4 deletions
+2
View File
@@ -52,6 +52,8 @@ select Android's `values-pt-rBR`; Android qualifiers such as `pt-rBR` and
- **An owned APK snapshot is digested and decoded.** `target.sha256` and
`target.bytes` bind the observation to the exact APK that was decoded.
The Evidence subject carries the same snapshot digest and caller-selected
path, including when the original file changes during decoding.
- **Metadata comes from apktool.yml as apktool printed it.** Version codes
keep their own spelling (`'7'` versus `7`); `package_name` is read from
the decoded manifest, not inferred.
+10
View File
@@ -33,6 +33,16 @@ const run = async (operation, input) => {
failures += 1;
return null;
}
if (operation === "decode_android_resources") {
const evidence = result.value;
const target = evidence.normalized_result.target;
check(
"resource Evidence preserves the decoded APK subject identity",
evidence.subject?.digest.sha256 === target.sha256 &&
evidence.subject?.local_path === target.path &&
evidence.subject?.format === "apk",
);
}
return result.value.normalized_result;
};
+24 -4
View File
@@ -11,8 +11,12 @@ import { join } from "node:path";
import { describe, expect, onTestFinished, test } from "vitest";
import { ApktoolProvider } from "./ApktoolProvider.js";
import { ApktoolResourceAnalysisService } from "../application/apktool/ApktoolResourceAnalysisService.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { apktoolRequestSchema } from "../domain/apktool/apktoolResourceAnalysis.js";
import {
apktoolRequestSchema,
apktoolResultSchemas,
} from "../domain/apktool/apktoolResourceAnalysis.js";
import type { ApktoolRequest } from "../domain/apktool/apktoolResourceAnalysis.js";
const it = test.skipIf(process.platform === "win32");
@@ -491,10 +495,26 @@ it("uses an owned APK snapshot and framework cache and resolves regional locales
),
);
const apk = await apkFixture(root);
const result = await evidenceResult(providerOver(command), {
operation: "decode_android_resources",
input: { path: apk.path, include_strings: true, locale: "pt-BR" },
const observation = await new ApktoolResourceAnalysisService(
providerOver(command),
).execute("decode_android_resources", {
path: apk.path,
include_strings: true,
locale: "pt-BR",
});
if (!observation.ok) throw observation.error;
expect(observation.value.subject).toMatchObject({
local_path: apk.path,
format: "apk",
digest: { sha256: apk.sha256 },
});
expect(observation.value.limitations).not.toContain(
"Artifact identity is unavailable for this observation.",
);
expect(await readFile(apk.path, "utf8")).toBe("changed original");
const result = apktoolResultSchemas.decode_android_resources.parse(
observation.value.normalized_result,
);
expect(result.target).toMatchObject({
sha256: apk.sha256,
bytes: apk.bytes,
+1
View File
@@ -801,6 +801,7 @@ export class ApktoolProvider {
},
provider,
{
subject: { path: input.path, sha256, format: "apk" },
limitations: [
"Decoding skips smali (--no-src); Java-source recovery belongs to the JADX family",
...(input.locale !== undefined &&