Import GingerCal 1.0 source
This commit is contained in:
commit
01f49852ab
116 files changed
+5079
No files matched your search
@@ -0,0 +1,158 @@
|
||||
#!/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)
|
||||
Reference in new issue
Block a user