getapk 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
getapk-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rudranetra India Private Limited (https://rudranetra.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
getapk-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: getapk
3
+ Version: 1.0.0
4
+ Summary: Pull an installed app from an Android device over adb as an APK (or an APKS bundle for split APKs)
5
+ License: MIT
6
+ Project-URL: Homepage, https://rudranetra.com/tools
7
+ Project-URL: Source, https://github.com/RansomWhisker/getAPK
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Dynamic: license-file
12
+
13
+ # getAPK
14
+
15
+ **Pull an installed app off your Android phone as an `.apk` (or an `.apks` bundle for split apps) — in one command.**
16
+
17
+ `getapk` talks to your device through `adb`, finds where an app is installed, copies every APK file it consists of, checks each one is a real APK, and writes a single ready-to-analyse file with its SHA-256. It is the Android counterpart of [getIPA](https://github.com/RansomWhisker/getIPA).
18
+
19
+ > A tool by [**rudranetra.com**](https://rudranetra.com) · more at [rudranetra.com/tools](https://rudranetra.com/tools)
20
+
21
+ ## Why not just `adb pull`?
22
+
23
+ Most Play Store apps are installed as *split APKs*: `base.apk` plus one file per CPU architecture, language and screen density. Pulling only `base.apk` gives you something that will not install and is missing native libraries and resources. getapk pulls them all and bundles them, base first, into one `.apks` file that [AppSleuth](https://github.com/RansomWhisker/AppSleuth) and bundletool-style tools understand.
24
+
25
+ ## Install
26
+
27
+ pipx install getapk
28
+
29
+ Requires Python 3.9+ and Android platform-tools (`adb`) on your PATH. Works on macOS, Linux and Windows. No Python dependencies.
30
+
31
+ On the phone: enable Developer options → USB debugging, connect, and accept the authorisation prompt. No root needed.
32
+
33
+ ## Use
34
+
35
+ getapk --list # user-installed apps with versions and installer
36
+ getapk --list -m bank # filter by text
37
+ getapk com.example.app -o ./apps # one app
38
+ getapk com.a.app com.b.app # several
39
+ getapk --all -o ./apps # every user-installed app
40
+ getapk --all --include-system # include preinstalled apps
41
+ getapk -s SERIAL com.example.app # pick a device when several are connected
42
+
43
+ Single-APK apps are written as `<package>-<version>.apk`; split apps as `<package>-<version>.apks`. Use `--format apks` to always bundle. Then:
44
+
45
+ appsleuth scan com.example.app-1.2.3.apks
46
+
47
+ ## Security
48
+
49
+ * Package names, device serials and every path reported by the device are validated before use; nothing is passed through a shell on your computer, and the one command run on the phone only ever receives a validated package name.
50
+ * Output file names are sanitised, so a hostile device cannot write outside the output directory.
51
+ * Each pulled file must be a valid APK (a zip containing `AndroidManifest.xml`).
52
+ * Files are written to a temporary directory and moved into place only when complete.
53
+
54
+ Only use getapk on devices and apps you own or are authorised to analyse. Apps may be copyrighted; extraction is for your own security analysis.
55
+
56
+ ## Tests
57
+
58
+ python -m unittest discover -s tests -t .
59
+
60
+ MIT licence · © Rudranetra India Private Limited
getapk-1.0.0/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # getAPK
2
+
3
+ **Pull an installed app off your Android phone as an `.apk` (or an `.apks` bundle for split apps) — in one command.**
4
+
5
+ `getapk` talks to your device through `adb`, finds where an app is installed, copies every APK file it consists of, checks each one is a real APK, and writes a single ready-to-analyse file with its SHA-256. It is the Android counterpart of [getIPA](https://github.com/RansomWhisker/getIPA).
6
+
7
+ > A tool by [**rudranetra.com**](https://rudranetra.com) · more at [rudranetra.com/tools](https://rudranetra.com/tools)
8
+
9
+ ## Why not just `adb pull`?
10
+
11
+ Most Play Store apps are installed as *split APKs*: `base.apk` plus one file per CPU architecture, language and screen density. Pulling only `base.apk` gives you something that will not install and is missing native libraries and resources. getapk pulls them all and bundles them, base first, into one `.apks` file that [AppSleuth](https://github.com/RansomWhisker/AppSleuth) and bundletool-style tools understand.
12
+
13
+ ## Install
14
+
15
+ pipx install getapk
16
+
17
+ Requires Python 3.9+ and Android platform-tools (`adb`) on your PATH. Works on macOS, Linux and Windows. No Python dependencies.
18
+
19
+ On the phone: enable Developer options → USB debugging, connect, and accept the authorisation prompt. No root needed.
20
+
21
+ ## Use
22
+
23
+ getapk --list # user-installed apps with versions and installer
24
+ getapk --list -m bank # filter by text
25
+ getapk com.example.app -o ./apps # one app
26
+ getapk com.a.app com.b.app # several
27
+ getapk --all -o ./apps # every user-installed app
28
+ getapk --all --include-system # include preinstalled apps
29
+ getapk -s SERIAL com.example.app # pick a device when several are connected
30
+
31
+ Single-APK apps are written as `<package>-<version>.apk`; split apps as `<package>-<version>.apks`. Use `--format apks` to always bundle. Then:
32
+
33
+ appsleuth scan com.example.app-1.2.3.apks
34
+
35
+ ## Security
36
+
37
+ * Package names, device serials and every path reported by the device are validated before use; nothing is passed through a shell on your computer, and the one command run on the phone only ever receives a validated package name.
38
+ * Output file names are sanitised, so a hostile device cannot write outside the output directory.
39
+ * Each pulled file must be a valid APK (a zip containing `AndroidManifest.xml`).
40
+ * Files are written to a temporary directory and moved into place only when complete.
41
+
42
+ Only use getapk on devices and apps you own or are authorised to analyse. Apps may be copyrighted; extraction is for your own security analysis.
43
+
44
+ ## Tests
45
+
46
+ python -m unittest discover -s tests -t .
47
+
48
+ MIT licence · © Rudranetra India Private Limited
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: getapk
3
+ Version: 1.0.0
4
+ Summary: Pull an installed app from an Android device over adb as an APK (or an APKS bundle for split APKs)
5
+ License: MIT
6
+ Project-URL: Homepage, https://rudranetra.com/tools
7
+ Project-URL: Source, https://github.com/RansomWhisker/getAPK
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Dynamic: license-file
12
+
13
+ # getAPK
14
+
15
+ **Pull an installed app off your Android phone as an `.apk` (or an `.apks` bundle for split apps) — in one command.**
16
+
17
+ `getapk` talks to your device through `adb`, finds where an app is installed, copies every APK file it consists of, checks each one is a real APK, and writes a single ready-to-analyse file with its SHA-256. It is the Android counterpart of [getIPA](https://github.com/RansomWhisker/getIPA).
18
+
19
+ > A tool by [**rudranetra.com**](https://rudranetra.com) · more at [rudranetra.com/tools](https://rudranetra.com/tools)
20
+
21
+ ## Why not just `adb pull`?
22
+
23
+ Most Play Store apps are installed as *split APKs*: `base.apk` plus one file per CPU architecture, language and screen density. Pulling only `base.apk` gives you something that will not install and is missing native libraries and resources. getapk pulls them all and bundles them, base first, into one `.apks` file that [AppSleuth](https://github.com/RansomWhisker/AppSleuth) and bundletool-style tools understand.
24
+
25
+ ## Install
26
+
27
+ pipx install getapk
28
+
29
+ Requires Python 3.9+ and Android platform-tools (`adb`) on your PATH. Works on macOS, Linux and Windows. No Python dependencies.
30
+
31
+ On the phone: enable Developer options → USB debugging, connect, and accept the authorisation prompt. No root needed.
32
+
33
+ ## Use
34
+
35
+ getapk --list # user-installed apps with versions and installer
36
+ getapk --list -m bank # filter by text
37
+ getapk com.example.app -o ./apps # one app
38
+ getapk com.a.app com.b.app # several
39
+ getapk --all -o ./apps # every user-installed app
40
+ getapk --all --include-system # include preinstalled apps
41
+ getapk -s SERIAL com.example.app # pick a device when several are connected
42
+
43
+ Single-APK apps are written as `<package>-<version>.apk`; split apps as `<package>-<version>.apks`. Use `--format apks` to always bundle. Then:
44
+
45
+ appsleuth scan com.example.app-1.2.3.apks
46
+
47
+ ## Security
48
+
49
+ * Package names, device serials and every path reported by the device are validated before use; nothing is passed through a shell on your computer, and the one command run on the phone only ever receives a validated package name.
50
+ * Output file names are sanitised, so a hostile device cannot write outside the output directory.
51
+ * Each pulled file must be a valid APK (a zip containing `AndroidManifest.xml`).
52
+ * Files are written to a temporary directory and moved into place only when complete.
53
+
54
+ Only use getapk on devices and apps you own or are authorised to analyse. Apps may be copyrighted; extraction is for your own security analysis.
55
+
56
+ ## Tests
57
+
58
+ python -m unittest discover -s tests -t .
59
+
60
+ MIT licence · © Rudranetra India Private Limited
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ getapk.egg-info/PKG-INFO
5
+ getapk.egg-info/SOURCES.txt
6
+ getapk.egg-info/dependency_links.txt
7
+ getapk.egg-info/entry_points.txt
8
+ getapk.egg-info/top_level.txt
9
+ getapk_cli/__init__.py
10
+ getapk_cli/cli.py
11
+ tests/test_getapk.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ getapk = getapk_cli.cli:main
@@ -0,0 +1 @@
1
+ getapk_cli
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,251 @@
1
+ """getapk: pull installed apps from an Android device over adb.
2
+
3
+ Single-APK apps become `<package>-<version>.apk`. Apps installed as split APKs (most Play Store
4
+ apps today) become `<package>-<version>.apks`, a zip holding base.apk and every split, which
5
+ AppSleuth and bundletool-style tools understand. Only use it on devices and apps you are
6
+ authorised to analyse.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import hashlib
12
+ import os
13
+ import re
14
+ import shutil
15
+ import subprocess
16
+ import sys
17
+ import tempfile
18
+ import zipfile
19
+ from typing import Dict, List, Optional
20
+
21
+ from . import __version__
22
+
23
+ PKG_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z0-9_]+)+$")
24
+ SERIAL_RE = re.compile(r"^[A-Za-z0-9._:\-]{1,100}$")
25
+ DEVICE_PATH_RE = re.compile(r"^/[A-Za-z0-9_./=~+@\-]+\.apk$")
26
+ ADB_TIMEOUT = 120
27
+ PULL_TIMEOUT = 900
28
+
29
+
30
+ class AdbError(Exception):
31
+ pass
32
+
33
+
34
+ def die(msg: str, code: int = 1):
35
+ print(f"error: {msg}", file=sys.stderr)
36
+ sys.exit(code)
37
+
38
+
39
+ class Adb:
40
+ def __init__(self, serial: Optional[str] = None, exe: Optional[str] = None):
41
+ self.exe = exe or shutil.which("adb")
42
+ if not self.exe:
43
+ raise AdbError("adb not found. Install Android platform-tools and put adb on your PATH "
44
+ "(or pass --adb /path/to/adb).")
45
+ if serial is not None and not SERIAL_RE.match(serial):
46
+ raise AdbError("invalid device serial")
47
+ self.serial = serial
48
+
49
+ def _base(self) -> List[str]:
50
+ return [self.exe] + (["-s", self.serial] if self.serial else [])
51
+
52
+ def run(self, *args: str, timeout: int = ADB_TIMEOUT) -> str:
53
+ try:
54
+ r = subprocess.run(self._base() + list(args), capture_output=True, text=True,
55
+ encoding="utf-8", errors="replace", timeout=timeout)
56
+ except subprocess.TimeoutExpired:
57
+ raise AdbError(f"adb {' '.join(args[:2])} timed out")
58
+ if r.returncode != 0:
59
+ raise AdbError((r.stderr or r.stdout).strip()[:300] or f"adb exited {r.returncode}")
60
+ return r.stdout
61
+
62
+ def devices(self) -> List[Dict[str, str]]:
63
+ out = subprocess.run([self.exe, "devices", "-l"], capture_output=True, text=True,
64
+ encoding="utf-8", errors="replace", timeout=ADB_TIMEOUT).stdout
65
+ rows = []
66
+ for line in out.splitlines()[1:]:
67
+ parts = line.split()
68
+ if len(parts) >= 2:
69
+ model = next((p.split(":", 1)[1] for p in parts[2:] if p.startswith("model:")), "")
70
+ rows.append({"serial": parts[0], "state": parts[1], "model": model})
71
+ return rows
72
+
73
+ def check_device(self):
74
+ ds = self.devices()
75
+ ready = [d for d in ds if d["state"] == "device"]
76
+ if self.serial:
77
+ if not any(d["serial"] == self.serial and d["state"] == "device" for d in ds):
78
+ raise AdbError(f"device {self.serial} not found or not authorised (check the phone for the USB debugging prompt)")
79
+ elif not ready:
80
+ bad = [d for d in ds if d["state"] != "device"]
81
+ raise AdbError("no authorised device. " + (f"{bad[0]['serial']} is '{bad[0]['state']}': "
82
+ "unlock the phone and accept the USB debugging prompt." if bad else
83
+ "Connect a device with USB debugging enabled."))
84
+ elif len(ready) > 1:
85
+ raise AdbError("several devices connected; choose one with -s SERIAL: " +
86
+ ", ".join(f"{d['serial']} ({d['model']})" for d in ready))
87
+
88
+ def packages(self, include_system: bool = False) -> List[str]:
89
+ out = self.run("shell", "pm", "list", "packages", *([] if include_system else ["-3"]))
90
+ pk = sorted({l.split(":", 1)[1].strip() for l in out.splitlines() if l.startswith("package:")})
91
+ return [p for p in pk if PKG_RE.match(p)]
92
+
93
+ def apk_paths(self, package: str) -> List[str]:
94
+ if not PKG_RE.match(package):
95
+ raise AdbError(f"invalid package name: {package!r}")
96
+ out = self.run("shell", "pm", "path", package)
97
+ paths = [l.split(":", 1)[1].strip() for l in out.splitlines() if l.startswith("package:")]
98
+ bad = [p for p in paths if not DEVICE_PATH_RE.match(p)]
99
+ if bad:
100
+ raise AdbError(f"unexpected path reported by the device: {bad[0][:80]!r}")
101
+ # base first, then splits, in a stable order
102
+ return sorted(paths, key=lambda p: (os.path.basename(p) != "base.apk", p))
103
+
104
+ def version(self, package: str) -> Dict[str, str]:
105
+ info = {"name": "", "code": "", "installer": ""}
106
+ try:
107
+ out = self.run("shell", "dumpsys", "package", package)
108
+ except AdbError:
109
+ return info
110
+ for line in out.splitlines():
111
+ line = line.strip()
112
+ if line.startswith("versionName=") and not info["name"]:
113
+ info["name"] = line.split("=", 1)[1].strip()
114
+ elif line.startswith("versionCode=") and not info["code"]:
115
+ info["code"] = line.split("=", 1)[1].split()[0]
116
+ elif line.startswith("installerPackageName=") and not info["installer"]:
117
+ info["installer"] = line.split("=", 1)[1].strip()
118
+ return info
119
+
120
+ def pull(self, remote: str, local: str):
121
+ self.run("pull", remote, local, timeout=PULL_TIMEOUT)
122
+
123
+
124
+ def safe_part(s: str) -> str:
125
+ return re.sub(r"[^A-Za-z0-9._+-]", "_", s)[:80] or "unknown"
126
+
127
+
128
+ def sha256_file(path: str) -> str:
129
+ h = hashlib.sha256()
130
+ with open(path, "rb") as fh:
131
+ for chunk in iter(lambda: fh.read(1 << 20), b""):
132
+ h.update(chunk)
133
+ return h.hexdigest()
134
+
135
+
136
+ def looks_like_apk(path: str) -> bool:
137
+ try:
138
+ with zipfile.ZipFile(path) as z:
139
+ return "AndroidManifest.xml" in z.namelist()
140
+ except (zipfile.BadZipFile, OSError):
141
+ return False
142
+
143
+
144
+ def pull_app(adb: Adb, package: str, outdir: str, fmt: str = "auto", quiet: bool = False) -> Dict[str, object]:
145
+ paths = adb.apk_paths(package)
146
+ if not paths:
147
+ raise AdbError(f"{package} is not installed")
148
+ ver = adb.version(package)
149
+ stem = safe_part(package) + (f"-{safe_part(ver['name'])}" if ver["name"] else "")
150
+ os.makedirs(outdir, exist_ok=True)
151
+ work = tempfile.mkdtemp(prefix="getapk_")
152
+ try:
153
+ local = []
154
+ for i, remote in enumerate(paths):
155
+ name = safe_part(os.path.basename(remote))
156
+ if name in [os.path.basename(x) for x in local]:
157
+ name = f"{i}_{name}"
158
+ dst = os.path.join(work, name)
159
+ adb.pull(remote, dst)
160
+ if not looks_like_apk(dst):
161
+ raise AdbError(f"{name} did not pull as a valid APK")
162
+ local.append(dst)
163
+ split = len(local) > 1
164
+ if fmt == "apk" and split:
165
+ raise AdbError(f"{package} is a split-APK app ({len(local)} files); use --format auto or apks")
166
+ if split or fmt == "apks":
167
+ final = os.path.join(outdir, stem + ".apks")
168
+ tmp_out = final + ".part"
169
+ with zipfile.ZipFile(tmp_out, "w", zipfile.ZIP_STORED) as z: # APKs are already compressed
170
+ for f in local:
171
+ z.write(f, os.path.basename(f))
172
+ os.replace(tmp_out, final)
173
+ else:
174
+ final = os.path.join(outdir, stem + ".apk")
175
+ shutil.copyfile(local[0], final)
176
+ return {"package": package, "path": final, "splits": len(local), "version": ver["name"],
177
+ "size": os.path.getsize(final), "sha256": sha256_file(final), "installer": ver["installer"]}
178
+ finally:
179
+ shutil.rmtree(work, ignore_errors=True)
180
+
181
+
182
+ def fmt_size(n: int) -> str:
183
+ return f"{n / 1e6:.1f} MB" if n >= 1e6 else f"{n / 1e3:.0f} KB"
184
+
185
+
186
+ def cmd_list(adb: Adb, a) -> int:
187
+ pk = adb.packages(a.include_system)
188
+ if a.match:
189
+ pk = [p for p in pk if a.match.lower() in p.lower()]
190
+ if not pk:
191
+ print("No matching apps.")
192
+ return 1
193
+ for i, p in enumerate(pk, 1):
194
+ v = adb.version(p)
195
+ print(f"{i:3}. {p:<55} {v['name'] or '?':<14} {v['installer'] or ''}")
196
+ return 0
197
+
198
+
199
+ def main(argv: Optional[List[str]] = None):
200
+ for s in (sys.stdout, sys.stderr):
201
+ try:
202
+ s.reconfigure(encoding="utf-8", errors="replace")
203
+ except (AttributeError, ValueError):
204
+ pass
205
+ ap = argparse.ArgumentParser(prog="getapk", description=__doc__,
206
+ formatter_class=argparse.RawDescriptionHelpFormatter)
207
+ ap.add_argument("package", nargs="*", help="package name(s) to pull, e.g. com.example.app")
208
+ ap.add_argument("-l", "--list", action="store_true", help="list installed apps")
209
+ ap.add_argument("--all", action="store_true", help="pull every (user-installed) app")
210
+ ap.add_argument("-m", "--match", help="with --list/--all: only packages containing this text")
211
+ ap.add_argument("--include-system", action="store_true", help="include system/preinstalled apps")
212
+ ap.add_argument("-s", "--serial", help="device serial (needed when several devices are connected)")
213
+ ap.add_argument("-o", "--out", default=".", help="output directory (default: current)")
214
+ ap.add_argument("--format", choices=["auto", "apk", "apks"], default="auto",
215
+ help="auto: .apk for single-APK apps, .apks bundle for split apps")
216
+ ap.add_argument("--adb", help="path to the adb executable")
217
+ ap.add_argument("--version", action="version", version=f"getapk {__version__}")
218
+ a = ap.parse_args(argv)
219
+ try:
220
+ adb = Adb(a.serial, a.adb)
221
+ adb.check_device()
222
+ if a.list:
223
+ sys.exit(cmd_list(adb, a))
224
+ targets = list(a.package)
225
+ if a.all:
226
+ targets += [p for p in adb.packages(a.include_system)
227
+ if (not a.match or a.match.lower() in p.lower()) and p not in targets]
228
+ if not targets:
229
+ ap.print_usage(sys.stderr)
230
+ die("give a package name, or use --list / --all", 2)
231
+ failures = 0
232
+ for p in targets:
233
+ try:
234
+ r = pull_app(adb, p, a.out, a.format)
235
+ kind = f"{r['splits']} splits" if r["splits"] > 1 else "single APK"
236
+ print(f"[+] {r['package']} {r['version'] or ''} -> {r['path']} ({kind}, {fmt_size(r['size'])})")
237
+ print(f" sha256 {r['sha256']}")
238
+ except AdbError as e:
239
+ failures += 1
240
+ print(f"[-] {p}: {e}", file=sys.stderr)
241
+ if failures:
242
+ sys.exit(1)
243
+ except AdbError as e:
244
+ die(str(e))
245
+ except KeyboardInterrupt:
246
+ print("\ninterrupted", file=sys.stderr)
247
+ sys.exit(130)
248
+
249
+
250
+ if __name__ == "__main__":
251
+ main()
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "getapk"
7
+ version = "1.0.0"
8
+ description = "Pull an installed app from an Android device over adb as an APK (or an APKS bundle for split APKs)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ dependencies = []
13
+
14
+ [project.scripts]
15
+ getapk = "getapk_cli.cli:main"
16
+
17
+ [project.urls]
18
+ Homepage = "https://rudranetra.com/tools"
19
+ Source = "https://github.com/RansomWhisker/getAPK"
20
+
21
+ [tool.setuptools]
22
+ packages = ["getapk_cli"]
getapk-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,79 @@
1
+ import os
2
+ import tempfile
3
+ import unittest
4
+ import zipfile
5
+
6
+ from getapk_cli import cli
7
+
8
+
9
+ def make_apk(path):
10
+ with zipfile.ZipFile(path, "w") as z:
11
+ z.writestr("AndroidManifest.xml", b"x")
12
+
13
+
14
+ class FakeAdb(cli.Adb):
15
+ def __init__(self, paths, bad_apk=False):
16
+ self.exe, self.serial = "adb", None
17
+ self._paths, self._bad = paths, bad_apk
18
+
19
+ def run(self, *args, timeout=0):
20
+ if args[:2] == ("shell", "pm") and args[2] == "path":
21
+ return "".join(f"package:{p}\n" for p in self._paths)
22
+ if args[:2] == ("shell", "dumpsys"):
23
+ return " versionCode=12 minSdk=24\n versionName=1.2.3\n installerPackageName=com.android.vending\n"
24
+ if args[0] == "pull":
25
+ if self._bad:
26
+ open(args[2], "wb").write(b"not a zip")
27
+ else:
28
+ make_apk(args[2])
29
+ return ""
30
+ raise AssertionError(args)
31
+
32
+
33
+ class Pull(unittest.TestCase):
34
+ def test_single_apk(self):
35
+ out = tempfile.mkdtemp()
36
+ r = cli.pull_app(FakeAdb(["/data/app/x/base.apk"]), "com.example.app", out)
37
+ self.assertTrue(r["path"].endswith("com.example.app-1.2.3.apk"))
38
+ self.assertEqual(r["splits"], 1)
39
+
40
+ def test_split_becomes_apks_base_first(self):
41
+ out = tempfile.mkdtemp()
42
+ paths = ["/data/app/x/split_config.en.apk", "/data/app/x/base.apk", "/data/app/x/split_config.arm64_v8a.apk"]
43
+ r = cli.pull_app(FakeAdb(paths), "com.example.app", out)
44
+ self.assertTrue(r["path"].endswith(".apks"))
45
+ with zipfile.ZipFile(r["path"]) as z:
46
+ self.assertEqual(z.namelist()[0], "base.apk")
47
+ self.assertEqual(len(z.namelist()), 3)
48
+
49
+ def test_rejects_non_apk(self):
50
+ with self.assertRaises(cli.AdbError):
51
+ cli.pull_app(FakeAdb(["/data/app/x/base.apk"], bad_apk=True), "com.example.app", tempfile.mkdtemp())
52
+
53
+ def test_format_apk_refuses_split(self):
54
+ with self.assertRaises(cli.AdbError):
55
+ cli.pull_app(FakeAdb(["/data/app/x/base.apk", "/data/app/x/split_a.apk"]), "com.example.app",
56
+ tempfile.mkdtemp(), fmt="apk")
57
+
58
+
59
+ class Validation(unittest.TestCase):
60
+ def test_package_names(self):
61
+ for bad in ("a; rm -rf /", "$(id)", "com.example.app && x", "../x", "single", ""):
62
+ with self.assertRaises(cli.AdbError, msg=bad):
63
+ FakeAdb([]).apk_paths(bad)
64
+
65
+ def test_hostile_device_path_rejected(self):
66
+ with self.assertRaises(cli.AdbError):
67
+ FakeAdb(["/data/app/x/base.apk; id"]).apk_paths("com.example.app")
68
+ with self.assertRaises(cli.AdbError):
69
+ FakeAdb(["../../etc/passwd"]).apk_paths("com.example.app")
70
+
71
+ def test_serial_and_safe_name(self):
72
+ with self.assertRaises(cli.AdbError):
73
+ cli.Adb("x; y", exe="adb")
74
+ self.assertEqual(cli.safe_part("../../a b"), ".._.._a_b")
75
+ self.assertNotIn("/", cli.safe_part("a/b"))
76
+
77
+
78
+ if __name__ == "__main__":
79
+ unittest.main()