fix(apktool): name host requirements in configuration refusals (#1946)

A missing apktool launcher projected the generic "unavailable for the
current target" message and remediation, leaving the actual reason only
in details.reason. A relative REA_APKTOOL_COMMAND escaped as UNKNOWN
because command selection ran before the try that maps configuration
failures.

Resolve the selection inside that try for both execution and
availability, and pass the refusal reason as userMessage, as JADX and
the adapters fixed in #1224 do.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822
2026-10-12 04:13:23 +08:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 0ae38e1e31
commit 7cdb7981c3
2 changed files with 46 additions and 16 deletions
+15 -9
View File
@@ -153,12 +153,13 @@ const runApktool = async (
invocation = apktoolInvocation(selection, arguments_, hostEnvironment);
} catch (cause) {
if (!(cause instanceof WindowsBatchCommandError)) throw cause;
const reason = `The apktool batch launcher cannot receive REA's decode paths through cmd.exe: ${cause.message}`;
return err(
new AnalysisCapabilityUnavailableError(
APKTOOL_PROVIDER_IDENTITY.id,
operation,
`The apktool batch launcher cannot receive REA's decode paths through cmd.exe: ${cause.message}`,
{ cause },
reason,
{ cause, userMessage: reason },
),
);
}
@@ -345,8 +346,8 @@ export class ApktoolProvider {
async #inspectAvailability(
signal: AbortSignal,
): Promise<ProviderAvailability> {
const selection = this.selection();
try {
const selection = this.selection();
await inspectApktoolClient(selection, signal, this.environment);
throwIfCancelled("inspect_apktool_client", signal);
return {
@@ -397,9 +398,10 @@ export class ApktoolProvider {
): Promise<Result<AnalysisExecution, AnalysisError>> {
if (signal?.aborted === true)
return err(new AnalysisCancelledError(request.operation));
const selection = this.selection();
let selection: ApktoolCommandSelection;
let identity: ApktoolClientIdentity;
try {
selection = this.selection();
identity = await inspectApktoolClient(
selection,
signal,
@@ -407,17 +409,21 @@ export class ApktoolProvider {
);
} catch (cause) {
if (cause instanceof AnalysisCancelledError) return err(cause);
if (cause instanceof ApktoolConfigurationFailure)
if (cause instanceof ApktoolConfigurationFailure) {
// A host configuration refusal; the target is not the problem.
const reason =
typeof cause.diagnostics.remediation === "string"
? `${cause.message}. ${cause.diagnostics.remediation}.`
: `${cause.message}.`;
return err(
new AnalysisCapabilityUnavailableError(
APKTOOL_PROVIDER_IDENTITY.id,
request.operation,
typeof cause.diagnostics.remediation === "string"
? `${cause.message} ${cause.diagnostics.remediation}`
: cause.message,
{ cause },
reason,
{ cause, userMessage: reason },
),
);
}
return err(
new AnalysisProtocolError(
`apktool version probe failed for ${request.operation}`,
+31 -7
View File
@@ -12,13 +12,37 @@ it.each(["inspect-apktool-client", "decode-android-resources"])(
},
);
it("reports a missing apktool launcher with recovery guidance", async () => {
const result = await serve(["inspect-apktool-client", "--json"], {
REA_APKTOOL_COMMAND: "/nonexistent/apktool",
});
expect(result.stdout).toContain("capability_unavailable");
expect(result.stdout).toContain("REA_APKTOOL_COMMAND");
});
it.each([
[
["inspect-apktool-client", "--json"],
"/nonexistent/apktool",
"The selected apktool launcher was not found: /nonexistent/apktool. Set REA_APKTOOL_COMMAND",
],
[
["decode-android-resources", "/targets/app.apk", "--json"],
"/nonexistent/apktool",
"The selected apktool launcher was not found: /nonexistent/apktool. Set REA_APKTOOL_COMMAND",
],
[
["decode-android-resources", "/targets/app.apk", "--json"],
"relative/apktool",
"REA_APKTOOL_COMMAND must be an absolute path to the apktool launcher.",
],
])(
"%j with REA_APKTOOL_COMMAND=%s names the host requirement",
async (argv, command, reason) => {
const result = await serve(argv, { REA_APKTOOL_COMMAND: command });
const output = JSON.parse(result.stdout) as {
code?: unknown;
message?: unknown;
remediation?: { action?: unknown };
};
expect(output.code).toBe("capability_unavailable");
// The target is not the problem; the refusal must say what to configure.
expect(output.message).toEqual(expect.stringContaining(reason));
expect(output.remediation?.action).toEqual(expect.stringContaining(reason));
},
);
it("validates the locale option before launching apktool", async () => {
const result = await serve([