diff --git a/README.md b/README.md index cd5e805..2ebc740 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,12 @@ A Claude Code skill that decompiles Android APK/XAPK/JAR/AAR files and **extract ## Requirements **Required:** + - Java JDK 17+ - [jadx](https://github.com/skylot/jadx) (CLI) **Optional (recommended):** + - [Vineflower](https://github.com/Vineflower/vineflower) or [Fernflower](https://github.com/JetBrains/fernflower) — better output on complex Java code - [dex2jar](https://github.com/ThexXTURBOXx/dex2jar) — needed to use Fernflower on APK/DEX files @@ -28,7 +30,7 @@ See `plugins/android-reverse-engineering/skills/android-reverse-engineering/refe Inside Claude Code, run: -``` +```text /plugin marketplace add SimoneAvogadro/android-reverse-engineering-skill /plugin install android-reverse-engineering@android-reverse-engineering-skill ``` @@ -43,7 +45,7 @@ git clone https://github.com/SimoneAvogadro/android-reverse-engineering-skill.gi Then in Claude Code: -``` +```text /plugin marketplace add /path/to/android-reverse-engineering-skill /plugin install android-reverse-engineering@android-reverse-engineering-skill ``` @@ -52,7 +54,7 @@ Then in Claude Code: ### Slash command -``` +```text /decompile path/to/app.apk ``` @@ -100,7 +102,7 @@ bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scri ## Repository Structure -``` +```text android-reverse-engineering-skill/ ├── .claude-plugin/ │ └── marketplace.json # Marketplace catalog @@ -118,10 +120,14 @@ android-reverse-engineering-skill/ │ │ │ ├── api-extraction-patterns.md │ │ │ └── call-flow-analysis.md │ │ └── scripts/ -│ │ ├── check-deps.sh +│ │ ├── check-deps.sh # Bash +│ │ ├── check-deps.ps1 # PowerShell │ │ ├── install-dep.sh +│ │ ├── install-dep.ps1 │ │ ├── decompile.sh -│ │ └── find-api-calls.sh +│ │ ├── decompile.ps1 +│ │ ├── find-api-calls.sh +│ │ └── find-api-calls.ps1 │ └── commands/ │ └── decompile.md # /decompile slash command ├── LICENSE diff --git a/plugins/android-reverse-engineering/commands/decompile.md b/plugins/android-reverse-engineering/commands/decompile.md index 2489b82..34c888e 100644 --- a/plugins/android-reverse-engineering/commands/decompile.md +++ b/plugins/android-reverse-engineering/commands/decompile.md @@ -46,16 +46,19 @@ After any installations, re-run `check-deps.sh` to verify. Do not proceed until Run the decompile script on the target file. Choose the engine based on the input: - **APK or XAPK** → use jadx first (handles resources natively; XAPK is auto-extracted): + ```bash bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/decompile.sh ``` - **JAR/AAR** and Fernflower is available → prefer fernflower for better Java output: + ```bash bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/decompile.sh --engine fernflower ``` - **If jadx output has warnings** or the user wants the best quality → run both and compare: + ```bash bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/decompile.sh --engine both ``` @@ -79,6 +82,7 @@ After decompilation completes: ### Step 5: Offer next steps Tell the user what they can do next: + - **Trace call flows**: "I can follow the execution flow from any Activity to its API calls" - **Extract APIs**: "I can search for all HTTP endpoints and document them" - **Analyze specific classes**: "Point me to a specific class or feature to analyze" diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/SKILL.md b/plugins/android-reverse-engineering/skills/android-reverse-engineering/SKILL.md index de9c523..6b31074 100644 --- a/plugins/android-reverse-engineering/skills/android-reverse-engineering/SKILL.md +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/SKILL.md @@ -15,6 +15,11 @@ This skill requires **Java JDK 17+** and **jadx** to be installed. **Fernflower/ bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/check-deps.sh ``` +On Windows (PowerShell): +```powershell +& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/check-deps.ps1" +``` + If anything is missing, follow the installation instructions in `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/setup-guide.md`. ## Workflow @@ -29,6 +34,11 @@ Before decompiling, confirm that the required tools are available — and instal bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/check-deps.sh ``` +On Windows (PowerShell): +```powershell +& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/check-deps.ps1" +``` + The output contains machine-readable lines: - `INSTALL_REQUIRED:` — must be installed before proceeding - `INSTALL_OPTIONAL:` — recommended but not blocking @@ -39,11 +49,18 @@ The output contains machine-readable lines: bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.sh ``` +On Windows (PowerShell): +```powershell +& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.ps1" +``` + The install script detects the OS and package manager, then: - Installs without sudo when possible (downloads to `~/.local/share/`, symlinks in `~/.local/bin/`) - Uses sudo and the system package manager when necessary (apt, dnf, pacman) - If sudo is needed but unavailable or the user declines, it prints the exact manual command and exits with code 2 — show these instructions to the user +**Windows notes**: The PowerShell install script uses `winget`, `scoop`, or `choco` (in that order). If none are available, it downloads directly to `%USERPROFILE%\.local\share\` and adds the directory to the user's PATH. After running `install-dep.ps1`, the PATH is persisted but the current terminal session may not see it. The `check-deps.ps1` and `decompile.ps1` scripts automatically refresh PATH from the user environment, so re-running them will find newly installed tools without restarting the terminal. + **For optional dependencies**, ask the user if they want to install them. Vineflower and dex2jar are recommended for best results. After installation, re-run `check-deps.sh` to confirm everything is in place. Do not proceed to Phase 2 until all required dependencies are OK. @@ -58,8 +75,15 @@ Use the decompile wrapper script to process the target file. The script supports bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/decompile.sh [OPTIONS] ``` +On Windows (PowerShell): +```powershell +& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/decompile.ps1" [OPTIONS] +``` + For **XAPK** files (ZIP bundles containing multiple APKs, used by APKPure and similar stores): the script automatically extracts the archive, identifies all APK files inside (base + split APKs), and decompiles each one into a separate subdirectory. The XAPK manifest is copied to the output for reference. +**Split/bundled APK detection**: Some APKs are actually bundle wrappers — the outer APK contains `base.apk` plus `split_config.*.apk` files inside its resources directory. When this happens, jadx will decompile the thin wrapper and produce very few Java files. The decompile scripts automatically detect this (≤10 Java files + inner APKs present) and re-decompile `base.apk` into an `/base/` subdirectory. Config-only splits (ABI, language, density) are skipped. The main decompiled source will be in `/base/sources/`. + Options: - `-o ` — Custom output directory (default: `-decompiled`) - `--deobf` — Enable deobfuscation (recommended for obfuscated apps) @@ -137,6 +161,11 @@ Find all API endpoints and produce structured documentation. bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.sh /sources/ ``` +On Windows (PowerShell): +```powershell +& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.ps1" /sources/ +``` + Targeted searches: ```bash # Only Retrofit @@ -149,6 +178,18 @@ bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-c bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.sh /sources/ --auth ``` +On Windows (PowerShell): +```powershell +# Only Retrofit +& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.ps1" /sources/ -Retrofit + +# Only hardcoded URLs +& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.ps1" /sources/ -Urls + +# Only auth patterns +& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.ps1" /sources/ -Auth +``` + Then, for each discovered endpoint, read the surrounding source code to extract: - HTTP method and path - Base URL diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.ps1 b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.ps1 new file mode 100644 index 0000000..3ced181 --- /dev/null +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.ps1 @@ -0,0 +1,165 @@ +# check-deps.ps1 — Verify dependencies and report what's missing +# Output includes machine-readable INSTALL_REQUIRED/INSTALL_OPTIONAL lines. +$ErrorActionPreference = 'Stop' + +# Refresh PATH from user environment so we pick up tools installed in the same session +$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User') +if ($userPath) { + foreach ($dir in $userPath -split ';') { + if ($dir -and $env:PATH -notlike "*$dir*") { + $env:PATH = "$dir;$env:PATH" + } + } +} + +$REQUIRED_JAVA_MAJOR = 17 +$errors = 0 +$missingRequired = @() +$missingOptional = @() + +Write-Host "=== Android Reverse Engineering: Dependency Check ===" +Write-Host "" + +# --- Java --- +$javaOk = $false +$javaBin = Get-Command java -ErrorAction SilentlyContinue +if ($javaBin) { + $javaVersionOutput = & java -version 2>&1 | Select-Object -First 1 + $javaVersionStr = "$javaVersionOutput" + if ($javaVersionStr -match '"(\d+)') { + $javaVersion = [int]$Matches[1] + if ($javaVersion -eq 1 -and $javaVersionStr -match '"1\.(\d+)') { + $javaVersion = [int]$Matches[1] + } + if ($javaVersion -ge $REQUIRED_JAVA_MAJOR) { + Write-Host "[OK] Java $javaVersion detected" + $javaOk = $true + } else { + Write-Host "[WARN] Java detected but version $javaVersion is below $REQUIRED_JAVA_MAJOR" + $errors++ + $missingRequired += "java" + } + } else { + Write-Host "[WARN] Java detected but could not parse version from: $javaVersionStr" + $errors++ + $missingRequired += "java" + } +} else { + Write-Host "[MISSING] Java is not installed or not in PATH" + $errors++ + $missingRequired += "java" +} + +# --- jadx --- +$jadxBin = Get-Command jadx -ErrorAction SilentlyContinue +if (-not $jadxBin) { + # Check common Windows install locations + $jadxCandidates = @( + "$env:USERPROFILE\.local\share\jadx\bin\jadx.bat", + "$env:USERPROFILE\jadx\bin\jadx.bat", + "$env:LOCALAPPDATA\jadx\bin\jadx.bat" + ) + foreach ($c in $jadxCandidates) { + if (Test-Path $c) { + $jadxBin = $c + break + } + } +} +if ($jadxBin) { + try { + $jadxCmd = if ($jadxBin -is [string]) { $jadxBin } else { 'jadx' } + $jadxVersion = & $jadxCmd --version 2>$null + Write-Host "[OK] jadx $jadxVersion detected" + } catch { + Write-Host "[OK] jadx detected" + } +} else { + Write-Host "[MISSING] jadx is not installed or not in PATH" + $errors++ + $missingRequired += "jadx" +} + +# --- Fernflower / Vineflower --- +$ffFound = $false +$vineflowerBin = Get-Command vineflower -ErrorAction SilentlyContinue +$fernflowerBin = Get-Command fernflower -ErrorAction SilentlyContinue +if ($vineflowerBin) { + Write-Host "[OK] vineflower CLI detected" + $ffFound = $true +} elseif ($fernflowerBin) { + Write-Host "[OK] fernflower CLI detected" + $ffFound = $true +} else { + $ffCandidates = @( + $env:FERNFLOWER_JAR_PATH, + "$env:USERPROFILE\.local\share\vineflower\vineflower.jar", + "$env:USERPROFILE\fernflower\build\libs\fernflower.jar", + "$env:USERPROFILE\vineflower\build\libs\vineflower.jar", + "$env:USERPROFILE\fernflower\fernflower.jar", + "$env:USERPROFILE\vineflower\vineflower.jar" + ) + foreach ($candidate in $ffCandidates) { + if ($candidate -and (Test-Path $candidate -ErrorAction SilentlyContinue)) { + Write-Host "[OK] Fernflower/Vineflower JAR found: $candidate" + $ffFound = $true + break + } + } +} +if (-not $ffFound) { + Write-Host "[MISSING] Fernflower/Vineflower not found (optional - better output on complex Java code)" + $missingOptional += "vineflower" +} + +# --- dex2jar --- +$d2jBin = Get-Command d2j-dex2jar -ErrorAction SilentlyContinue +if (-not $d2jBin) { + $d2jBin = Get-Command d2j-dex2jar.bat -ErrorAction SilentlyContinue +} +if ($d2jBin) { + Write-Host "[OK] dex2jar detected" +} else { + Write-Host "[MISSING] dex2jar not found (optional - needed to use Fernflower on APK/DEX files)" + $missingOptional += "dex2jar" +} + +# --- Optional: apktool --- +if (Get-Command apktool -ErrorAction SilentlyContinue) { + Write-Host "[OK] apktool detected (optional)" +} else { + Write-Host "[MISSING] apktool not found (optional - useful for resource decoding)" + $missingOptional += "apktool" +} + +# --- Optional: adb --- +if (Get-Command adb -ErrorAction SilentlyContinue) { + Write-Host "[OK] adb detected (optional)" +} else { + Write-Host "[MISSING] adb not found (optional - useful for pulling APKs from devices)" + $missingOptional += "adb" +} + +# --- Machine-readable summary --- +Write-Host "" +foreach ($dep in $missingRequired) { + Write-Host "INSTALL_REQUIRED:$dep" +} +foreach ($dep in $missingOptional) { + Write-Host "INSTALL_OPTIONAL:$dep" +} + +Write-Host "" +if ($errors -gt 0) { + Write-Host "*** $($missingRequired.Count) required dependency/ies missing. ***" + Write-Host "Run install-dep.ps1 to install, or see references/setup-guide.md." + exit 1 +} else { + if ($missingOptional.Count -gt 0) { + Write-Host "Required dependencies OK. $($missingOptional.Count) optional dependency/ies missing." + Write-Host "Run install-dep.ps1 to install optional tools." + } else { + Write-Host "All dependencies are installed. Ready to decompile." + } + exit 0 +} diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.ps1 b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.ps1 new file mode 100644 index 0000000..684c419 --- /dev/null +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.ps1 @@ -0,0 +1,400 @@ +# decompile.ps1 — Decompile APK/XAPK/JAR/AAR using jadx, fernflower, or both +param( + [Alias('o')] + [string]$Output, + [switch]$Deobf, + [switch]$NoRes, + [string]$Engine = 'jadx', + [Parameter(Position=0)] + [string]$InputFile, + [Alias('h')] + [switch]$Help +) + +$ErrorActionPreference = 'Stop' + +# Refresh PATH from user environment so we pick up tools installed in the same session +$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User') +if ($userPath) { + foreach ($dir in $userPath -split ';') { + if ($dir -and $env:PATH -notlike "*$dir*") { + $env:PATH = "$dir;$env:PATH" + } + } +} + +function Show-Usage { + Write-Host @" +Usage: decompile.ps1 [OPTIONS] + +Decompile an Android APK, XAPK, JAR, or AAR file. + +Arguments: + Path to the .apk, .xapk, .jar, or .aar file + +Options: + -Output DIR Output directory (default: -decompiled) + -Deobf Enable deobfuscation of names + -NoRes Skip resource decoding (faster, code-only) + -Engine ENGINE Decompiler engine: jadx, fernflower, or both (default: jadx) + -Help Show this help message + +Engines: + jadx Use jadx (default). Handles APK/JAR/AAR natively, decodes resources. + fernflower Use Fernflower/Vineflower. Better on complex Java, lambdas, generics. + For APK files, requires dex2jar as intermediate step. + both Run both decompilers side by side for comparison. + jadx output -> /jadx/ + fernflower -> /fernflower/ + +Environment: + FERNFLOWER_JAR_PATH Path to fernflower.jar or vineflower.jar + +Examples: + .\decompile.ps1 app-release.apk + .\decompile.ps1 app-bundle.xapk + .\decompile.ps1 -Engine both -Deobf app-release.apk + .\decompile.ps1 -Engine fernflower library.jar +"@ + exit 0 +} + +if ($Help) { Show-Usage } + +# --- Validate input --- +if (-not $InputFile) { + Write-Host "Error: No input file specified." -ForegroundColor Red + Show-Usage +} + +if (-not (Test-Path $InputFile)) { + Write-Host "Error: File not found: $InputFile" -ForegroundColor Red + exit 1 +} + +$extLower = [IO.Path]::GetExtension($InputFile).TrimStart('.').ToLower() +if ($extLower -notin @('apk', 'xapk', 'jar', 'aar')) { + Write-Host "Error: Unsupported file type '.$extLower'. Expected .apk, .xapk, .jar, or .aar" -ForegroundColor Red + exit 1 +} + +if ($Engine -notin @('jadx', 'fernflower', 'both')) { + Write-Host "Error: Unknown engine '$Engine'. Use jadx, fernflower, or both." -ForegroundColor Red + exit 1 +} + +$baseName = [IO.Path]::GetFileNameWithoutExtension($InputFile) +$inputFileAbs = (Resolve-Path $InputFile).Path + +if (-not $Output) { + $Output = "$baseName-decompiled" +} + +# --- XAPK handling --- +$xapkExtractedDir = $null +$xapkApkFiles = @() + +if ($extLower -eq 'xapk') { + $xapkExtractedDir = Join-Path $env:TEMP "xapk-extract-$(Get-Random)" + Write-Host "=== Extracting XAPK archive ===" + New-Item -ItemType Directory -Path $xapkExtractedDir -Force | Out-Null + Expand-Archive -Path $inputFileAbs -DestinationPath $xapkExtractedDir -Force + + # Show manifest.json if present + $manifestPath = Join-Path $xapkExtractedDir 'manifest.json' + if (Test-Path $manifestPath) { + Write-Host "XAPK manifest found:" + Get-Content $manifestPath + Write-Host "" + } + + # Find all APK files inside + $xapkApkFiles = Get-ChildItem -Path $xapkExtractedDir -Recurse -Filter '*.apk' | Sort-Object Name + + if ($xapkApkFiles.Count -eq 0) { + Write-Host "Error: No APK files found inside XAPK archive." -ForegroundColor Red + Remove-Item $xapkExtractedDir -Recurse -Force + exit 1 + } + + Write-Host "Found $($xapkApkFiles.Count) APK(s) inside XAPK:" + foreach ($f in $xapkApkFiles) { + Write-Host " - $($f.Name)" + } + Write-Host "" +} + +# --- Locate fernflower JAR --- +function Find-FernflowerJar { + if ($env:FERNFLOWER_JAR_PATH -and (Test-Path $env:FERNFLOWER_JAR_PATH -ErrorAction SilentlyContinue)) { + return $env:FERNFLOWER_JAR_PATH + } + $candidates = @( + "$env:USERPROFILE\.local\share\vineflower\vineflower.jar", + "$env:USERPROFILE\fernflower\build\libs\fernflower.jar", + "$env:USERPROFILE\vineflower\build\libs\vineflower.jar", + "$env:USERPROFILE\fernflower\fernflower.jar", + "$env:USERPROFILE\vineflower\vineflower.jar" + ) + foreach ($c in $candidates) { + if (Test-Path $c -ErrorAction SilentlyContinue) { return $c } + } + return $null +} + +# --- Locate dex2jar --- +function Find-Dex2Jar { + $cmd = Get-Command d2j-dex2jar -ErrorAction SilentlyContinue + if ($cmd) { return $cmd.Source } + $cmd = Get-Command d2j-dex2jar.bat -ErrorAction SilentlyContinue + if ($cmd) { return $cmd.Source } + return $null +} + +# --- jadx decompilation --- +function Invoke-Jadx { + param([string]$OutDir, [string]$FileAbs, [string]$FileExt) + + if (-not (Get-Command jadx -ErrorAction SilentlyContinue)) { + Write-Host "Error: jadx is not installed or not in PATH." -ForegroundColor Red + return $false + } + + $jadxArgs = @('-d', $OutDir) + if ($Deobf) { $jadxArgs += '--deobf' } + if ($NoRes) { $jadxArgs += '--no-res' } + $jadxArgs += '--show-bad-code' + $jadxArgs += $FileAbs + + Write-Host "Running: jadx $($jadxArgs -join ' ')" + & jadx @jadxArgs + + $sourcesDir = Join-Path $OutDir 'sources' + if (Test-Path $sourcesDir) { + $count = (Get-ChildItem -Path $sourcesDir -Recurse -Filter '*.java').Count + Write-Host "jadx output: $sourcesDir\" + Write-Host "Java files decompiled by jadx: $count" + } + return $true +} + +# --- Fernflower decompilation --- +function Invoke-Fernflower { + param([string]$OutDir, [string]$FileAbs, [string]$FileExt) + + $ffJar = Find-FernflowerJar + if (-not $ffJar) { + Write-Host "Error: Fernflower/Vineflower JAR not found." -ForegroundColor Red + Write-Host "Set FERNFLOWER_JAR_PATH or see references/setup-guide.md" + return $false + } + + New-Item -ItemType Directory -Path $OutDir -Force | Out-Null + + $jarToDecompile = $FileAbs + $convertedJar = $null + + # For APK/AAR, we need dex2jar first + if ($FileExt -in @('apk', 'aar')) { + $d2j = Find-Dex2Jar + if (-not $d2j) { + Write-Host "Error: dex2jar is required to use Fernflower on .$FileExt files." -ForegroundColor Red + Write-Host "Install dex2jar - see references/setup-guide.md" + return $false + } + + Write-Host "Converting $FileExt to JAR with dex2jar..." + $convertedJar = Join-Path $OutDir "$baseName-dex2jar.jar" + & $d2j -f -o $convertedJar $FileAbs 2>&1 | Write-Host + if (-not (Test-Path $convertedJar)) { + Write-Host "Error: dex2jar conversion failed." -ForegroundColor Red + return $false + } + $jarToDecompile = $convertedJar + } + + # Build fernflower args + $ffArgs = @('-dgs=1', '-mpm=60') + if ($Deobf) { $ffArgs += '-ren=1' } + $ffArgs += $jarToDecompile + $ffArgs += $OutDir + + Write-Host "Running: java -jar $ffJar $($ffArgs -join ' ')" + & java -jar $ffJar @ffArgs + + # Fernflower outputs a JAR containing .java files — extract it + $resultJar = Join-Path $OutDir ([IO.Path]::GetFileName($jarToDecompile)) + if (Test-Path $resultJar) { + $sourcesDir = Join-Path $OutDir 'sources' + New-Item -ItemType Directory -Path $sourcesDir -Force | Out-Null + Expand-Archive -Path $resultJar -DestinationPath $sourcesDir -Force + Remove-Item $resultJar -Force + $count = (Get-ChildItem -Path $sourcesDir -Recurse -Filter '*.java').Count + Write-Host "Fernflower output: $sourcesDir\" + Write-Host "Java files decompiled by Fernflower: $count" + } + + # Clean up intermediate dex2jar output + if ($convertedJar -and (Test-Path $convertedJar -ErrorAction SilentlyContinue)) { + Remove-Item $convertedJar -Force + } + return $true +} + +# --- Summary helper --- +function Show-Structure { + param([string]$SrcDir, [string]$Label) + if (Test-Path $SrcDir) { + Write-Host "" + Write-Host "Top-level packages ($Label):" + Get-ChildItem -Path $SrcDir -Directory -Recurse -Depth 2 | + Select-Object -First 20 | + ForEach-Object { $_.FullName.Replace("$SrcDir\", '') } | + Sort-Object + } +} + +# --- Decompile a single file --- +function Invoke-DecompileSingle { + param([string]$FileAbs, [string]$OutDir, [string]$Label) + + $fileExt = [IO.Path]::GetExtension($FileAbs).TrimStart('.').ToLower() + + if ($Label) { + Write-Host "=== Decompiling $Label (engine: $Engine) ===" + } + + switch ($Engine) { + 'jadx' { + Invoke-Jadx -OutDir $OutDir -FileAbs $FileAbs -FileExt $fileExt + Show-Structure (Join-Path $OutDir 'sources') 'jadx' + } + 'fernflower' { + Invoke-Fernflower -OutDir $OutDir -FileAbs $FileAbs -FileExt $fileExt + Show-Structure (Join-Path $OutDir 'sources') 'fernflower' + } + 'both' { + Write-Host "--- Pass 1: jadx ---" + Invoke-Jadx -OutDir (Join-Path $OutDir 'jadx') -FileAbs $FileAbs -FileExt $fileExt + Write-Host "" + Write-Host "--- Pass 2: Fernflower ---" + Invoke-Fernflower -OutDir (Join-Path $OutDir 'fernflower') -FileAbs $FileAbs -FileExt $fileExt + + Show-Structure (Join-Path $OutDir 'jadx\sources') 'jadx' + Show-Structure (Join-Path $OutDir 'fernflower\sources') 'fernflower' + + Write-Host "" + Write-Host "=== Comparison ===" + $jadxCount = 0; $ffCount = 0 + $jadxSources = Join-Path $OutDir 'jadx\sources' + $ffSources = Join-Path $OutDir 'fernflower\sources' + if (Test-Path $jadxSources) { + $jadxCount = (Get-ChildItem -Path $jadxSources -Recurse -Filter '*.java').Count + } + if (Test-Path $ffSources) { + $ffCount = (Get-ChildItem -Path $ffSources -Recurse -Filter '*.java').Count + } + Write-Host "jadx: $jadxCount Java files" + Write-Host "Fernflower: $ffCount Java files" + + if (Test-Path $jadxSources) { + $jadxErrors = (Get-ChildItem -Path $jadxSources -Recurse -Filter '*.java' -File | + Select-String -Pattern 'JADX WARNING|JADX WARN|JADX ERROR|Code decompiled incorrectly' -SimpleMatch -ErrorAction SilentlyContinue | + Select-Object -ExpandProperty Path -Unique).Count + Write-Host "jadx files with warnings/errors: $jadxErrors" + } + Write-Host "" + Write-Host "Tip: compare specific classes between jadx/ and fernflower/ to pick the better output." + } + } +} + +# --- Run --- +Write-Host "=== Decompiling $InputFile (engine: $Engine) ===" +Write-Host "Output directory: $Output" +Write-Host "" + +if ($extLower -eq 'xapk') { + New-Item -ItemType Directory -Path $Output -Force | Out-Null + + # Copy XAPK manifest for reference + $manifestSrc = Join-Path $xapkExtractedDir 'manifest.json' + if (Test-Path $manifestSrc) { + Copy-Item $manifestSrc (Join-Path $Output 'xapk-manifest.json') + } + + # List OBB files + $obbFiles = Get-ChildItem -Path $xapkExtractedDir -Recurse -Filter '*.obb' -ErrorAction SilentlyContinue + if ($obbFiles) { + Write-Host "OBB files found (not decompiled, data-only):" + foreach ($obb in $obbFiles) { + $size = '{0:N1} MB' -f ($obb.Length / 1MB) + Write-Host " - $($obb.Name) ($size)" + } + Write-Host "" + } + + foreach ($apkFile in $xapkApkFiles) { + $apkName = [IO.Path]::GetFileNameWithoutExtension($apkFile.Name) + Write-Host "" + Write-Host "======================================================" + Invoke-DecompileSingle -FileAbs $apkFile.FullName -OutDir (Join-Path $Output $apkName) -Label "$($apkFile.Name)" + } + + # Cleanup extracted XAPK + Remove-Item $xapkExtractedDir -Recurse -Force + + Write-Host "" + Write-Host "=== XAPK decompilation complete ===" + Write-Host "Subdirectories in ${Output}\" + Get-ChildItem -Path $Output -Directory | ForEach-Object { Write-Host $_.Name } +} else { + Invoke-DecompileSingle -FileAbs $inputFileAbs -OutDir $Output -Label '' + + # --- Split/bundled APK detection --- + # Some APKs are bundles: the outer APK contains + # base.apk + split_config.*.apk inside the resources directory. jadx will + # decompile the thin outer wrapper and produce very few Java files. + # Detect this and automatically decompile the inner base.apk. + $sourcesDir = Join-Path $Output 'sources' + $resourcesDir = Join-Path $Output 'resources' + if ((Test-Path $sourcesDir) -and (Test-Path $resourcesDir)) { + $javaCount = (Get-ChildItem -Path $sourcesDir -Recurse -Filter '*.java' -File -ErrorAction SilentlyContinue).Count + $innerApks = Get-ChildItem -Path $resourcesDir -Filter '*.apk' -File -ErrorAction SilentlyContinue + $baseApk = $innerApks | Where-Object { $_.Name -eq 'base.apk' } + + if ($javaCount -le 10 -and $baseApk) { + Write-Host "" + Write-Host "=== Split/bundled APK detected ===" + Write-Host "Outer APK produced only $javaCount Java file(s) but contains $($innerApks.Count) inner APK(s):" + foreach ($inner in $innerApks) { + Write-Host " - $($inner.Name)" + } + Write-Host "" + Write-Host "Decompiling base.apk (contains the actual app code)..." + $baseOutput = Join-Path $Output 'base' + Invoke-DecompileSingle -FileAbs $baseApk.FullName -OutDir $baseOutput -Label 'base.apk' + + # Decompile any split APKs that aren't just config splits + $splitApks = $innerApks | Where-Object { $_.Name -ne 'base.apk' -and $_.Name -notmatch 'split_config\.' } + foreach ($split in $splitApks) { + $splitName = [IO.Path]::GetFileNameWithoutExtension($split.Name) + Write-Host "" + Write-Host "Decompiling $($split.Name)..." + Invoke-DecompileSingle -FileAbs $split.FullName -OutDir (Join-Path $Output $splitName) -Label $split.Name + } + + if ($innerApks | Where-Object { $_.Name -match 'split_config\.' }) { + Write-Host "" + Write-Host "Skipped config splits (resource/ABI only):" + $innerApks | Where-Object { $_.Name -match 'split_config\.' } | ForEach-Object { Write-Host " - $($_.Name)" } + } + + Write-Host "" + Write-Host "NOTE: The main decompiled source is in: $(Join-Path $Output 'base\sources')" + } + } + + Write-Host "" + Write-Host "=== Decompilation complete ===" +} diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh index 5277abb..f13e252 100755 --- a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh @@ -528,6 +528,51 @@ if [[ "$ext_lower" == "xapk" ]]; then ls -1 "$OUTPUT_DIR/" else decompile_single "$INPUT_FILE_ABS" "$OUTPUT_DIR" "" + + # --- Split/bundled APK detection --- + # Some APKs are bundles: the outer APK contains base.apk + split_config.*.apk + # inside the resources directory. jadx will decompile the thin outer wrapper + # and produce very few Java files. Detect this and re-decompile base.apk. + sources_dir="$OUTPUT_DIR/sources" + resources_dir="$OUTPUT_DIR/resources" + if [[ -d "$sources_dir" && -d "$resources_dir" ]]; then + java_count=$(find "$sources_dir" -name "*.java" -type f 2>/dev/null | wc -l) + base_apk=$(find "$resources_dir" -maxdepth 1 -name "base.apk" -type f 2>/dev/null | head -1) + inner_apk_count=$(find "$resources_dir" -maxdepth 1 -name "*.apk" -type f 2>/dev/null | wc -l) + + if [[ "$java_count" -le 10 && -n "$base_apk" ]]; then + echo + echo "=== Split/bundled APK detected ===" + echo "Outer APK produced only $java_count Java file(s) but contains $inner_apk_count inner APK(s):" + find "$resources_dir" -maxdepth 1 -name "*.apk" -type f -exec basename {} \; | while read -r f; do echo " - $f"; done + echo + echo "Decompiling base.apk (contains the actual app code)..." + decompile_single "$base_apk" "$OUTPUT_DIR/base" "base.apk" + + # Decompile non-config split APKs + while IFS= read -r -d '' split_apk; do + split_name=$(basename "$split_apk" .apk) + case "$split_name" in + base|split_config.*) continue ;; + esac + echo + echo "Decompiling $split_name.apk..." + decompile_single "$split_apk" "$OUTPUT_DIR/$split_name" "$split_name.apk" + done < <(find "$resources_dir" -maxdepth 1 -name "*.apk" -type f -print0 2>/dev/null) + + # Report skipped config splits + config_splits=$(find "$resources_dir" -maxdepth 1 -name "split_config.*.apk" -type f 2>/dev/null) + if [[ -n "$config_splits" ]]; then + echo + echo "Skipped config splits (resource/ABI only):" + echo "$config_splits" | while read -r f; do echo " - $(basename "$f")"; done + fi + + echo + echo "Main decompiled source is in: $OUTPUT_DIR/base/sources/" + fi + fi + echo echo "=== Decompilation complete ===" fi diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.ps1 b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.ps1 new file mode 100644 index 0000000..9084795 --- /dev/null +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.ps1 @@ -0,0 +1,121 @@ +# find-api-calls.ps1 — Search decompiled source for API calls and HTTP endpoints +param( + [Parameter(Position=0)] + [string]$SourceDir, + [switch]$Retrofit, + [switch]$OkHttp, + [switch]$Volley, + [switch]$Urls, + [switch]$Auth, + [switch]$All, + [Alias('h')] + [switch]$Help +) + +$ErrorActionPreference = 'Stop' + +function Show-Usage { + Write-Host @" +Usage: find-api-calls.ps1 [OPTIONS] + +Search decompiled Java/Kotlin source for HTTP API calls and endpoints. + +Arguments: + Path to the decompiled sources directory + +Options: + -Retrofit Search only for Retrofit annotations + -OkHttp Search only for OkHttp patterns + -Volley Search only for Volley patterns + -Urls Search only for hardcoded URLs + -Auth Search only for auth-related patterns + -All Search all patterns (default) + -Help Show this help message + +Output: + Results are printed as file:line:match for easy navigation. +"@ + exit 0 +} + +if ($Help) { Show-Usage } + +if (-not $SourceDir) { + Write-Host "Error: No source directory specified." -ForegroundColor Red + Show-Usage +} + +if (-not (Test-Path $SourceDir)) { + Write-Host "Error: Directory not found: $SourceDir" -ForegroundColor Red + exit 1 +} + +# Default to all if no specific flag set +$searchAll = (-not $Retrofit -and -not $OkHttp -and -not $Volley -and -not $Urls -and -not $Auth) -or $All + +function Write-Section { + param([string]$Title) + Write-Host "" + Write-Host "==== $Title ====" + Write-Host "" +} + +function Search-Sources { + param([string]$Pattern) + Get-ChildItem -Path $SourceDir -Recurse -Include '*.java','*.kt' -File | + Select-String -Pattern $Pattern -ErrorAction SilentlyContinue | + ForEach-Object { + "$($_.Path):$($_.LineNumber):$($_.Line.Trim())" + } +} + +# --- Retrofit --- +if ($searchAll -or $Retrofit) { + Write-Section "Retrofit Annotations" + Search-Sources '@(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|HTTP)\s*\(' + + Write-Section "Retrofit Headers & Parameters" + Search-Sources '@(Headers|Header|Query|QueryMap|Path|Body|Field|FieldMap|Part|PartMap|Url)\s*\(' + + Write-Section "Retrofit Base URL" + Search-Sources '(baseUrl|base_url)\s*\(' +} + +# --- OkHttp --- +if ($searchAll -or $OkHttp) { + Write-Section "OkHttp Request Building" + Search-Sources '(Request\.Builder|HttpUrl|\.newCall|\.enqueue|addInterceptor|addNetworkInterceptor)' + + Write-Section "OkHttp URL Construction" + Search-Sources '(\.url\s*\(|\.addQueryParameter|\.addPathSegment|\.scheme\s*\(|\.host\s*\()' +} + +# --- Volley --- +if ($searchAll -or $Volley) { + Write-Section "Volley Requests" + Search-Sources '(StringRequest|JsonObjectRequest|JsonArrayRequest|ImageRequest|RequestQueue|Volley\.newRequestQueue)' +} + +# --- Hardcoded URLs --- +if ($searchAll -or $Urls) { + Write-Section "Hardcoded URLs (http:// and https://)" + Search-Sources '"https?://[^"]+' + + Write-Section "HttpURLConnection" + Search-Sources '(openConnection|setRequestMethod|HttpURLConnection|HttpsURLConnection)' + + Write-Section "WebView URLs" + Search-Sources '(loadUrl|loadData|evaluateJavascript|addJavascriptInterface|WebViewClient|WebChromeClient)' +} + +# --- Auth patterns --- +if ($searchAll -or $Auth) { + Write-Section "Authentication & API Keys" + Search-Sources '(?i)(api[_\-]?key|auth[_\-]?token|bearer|authorization|x-api-key|client[_\-]?secret|access[_\-]?token)' + + Write-Section "Base URLs and Constants" + Search-Sources '(?i)(BASE_URL|API_URL|SERVER_URL|ENDPOINT|API_BASE|HOST_NAME)' +} + +Write-Host "" +Write-Host "=== Search complete ===" diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.ps1 b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.ps1 new file mode 100644 index 0000000..eb73dd9 --- /dev/null +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.ps1 @@ -0,0 +1,344 @@ +# install-dep.ps1 — Install a single dependency for Android reverse engineering +# Usage: install-dep.ps1 +# Dependencies: java, jadx, vineflower, dex2jar, apktool, adb +# +# Exit codes: +# 0 — installed successfully +# 1 — installation failed +# 2 — requires manual action +param( + [Parameter(Position=0)] + [string]$Dep +) + +$ErrorActionPreference = 'Stop' + +function Show-Usage { + Write-Host @" +Usage: install-dep.ps1 + +Install a dependency required for Android reverse engineering. + +Available dependencies: + java Java JDK 17+ + jadx jadx decompiler + vineflower Vineflower (Fernflower fork) decompiler + dex2jar DEX to JAR converter + apktool Android resource decoder + adb Android Debug Bridge + +The script detects available package managers (winget, scoop, choco), then: + - Installs using the first available manager + - Falls back to direct download to %USERPROFILE%\.local\share\ + - Prints manual instructions if no option works +"@ + exit 0 +} + +if (-not $Dep -or $Dep -eq '-h' -or $Dep -eq '--help') { Show-Usage } + +# --- Detect environment --- +$hasWinget = [bool](Get-Command winget -ErrorAction SilentlyContinue) +$hasScoop = [bool](Get-Command scoop -ErrorAction SilentlyContinue) +$hasChoco = [bool](Get-Command choco -ErrorAction SilentlyContinue) + +function Write-Info { param($msg) Write-Host "[INFO] $msg" } +function Write-Ok { param($msg) Write-Host "[OK] $msg" } +function Write-Fail { param($msg) Write-Host "[FAIL] $msg" -ForegroundColor Red } +function Write-Manual { + param($msg) + Write-Host "[MANUAL] $msg" -ForegroundColor Yellow + Write-Host " Cannot install automatically. Please install manually and retry." -ForegroundColor Yellow + exit 2 +} + +# --- Helper: download a file --- +function Invoke-Download { + param([string]$Url, [string]$Dest) + Write-Info "Downloading $Url..." + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest -Uri $Url -OutFile $Dest -UseBasicParsing +} + +# --- Helper: get latest GitHub release tag --- +function Get-GHLatestTag { + param([string]$Repo) + $url = "https://api.github.com/repos/$Repo/releases/latest" + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + $response = Invoke-RestMethod -Uri $url -UseBasicParsing + return $response.tag_name +} + +# --- Helper: ensure directory on PATH --- +function Add-ToUserPath { + param([string]$Dir) + $currentPath = [Environment]::GetEnvironmentVariable('PATH', 'User') + if ($currentPath -notlike "*$Dir*") { + [Environment]::SetEnvironmentVariable('PATH', "$Dir;$currentPath", 'User') + Write-Info "Added $Dir to user PATH. Restart your terminal to apply." + } + if ($env:PATH -notlike "*$Dir*") { + $env:PATH = "$Dir;$env:PATH" + } +} + +$localBin = Join-Path $env:USERPROFILE '.local\bin' +$localShare = Join-Path $env:USERPROFILE '.local\share' + +# ===================================================================== +# Dependency installers +# ===================================================================== + +function Install-Java { + $javaBin = Get-Command java -ErrorAction SilentlyContinue + if ($javaBin) { + $verOutput = & java -version 2>&1 | Select-Object -First 1 + if ("$verOutput" -match '"(\d+)') { + $ver = [int]$Matches[1] + if ($ver -ge 17) { + Write-Ok "Java $ver already installed" + return + } + } + } + + Write-Info "Installing Java JDK 17+..." + if ($hasWinget) { + Write-Info "Installing via winget..." + winget install --id Microsoft.OpenJDK.17 --accept-source-agreements --accept-package-agreements + } elseif ($hasScoop) { + Write-Info "Installing via scoop..." + scoop install openjdk17 + } elseif ($hasChoco) { + Write-Info "Installing via choco..." + choco install openjdk17 -y + } else { + Write-Manual "Install Java JDK 17+ from https://adoptium.net/" + } + + # Verify + $javaBin = Get-Command java -ErrorAction SilentlyContinue + if ($javaBin) { + Write-Ok "Java installed: $(& java -version 2>&1 | Select-Object -First 1)" + } else { + Write-Fail "Java installation may require a terminal restart for PATH update." + exit 1 + } +} + +function Install-Jadx { + if (Get-Command jadx -ErrorAction SilentlyContinue) { + Write-Ok "jadx already installed" + return + } + + # Try scoop first (cleanest on Windows) + if ($hasScoop) { + Write-Info "Installing jadx via scoop..." + scoop install jadx + if (Get-Command jadx -ErrorAction SilentlyContinue) { + Write-Ok "jadx installed via scoop" + return + } + } + + # Direct download from GitHub releases + Write-Info "Installing jadx from GitHub releases..." + $tag = Get-GHLatestTag "skylot/jadx" + if (-not $tag) { + Write-Fail "Could not determine latest jadx version." + Write-Manual "Download from https://github.com/skylot/jadx/releases/latest" + } + + $version = $tag -replace '^v', '' + $url = "https://github.com/skylot/jadx/releases/download/$tag/jadx-$version.zip" + $tmpZip = Join-Path $env:TEMP "jadx-$version.zip" + + Invoke-Download -Url $url -Dest $tmpZip + + $installDir = Join-Path $localShare 'jadx' + if (Test-Path $installDir) { Remove-Item $installDir -Recurse -Force } + New-Item -ItemType Directory -Path $installDir -Force | Out-Null + Expand-Archive -Path $tmpZip -DestinationPath $installDir -Force + Remove-Item $tmpZip -Force + + # Add jadx\bin to PATH + $jadxBin = Join-Path $installDir 'bin' + Add-ToUserPath $jadxBin + + if (Get-Command jadx -ErrorAction SilentlyContinue) { + Write-Ok "jadx $version installed to $installDir" + } else { + Write-Ok "jadx $version installed to $installDir" + Write-Info "Restart your terminal or run: `$env:PATH = '$jadxBin;' + `$env:PATH" + } +} + +function Install-Vineflower { + if (Get-Command vineflower -ErrorAction SilentlyContinue) { + Write-Ok "Vineflower CLI already installed" + return + } + if (Get-Command fernflower -ErrorAction SilentlyContinue) { + Write-Ok "Fernflower CLI already installed" + return + } + $ffCandidates = @( + $env:FERNFLOWER_JAR_PATH, + "$env:USERPROFILE\.local\share\vineflower\vineflower.jar", + "$env:USERPROFILE\vineflower\vineflower.jar", + "$env:USERPROFILE\fernflower\fernflower.jar" + ) + foreach ($c in $ffCandidates) { + if ($c -and (Test-Path $c -ErrorAction SilentlyContinue)) { + Write-Ok "Vineflower/Fernflower JAR already exists: $c" + return + } + } + + # Download JAR from GitHub releases + Write-Info "Installing Vineflower from GitHub releases..." + $tag = Get-GHLatestTag "Vineflower/vineflower" + if (-not $tag) { + Write-Fail "Could not determine latest Vineflower version." + Write-Manual "Download from https://github.com/Vineflower/vineflower/releases/latest" + } + + $version = $tag -replace '^v', '' + $url = "https://github.com/Vineflower/vineflower/releases/download/$tag/vineflower-$version.jar" + $installDir = Join-Path $localShare 'vineflower' + New-Item -ItemType Directory -Path $installDir -Force | Out-Null + + Invoke-Download -Url $url -Dest (Join-Path $installDir 'vineflower.jar') + + # Create wrapper batch file + New-Item -ItemType Directory -Path $localBin -Force | Out-Null + $wrapperPath = Join-Path $localBin 'vineflower.cmd' + Set-Content -Path $wrapperPath -Value "@echo off`r`njava -jar `"$installDir\vineflower.jar`" %*" + + Add-ToUserPath $localBin + [Environment]::SetEnvironmentVariable('FERNFLOWER_JAR_PATH', "$installDir\vineflower.jar", 'User') + $env:FERNFLOWER_JAR_PATH = "$installDir\vineflower.jar" + + Write-Ok "Vineflower $version installed to $installDir\vineflower.jar" + Write-Info "FERNFLOWER_JAR_PATH set to $installDir\vineflower.jar" +} + +function Install-Dex2Jar { + if ((Get-Command d2j-dex2jar -ErrorAction SilentlyContinue) -or + (Get-Command d2j-dex2jar.bat -ErrorAction SilentlyContinue)) { + Write-Ok "dex2jar already installed" + return + } + + Write-Info "Installing dex2jar from GitHub releases..." + $tag = try { Get-GHLatestTag "pxb1988/dex2jar" } catch { "v2.4" } + if (-not $tag) { $tag = "v2.4" } + + $version = $tag -replace '^v', '' + $url = "https://github.com/pxb1988/dex2jar/releases/download/$tag/dex-tools-v$version.zip" + $tmpZip = Join-Path $env:TEMP "dex2jar-$version.zip" + + try { + Invoke-Download -Url $url -Dest $tmpZip + } catch { + $url = "https://github.com/pxb1988/dex2jar/releases/download/$tag/dex-tools-$version.zip" + try { + Invoke-Download -Url $url -Dest $tmpZip + } catch { + Write-Fail "Download failed." + Write-Manual "Download from https://github.com/pxb1988/dex2jar/releases/latest" + } + } + + $installDir = Join-Path $localShare 'dex2jar' + if (Test-Path $installDir) { Remove-Item $installDir -Recurse -Force } + New-Item -ItemType Directory -Path $installDir -Force | Out-Null + Expand-Archive -Path $tmpZip -DestinationPath $installDir -Force + Remove-Item $tmpZip -Force + + # Find the actual bin directory (may be nested) + $d2jBat = Get-ChildItem -Path $installDir -Recurse -Filter 'd2j-dex2jar.bat' | Select-Object -First 1 + if (-not $d2jBat) { + $d2jBat = Get-ChildItem -Path $installDir -Recurse -Filter 'd2j-dex2jar.sh' | Select-Object -First 1 + } + if (-not $d2jBat) { + Write-Fail "Could not find d2j-dex2jar in extracted archive." + Write-Manual "Download and extract manually from https://github.com/pxb1988/dex2jar/releases" + } + + $binDir = $d2jBat.DirectoryName + Add-ToUserPath $binDir + + Write-Ok "dex2jar $version installed to $installDir" +} + +function Install-Apktool { + if (Get-Command apktool -ErrorAction SilentlyContinue) { + Write-Ok "apktool already installed" + return + } + + if ($hasScoop) { + Write-Info "Installing apktool via scoop..." + scoop install apktool + } elseif ($hasChoco) { + Write-Info "Installing apktool via choco..." + choco install apktool -y + } else { + Write-Manual "Install apktool from https://apktool.org/docs/install" + } + + if (Get-Command apktool -ErrorAction SilentlyContinue) { + Write-Ok "apktool installed" + } else { + Write-Fail "apktool installation may have failed." + exit 1 + } +} + +function Install-Adb { + if (Get-Command adb -ErrorAction SilentlyContinue) { + Write-Ok "adb already installed" + return + } + + if ($hasScoop) { + Write-Info "Installing adb via scoop..." + scoop install adb + } elseif ($hasChoco) { + Write-Info "Installing adb via choco..." + choco install adb -y + } elseif ($hasWinget) { + Write-Info "Installing via winget..." + winget install Google.PlatformTools --accept-source-agreements --accept-package-agreements + } else { + Write-Manual "Install Android SDK Platform Tools from https://developer.android.com/tools/releases/platform-tools" + } + + if (Get-Command adb -ErrorAction SilentlyContinue) { + Write-Ok "adb installed" + } else { + Write-Fail "adb installation may have failed." + exit 1 + } +} + +# ===================================================================== +# Dispatch +# ===================================================================== + +switch ($Dep) { + 'java' { Install-Java } + 'jadx' { Install-Jadx } + 'vineflower' { Install-Vineflower } + 'fernflower' { Install-Vineflower } + 'dex2jar' { Install-Dex2Jar } + 'apktool' { Install-Apktool } + 'adb' { Install-Adb } + default { + Write-Host "Error: Unknown dependency '$Dep'" -ForegroundColor Red + Write-Host "Available: java, jadx, vineflower, dex2jar, apktool, adb" + exit 1 + } +}