From e5ee3ac658cd10ba23700e1757c86ce0a4520406 Mon Sep 17 00:00:00 2001 From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:36:21 +0800 Subject: [PATCH] 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> --- docs/apktool-resource-analysis.md | 2 ++ scripts/verify-apktool.mjs | 10 ++++++++++ src/apktool/ApktoolProvider.test.ts | 28 ++++++++++++++++++++++++---- src/apktool/ApktoolProvider.ts | 1 + 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/docs/apktool-resource-analysis.md b/docs/apktool-resource-analysis.md index 447d6ccfc5..a1c9ed6def 100644 --- a/docs/apktool-resource-analysis.md +++ b/docs/apktool-resource-analysis.md @@ -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. diff --git a/scripts/verify-apktool.mjs b/scripts/verify-apktool.mjs index aaec8a7ac4..98ccfd52c7 100644 --- a/scripts/verify-apktool.mjs +++ b/scripts/verify-apktool.mjs @@ -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; }; diff --git a/src/apktool/ApktoolProvider.test.ts b/src/apktool/ApktoolProvider.test.ts index 386d96ad12..938e08dd21 100644 --- a/src/apktool/ApktoolProvider.test.ts +++ b/src/apktool/ApktoolProvider.test.ts @@ -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, diff --git a/src/apktool/ApktoolProvider.ts b/src/apktool/ApktoolProvider.ts index 1c33d515af..84d1c8d2fd 100644 --- a/src/apktool/ApktoolProvider.ts +++ b/src/apktool/ApktoolProvider.ts @@ -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 &&