diff --git a/src/apktool/ApktoolConfiguration.ts b/src/apktool/ApktoolConfiguration.ts index fb6f271dc3..b8458ec412 100644 --- a/src/apktool/ApktoolConfiguration.ts +++ b/src/apktool/ApktoolConfiguration.ts @@ -522,17 +522,23 @@ const versionProbeFailure = ( ...apktoolVersionProbeTimeoutGuidance(), }, ); + // EPERM is the other errno a refused exec can surface (for example a launcher + // blocked by an operating-system security policy), so it joins EACCES as a + // permission failure, not a missing launcher. if ( cause instanceof Error && "code" in cause && - (cause.code === "ENOENT" || cause.code === "EACCES") - ) + (cause.code === "ENOENT" || + cause.code === "EACCES" || + cause.code === "EPERM") + ) { + const execDenied = cause.code === "EACCES" || cause.code === "EPERM"; return new ApktoolConfigurationFailure( - cause.code === "EACCES" ? "command_not_executable" : "command_missing", - cause.code === "EACCES" + execDenied ? "command_not_executable" : "command_missing", + execDenied ? `Execution of the selected apktool launcher was denied: ${selection.command}` : `The selected apktool launcher was not found: ${selection.command}`, - cause.code === "EACCES" + execDenied ? APKTOOL_NOT_EXECUTABLE_REMEDIATION : APKTOOL_MISSING_REMEDIATION, { @@ -542,6 +548,7 @@ const versionProbeFailure = ( }, { cause }, ); + } const snapshot = cause instanceof OwnedCommandFailure ? cause.snapshot : null; if (cause instanceof OwnedCommandFailure && cause.reason === "output-limit") return new AnalysisResourceConstraintError( diff --git a/src/apktool/ApktoolVersionProbe.test.ts b/src/apktool/ApktoolVersionProbe.test.ts index 81aa47b035..8786991d03 100644 --- a/src/apktool/ApktoolVersionProbe.test.ts +++ b/src/apktool/ApktoolVersionProbe.test.ts @@ -201,6 +201,7 @@ it.each(["linux", "win32"] as const)( it.each([ ["EACCES", "command_not_executable"], + ["EPERM", "command_not_executable"], ["ENOENT", "command_missing"], ])("distinguishes launcher spawn %s", async (code, expected) => { vi.mocked(runOwnedCommand).mockRejectedValueOnce(