159 lines
6.1 KiB
Python
159 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Checks every Android / Apache HttpClient method and field the dex references against
|
|
published API declarations (NativeScript's android-platform-*.d.ts). Catches stub mistakes
|
|
(wrong name, arity, parameter or return kind) that would otherwise surface as
|
|
NoSuchMethodError on the phone. usage: check_api.py classes.dex android-platform-17.d.ts"""
|
|
import re
|
|
import sys
|
|
from loguru import logger
|
|
|
|
logger.remove()
|
|
from androguard.core.dex import DEX
|
|
|
|
dex_path, dts_path = sys.argv[1], sys.argv[2]
|
|
|
|
# ----------------------------------------------------------- parse the .d.ts
|
|
classes = {} # fqcn -> {"supers": [...], "methods": {name: [(params, ret)]}, "fields": set()}
|
|
stack = [] # (kind, name, depth_at_open)
|
|
depth = 0
|
|
cur = None
|
|
cls_re = re.compile(r"export (?:abstract )?class (\w+)(?:<[^{]*?>)?(?:\s+extends ([\w.]+)(?:<.*?>)?)?(?:\s+implements (.+?))?\s*\{")
|
|
mod_re = re.compile(r"(?:declare|export) module ([\w.]+) \{")
|
|
meth_re = re.compile(r"^\s*(?:public |protected )?(?:static )?(\w+)(?:<[^(]*>)?\((.*)\): (.+);\s*$")
|
|
field_re = re.compile(r"^\s*public (?:static )?(\w+): (.+);\s*$")
|
|
|
|
|
|
def strip_generics(t):
|
|
out, d = "", 0
|
|
for ch in t:
|
|
if ch == "<":
|
|
d += 1
|
|
elif ch == ">":
|
|
d -= 1
|
|
elif d == 0:
|
|
out += ch
|
|
return out.strip()
|
|
|
|
|
|
for line in open(dts_path, encoding="utf-8"):
|
|
m = mod_re.search(line)
|
|
c = cls_re.search(line)
|
|
if c:
|
|
name = ".".join([s[1] for s in stack if s[0] in ("mod", "cls")] + [c.group(1)])
|
|
supers = []
|
|
if c.group(2):
|
|
supers.append(c.group(2))
|
|
if c.group(3):
|
|
supers += [strip_generics(s) for s in strip_generics(c.group(3)).split(",")]
|
|
classes.setdefault(name, {"supers": [], "methods": {}, "fields": set()})["supers"] += supers
|
|
stack.append(("cls", c.group(1), depth))
|
|
cur = classes[name]
|
|
elif m:
|
|
for part in m.group(1).split("."):
|
|
stack.append(("mod", part, depth))
|
|
elif cur is not None and stack and stack[-1][0] == "cls":
|
|
mm = meth_re.match(line)
|
|
if mm and mm.group(1) not in ("constructor",):
|
|
params = [p for p in mm.group(2).split(", ") if p] if mm.group(2).strip() else []
|
|
ptypes = [strip_generics(p.split(": ", 1)[1]) if ": " in p else "any" for p in params]
|
|
cur["methods"].setdefault(mm.group(1), []).append((ptypes, strip_generics(mm.group(3))))
|
|
elif "public constructor(" in line and "implementation" not in line:
|
|
inner = line.split("public constructor(", 1)[1].rsplit(");", 1)[0]
|
|
params = [p for p in inner.split(", ") if p] if inner.strip() else []
|
|
cur["methods"].setdefault("<init>", []).append(
|
|
([strip_generics(p.split(": ", 1)[1]) for p in params], "void"))
|
|
else:
|
|
fm = field_re.match(line)
|
|
if fm:
|
|
cur["fields"].add(fm.group(1))
|
|
depth += line.count("{") - line.count("}")
|
|
while stack and depth <= stack[-1][2]:
|
|
stack.pop()
|
|
cur = classes.get(".".join(s[1] for s in stack if s[0] in ("mod", "cls"))) if stack and stack[-1][0] == "cls" else None
|
|
|
|
# ------------------------------------------------------------- type mapping
|
|
NUM = {"I", "J", "F", "D", "S", "B", "C"}
|
|
|
|
|
|
def ts_of(desc):
|
|
if desc in NUM:
|
|
return "number"
|
|
if desc == "Z":
|
|
return "boolean"
|
|
if desc == "V":
|
|
return "void"
|
|
if desc.startswith("["):
|
|
return "native.Array<%s>" % ts_of(desc[1:])
|
|
n = desc[1:-1].replace("/", ".").replace("$", ".")
|
|
if n in ("java.lang.String", "java.lang.CharSequence"):
|
|
return "string"
|
|
return n
|
|
|
|
|
|
def compatible(java, ts):
|
|
ts = ts.replace(" ", "")
|
|
if ts in ("any", "any[]"):
|
|
return True
|
|
if java.startswith("native.Array"):
|
|
return ts.startswith("native.Array") or ts.endswith("[]")
|
|
if java == "java.lang.Object":
|
|
return True
|
|
return java == ts or (java == "string" and ts == "string")
|
|
|
|
|
|
def lookup(cls, name, params, ret, seen=None):
|
|
seen = seen or set()
|
|
if cls in seen or cls not in classes:
|
|
return False
|
|
seen.add(cls)
|
|
for ptypes, r in classes[cls]["methods"].get(name, []):
|
|
if len(ptypes) == len(params) and all(compatible(j, t) for j, t in zip(params, ptypes)):
|
|
if name == "<init>" or compatible(ret, r) or r == "any":
|
|
return True
|
|
return any(lookup(s, name, params, ret, seen) for s in classes[cls]["supers"])
|
|
|
|
|
|
def has_field(cls, name, seen=None):
|
|
seen = seen or set()
|
|
if cls in seen or cls not in classes:
|
|
return False
|
|
seen.add(cls)
|
|
return name in classes[cls]["fields"] or any(has_field(s, name, seen) for s in classes[cls]["supers"])
|
|
|
|
|
|
# ------------------------------------------------------------- check the dex
|
|
raw = open(dex_path, "rb").read()
|
|
if raw[:2] == b"PK":
|
|
import zipfile
|
|
raw = zipfile.ZipFile(dex_path).read("classes.dex")
|
|
d = DEX(raw)
|
|
own = {c.get_name() for c in d.get_classes()}
|
|
bad, checked, seen = [], 0, set()
|
|
for mid in d.get_methods_id_item().get_obj():
|
|
cname = mid.get_class_name()
|
|
if not (cname.startswith("Landroid/") or cname.startswith("Lorg/apache/http/")) or cname in own:
|
|
continue
|
|
desc = mid.get_descriptor().replace(" ", "")
|
|
key = (cname, mid.get_name(), desc)
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
plist = re.findall(r"\[*(?:L[^;]+;|[ZBCSIJFD])", desc[1:desc.index(")")])
|
|
ret = desc[desc.index(")") + 1:]
|
|
cls = cname[1:-1].replace("/", ".").replace("$", ".")
|
|
checked += 1
|
|
if not lookup(cls, mid.get_name(), [ts_of(p) for p in plist], ts_of(ret)):
|
|
bad.append("%s.%s%s" % (cls, mid.get_name(), desc))
|
|
for fid in d.get_fields_id_item().get_obj():
|
|
cname = fid.get_class_name()
|
|
if not (cname.startswith("Landroid/") or cname.startswith("Lorg/apache/http/")) or cname in own:
|
|
continue
|
|
cls = cname[1:-1].replace("/", ".").replace("$", ".")
|
|
checked += 1
|
|
if not has_field(cls, fid.get_name()):
|
|
bad.append("field %s.%s" % (cls, fid.get_name()))
|
|
print("checked %d framework references against %d declared classes" % (checked, len(classes)))
|
|
for b in bad:
|
|
print(" NOT FOUND:", b)
|
|
sys.exit(1 if bad else 0)
|