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 &&