apkdev 2.0.0__py3-none-any.whl

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.
apkdev/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ apkdev — Android APK Development & Reverse Engineering Toolkit.
3
+
4
+ One command for everything APK:
5
+ - Create Kotlin projects with Gradle
6
+ - Build, sign, and install APKs
7
+ - Decode APKs with apktool
8
+ - Decompile to Java with jadx
9
+ - Disassemble/assemble smali
10
+ - Convert DEX <-> JAR
11
+ """
12
+
13
+ __version__ = "2.0.0"
apkdev/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow running as: python -m apkdev"""
2
+ from .cli import cli
3
+
4
+ if __name__ == "__main__":
5
+ cli()
apkdev/apkfile.py ADDED
@@ -0,0 +1,168 @@
1
+ """APK file operations — pure Python, no external dependencies."""
2
+
3
+ import zipfile
4
+ import os
5
+ from pathlib import Path
6
+
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+ from rich.panel import Panel
10
+ from rich.text import Text
11
+
12
+ console = Console()
13
+
14
+
15
+ def list_contents(apk: str) -> None:
16
+ """List files inside an APK using pure Python zipfile module."""
17
+ apk_path = Path(apk)
18
+ if not apk_path.exists():
19
+ console.print(f"[red]✘ File not found: {apk}[/]")
20
+ return
21
+
22
+ try:
23
+ with zipfile.ZipFile(str(apk_path), "r") as zf:
24
+ entries = zf.infolist()
25
+ except zipfile.BadZipFile:
26
+ console.print(f"[red]✘ Invalid APK file: {apk}[/]")
27
+ return
28
+
29
+ if not entries:
30
+ console.print("[dim]Empty APK[/]")
31
+ return
32
+
33
+ # Gather file info
34
+ files = []
35
+ dirs = {}
36
+ total_size = 0
37
+
38
+ for entry in entries:
39
+ name = entry.filename
40
+ size = entry.file_size
41
+ total_size += size
42
+ files.append((size, name, name.endswith("/")))
43
+
44
+ parts = name.rstrip("/").split("/")
45
+ if len(parts) > 1 and not name.endswith("/"):
46
+ dir_name = parts[0]
47
+ dirs.setdefault(dir_name, {"count": 0, "size": 0})
48
+ dirs[dir_name]["count"] += 1
49
+ dirs[dir_name]["size"] += size
50
+ elif name.endswith("/"):
51
+ # Directory entry
52
+ dir_name = name.rstrip("/")
53
+ dirs.setdefault(dir_name, {"count": 0, "size": 0})
54
+
55
+ # Add root files
56
+ root_count = sum(1 for s, n, d in files if "/" not in n and not d)
57
+ root_size = sum(s for s, n, d in files if "/" not in n and not d)
58
+ if root_count > 0:
59
+ dirs["(root)"] = {"count": root_count, "size": root_size}
60
+
61
+ # Summary
62
+ console.print(f"[bold cyan]📦 {apk_path.name}[/] — {len([f for f in files if not f[2]])} files, {total_size/1024/1024:.1f} MB")
63
+ console.print()
64
+
65
+ # Directory table
66
+ t = Table(border_style="blue")
67
+ t.add_column("Directory", style="bold")
68
+ t.add_column("Files", justify="right")
69
+ t.add_column("Size", justify="right")
70
+
71
+ for dir_name, info in sorted(dirs.items()):
72
+ size_str = f"{info['size']/1024:.0f} KB" if info['size'] < 1024*1024 else f"{info['size']/1024/1024:.1f} MB"
73
+ t.add_row(f"📁 {dir_name}", str(info["count"]), size_str)
74
+ console.print(t)
75
+
76
+ # Notable files
77
+ console.print()
78
+ notable_extensions = {".dex", ".so", ".apk", ".xml", ".arsc", ".png", ".jar", ".ttf", ".ogg", ".mp4"}
79
+ notable_names = {"AndroidManifest.xml", "resources.arsc", "classes.dex", "classes2.dex", "classes3.dex", "classes4.dex"}
80
+
81
+ notable = [(s, n) for s, n, d in files if not d and
82
+ (Path(n).suffix in notable_extensions or Path(n).name in notable_names)]
83
+ notable.sort(key=lambda x: -x[0])
84
+
85
+ if notable:
86
+ t2 = Table(border_style="cyan")
87
+ t2.add_column("File", style="bold")
88
+ t2.add_column("Size", justify="right")
89
+
90
+ icons = {".dex": "⚙️", ".so": "🦀", ".arsc": "🎨", ".xml": "📋", ".png": "🖼️",
91
+ ".jar": "📦", ".ttf": "🔤", ".ogg": "🎵", ".mp4": "🎬"}
92
+ name_icons = {"AndroidManifest.xml": "📋", "resources.arsc": "🎨"}
93
+
94
+ for size, name in notable[:40]:
95
+ ext = Path(name).suffix
96
+ icon = name_icons.get(Path(name).name) or icons.get(ext, "📄")
97
+ size_str = f"{size/1024:.0f} KB" if size < 1024*1024 else f"{size/1024/1024:.1f} MB"
98
+ t2.add_row(f"{icon} {name}", size_str)
99
+ console.print(t2)
100
+
101
+
102
+ def extract_apk(apk: str, output: str | None = None) -> None:
103
+ """Extract APK contents using pure Python zipfile module."""
104
+ apk_path = Path(apk)
105
+ if not apk_path.exists():
106
+ console.print(f"[red]✘ File not found: {apk}[/]")
107
+ return
108
+
109
+ out_dir = Path(output or apk_path.stem)
110
+ out_dir.mkdir(parents=True, exist_ok=True)
111
+
112
+ try:
113
+ with zipfile.ZipFile(str(apk_path), "r") as zf:
114
+ console.print(f"[yellow]📦 Extracting {apk_path.name} → {out_dir}/...[/]")
115
+ zf.extractall(path=str(out_dir))
116
+
117
+ count = sum(1 for _ in out_dir.rglob("*") if _.is_file())
118
+ console.print(f"[green]✔ Extracted {count} files → {out_dir}/[/]")
119
+ except Exception as e:
120
+ console.print(f"[red]✘ Extraction failed: {e}[/]")
121
+
122
+
123
+ def certificate(apk: str) -> None:
124
+ """Show APK signing certificate info using pure Python + apksigner if available."""
125
+ import shutil
126
+ import subprocess
127
+
128
+ apk_path = Path(apk)
129
+ if not apk_path.exists():
130
+ console.print(f"[red]✘ File not found: {apk}[/]")
131
+ return
132
+
133
+ # Try apksigner first (requires Java)
134
+ apksigner = shutil.which("apksigner") or shutil.which("apksigner.jar")
135
+ if apksigner:
136
+ r = subprocess.run(
137
+ [apksigner, "verify", "--print-certs", str(apk_path)],
138
+ capture_output=True, text=True, timeout=15
139
+ )
140
+ if r.returncode == 0 and r.stdout.strip():
141
+ lines = [l for l in r.stdout.strip().split("\n") if l.strip()]
142
+ for line in lines:
143
+ if ":" in line:
144
+ key, val = line.split(":", 1)
145
+ console.print(f" [bold]{key.strip()}:[/] {val.strip()}")
146
+ else:
147
+ console.print(f" {line}")
148
+ return
149
+
150
+ # Fallback: extract RSA from META-INF using pure Python
151
+ try:
152
+ with zipfile.ZipFile(str(apk_path), "r") as zf:
153
+ # Find signature files
154
+ sig_files = [n for n in zf.namelist() if n.startswith("META-INF/") and
155
+ (n.endswith(".RSA") or n.endswith(".DSA") or n.endswith(".EC") or
156
+ n.endswith(".SF") or "SIG" in n)]
157
+
158
+ if sig_files:
159
+ console.print(f"[yellow]Certificate info (extracted from APK metadata):[/]")
160
+ for sf in sig_files:
161
+ info = zf.getinfo(sf)
162
+ size_kb = info.file_size / 1024
163
+ console.print(f" 📜 {sf} ({size_kb:.1f} KB)")
164
+ console.print("\n[dim]For detailed cert info, install Java + apksigner[/]")
165
+ else:
166
+ console.print("[yellow]No signature files found — APK may be unsigned[/]")
167
+ except zipfile.BadZipFile:
168
+ console.print(f"[red]✘ Invalid APK file[/]")