Minamp 1.3

- Fix NoSuchMethodError on Android 2.x: Dalvik before 4.0 can't link Object
  methods (toString/equals/...) called through an interface. Patch the bundled
  Bouncy Castle (tools/patch_bc_dalvik.py) and our own CertVerifier, and make
  build.sh fail if tools/scan_dalvik.py finds any such call
- Treat a manually entered private address (10.x, 172.16-31.x, 192.168.x,
  .local) as home, so LAN servers that allow access without sign-in work
- Send Plex client headers on media requests (files and transcodes)
- Explain a 503 on a file as the server being unable to open it (drive or
  folder not available)
- Connection log: home/away, token presence, Plex's file check and a direct
  file request in the streaming test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xockoandClaude Opus 5.5 committed 2026-10-14 20:56:27 +02:00
1 parent a4b875ef34
commit 58d21b6d68
14 files changed
+218 -13

No files matched your search

+2 -2
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.minamp" package="com.minamp"
android:versionCode="3" android:versionCode="4"
android:versionName="1.2" android:versionName="1.3"
android:installLocation="auto"> android:installLocation="auto">
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" /> <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" />
+6
View File
@@ -22,6 +22,12 @@ javac -nowarn -Xlint:-options -source 8 -target 8 -encoding UTF-8 -g:source,line
-d $OUT/classes @$OUT/sources.txt 2>&1 | grep -v -E '^Picked up|^Note:' || true -d $OUT/classes @$OUT/sources.txt 2>&1 | grep -v -E '^Picked up|^Note:' || true
test -f $OUT/classes/com/minamp/HomeActivity.class test -f $OUT/classes/com/minamp/HomeActivity.class
echo "== Dalvik 2.x link check"
python3 tools/scan_dalvik.py $OUT/classes $ANDROID_JAR > $OUT/dalvik-scan.txt
if ! grep -q "^total patterns: 0$" $OUT/dalvik-scan.txt; then
cat $OUT/dalvik-scan.txt; echo "Calls Android 2.x can't link (see above): fix before shipping"; exit 1
fi
echo "== dex" echo "== dex"
java -Xmx2g -jar $T/dx.jar --dex --output=$OUT/classes.dex $OUT/classes 2>&1 | grep -v '^Picked up' || true java -Xmx2g -jar $T/dx.jar --dex --output=$OUT/classes.dex $OUT/classes 2>&1 | grep -v '^Picked up' || true
+17 -2
View File
@@ -499,7 +499,22 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
} }
lib.ensureMedia(t); lib.ensureMedia(t);
com.minamp.net.DiagLog.log("track: " + t.title + " [" + t.codec + "/" + t.container + " " + t.bitrate + " kbps] via " com.minamp.net.DiagLog.log("track: " + t.title + " [" + t.codec + "/" + t.container + " " + t.bitrate + " kbps] via "
+ (lib.server.active() == null ? "?" : Server.kindLabel(lib.server.active().kind))); + (lib.server.active() == null ? "?" : Server.kindLabel(lib.server.active().kind))
+ (lib.server.isLocal() ? " (home)" : " (away)"));
String tok = lib.server.token;
com.minamp.net.DiagLog.log("token: " + (tok == null || tok.length() == 0 ? "none (relying on the server allowing this network without sign-in)" : "set, " + tok.length() + " chars"));
com.minamp.net.DiagLog.log("server file check: " + lib.checkPart(t));
com.minamp.net.Http.Response fr = null;
try {
java.util.Map<String, String> h = lib.server.mediaHeaders();
h.put("Range", "bytes=0-15");
fr = com.minamp.net.Http.get(lib.directUrl(t), h, 20000);
com.minamp.net.DiagLog.log("original file request: " + fr.code + " " + (fr.code >= 400 ? com.minamp.plex.Plex.snippet(fr.readString()) : fr.header("content-type")));
} catch (Exception e) {
com.minamp.net.DiagLog.log("original file request failed: " + com.minamp.net.Threads.describe(e));
} finally {
com.minamp.net.Http.abort(fr);
}
int kbps = app.currentMaxKbps(); int kbps = app.currentMaxKbps();
if (kbps <= 0) kbps = 320; if (kbps <= 0) kbps = 320;
for (int v = 0; v < Library.VARIANTS; v++) { for (int v = 0; v < Library.VARIANTS; v++) {
@@ -508,7 +523,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
String url = lib.transcodeUrl(t, kbps, 0, session, v); String url = lib.transcodeUrl(t, kbps, 0, session, v);
com.minamp.net.Http.Response r = null; com.minamp.net.Http.Response r = null;
try { try {
r = com.minamp.net.Http.get(url, null, 30000); r = com.minamp.net.Http.get(url, lib.server.mediaHeaders(), 30000);
if (r.code < 400) { if (r.code < 400) {
byte[] buf = new byte[4096]; byte[] buf = new byte[4096];
int n = r.body.read(buf); int n = r.body.read(buf);
+1 -1
View File
@@ -161,7 +161,7 @@ public final class ImageLoader {
if (data == null) { if (data == null) {
if (app.server == null) return null; if (app.server == null) return null;
String url = app.server.imageUrl(j.thumb, j.size); String url = app.server.imageUrl(j.thumb, j.size);
Http.Response r = Http.get(url, null, 20000); Http.Response r = Http.get(url, app.server.mediaHeaders(), 20000);
if (r.code != 200) { r.close(); return null; } if (r.code != 200) { r.close(); return null; }
data = r.readAll(); data = r.readAll();
try { try {
+3 -2
View File
@@ -320,7 +320,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
Http.Response r = null; Http.Response r = null;
String why = null; String why = null;
for (int i = 0; i < urls.size(); i++) { for (int i = 0; i < urls.size(); i++) {
r = Http.get(urls.get(i), Plex.headers(null), 30000); r = Http.get(urls.get(i), App.get().server != null ? App.get().server.mediaHeaders() : Plex.headers(null), 30000);
if (r.code == 200) { if (r.code == 200) {
if (order != null) { if (order != null) {
final int idx = i; final int idx = i;
@@ -397,7 +397,8 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
refusalCode = 0; refusalCode = 0;
acceptedVariant = -1; acceptedVariant = -1;
// transcodes are fetched once and buffered, so player reconnects can't restart them // transcodes are fetched once and buffered, so player reconnects can't restart them
mp.setDataSource(proxy.register(routeId, urls, ext, transcoding)); mp.setDataSource(proxy.register(routeId, urls, ext, transcoding,
App.get().server != null ? App.get().server.mediaHeaders() : null));
} }
setState(BUFFERING, "Buffering…"); setState(BUFFERING, "Buffering…");
mp.prepareAsync(); mp.prepareAsync();
+2 -1
View File
@@ -160,7 +160,8 @@ public final class CertVerifier {
GeneralName n = arr[i]; GeneralName n = arr[i];
if (n.getTagNo() == GeneralName.dNSName) { if (n.getTagNo() == GeneralName.dNSName) {
sawDns = true; sawDns = true;
if (!ip && dnsMatch(n.getName().toString(), host)) return true; // String.valueOf, not getName().toString(): Dalvik < 4.0 can't call Object methods via an interface
if (!ip && dnsMatch(String.valueOf(n.getName()), host)) return true;
} else if (n.getTagNo() == GeneralName.iPAddress && ip) { } else if (n.getTagNo() == GeneralName.iPAddress && ip) {
byte[] a = ASN1OctetString.getInstance(n.getName()).getOctets(); byte[] a = ASN1OctetString.getInstance(n.getName()).getOctets();
if (ipString(a).equals(host)) return true; if (ipString(a).equals(host)) return true;
+12 -1
View File
@@ -50,6 +50,7 @@ public final class StreamProxy implements Runnable {
final String id; final String id;
final List<String> urls; final List<String> urls;
final boolean buffered; final boolean buffered;
Map<String, String> headers; // sent upstream with every request
int chosen = -1; // index that worked int chosen = -1; // index that worked
final List<Http.Response> live = new ArrayList<Http.Response>(); final List<Http.Response> live = new ArrayList<Http.Response>();
// buffered state (guarded by this) // buffered state (guarded by this)
@@ -95,8 +96,14 @@ public final class StreamProxy implements Runnable {
/** @return the local URL to give MediaPlayer, ending in an extension it may sniff. */ /** @return the local URL to give MediaPlayer, ending in an extension it may sniff. */
public String register(String id, List<String> remoteUrls, String ext, boolean buffered) { public String register(String id, List<String> remoteUrls, String ext, boolean buffered) {
return register(id, remoteUrls, ext, buffered, null);
}
public String register(String id, List<String> remoteUrls, String ext, boolean buffered, Map<String, String> headers) {
Route r = new Route(id, new ArrayList<String>(remoteUrls), buffered);
r.headers = headers;
synchronized (routes) { synchronized (routes) {
routes.put(id, new Route(id, new ArrayList<String>(remoteUrls), buffered)); routes.put(id, r);
} }
return "http://127.0.0.1:" + port() + "/s/" + id + "/stream." + ext; return "http://127.0.0.1:" + port() + "/s/" + id + "/stream." + ext;
} }
@@ -198,6 +205,7 @@ public final class StreamProxy implements Runnable {
/** Tries the route's URLs in order; returns the accepted response or throws with the server's words. */ /** Tries the route's URLs in order; returns the accepted response or throws with the server's words. */
private Http.Response openUpstream(Route route, String method, String range, int first, int last) throws IOException { private Http.Response openUpstream(Route route, String method, String range, int first, int last) throws IOException {
Map<String, String> h = new LinkedHashMap<String, String>(); Map<String, String> h = new LinkedHashMap<String, String>();
if (route.headers != null) h.putAll(route.headers);
h.put("Accept", "*/*"); h.put("Accept", "*/*");
if (range != null) h.put("Range", range); if (range != null) h.put("Range", range);
String refusal = null; String refusal = null;
@@ -216,6 +224,9 @@ public final class StreamProxy implements Runnable {
up.close(); up.close();
code = up.code; code = up.code;
refusal = "Server refused the stream (" + up.code + ")" + (body.trim().length() > 0 ? ": " + com.minamp.plex.Plex.snippet(body) : ""); refusal = "Server refused the stream (" + up.code + ")" + (body.trim().length() > 0 ? ": " + com.minamp.plex.Plex.snippet(body) : "");
if (up.code == 503 && route.urls.get(i).indexOf("/library/parts/") >= 0) {
refusal = "Plex couldn't open this file (503). Is the drive or folder with your music available to the server?";
}
DiagLog.log("stream variant " + i + " refused: " + refusal); DiagLog.log("stream variant " + i + " refused: " + refusal);
} }
throw new RefusedException(code, refusal); throw new RefusedException(code, refusal);
+19 -1
View File
@@ -251,12 +251,30 @@ public final class Library {
return server.url(base + "?" + q); return server.url(base + "?" + q);
} }
/** Whether the server can actually read a track's file (Plex's checkFiles), for diagnostics. */
public String checkPart(Item t) {
try {
Json.Obj mc = server.get("/library/metadata/" + t.ratingKey + "?checkFiles=1");
Json.Arr md = mc == null ? null : mc.optArr("Metadata");
Json.Obj m = md == null ? null : md.optObj(0);
Json.Arr media = m == null ? null : m.optArr("Media");
Json.Obj me = media == null ? null : media.optObj(0);
Json.Arr parts = me == null ? null : me.optArr("Part");
Json.Obj p = parts == null ? null : parts.optObj(0);
if (p == null) return "no file information returned";
return "exists=" + p.optString("exists", "?") + " accessible=" + p.optString("accessible", "?")
+ " file=" + p.optString("file", "?");
} catch (Exception e) {
return com.minamp.net.Threads.describe(e);
}
}
/** Ask the server what it would do with a transcode request (for the diagnostics screen). */ /** Ask the server what it would do with a transcode request (for the diagnostics screen). */
public String decision(Item t, int kbps, int variant) { public String decision(Item t, int kbps, int variant) {
try { try {
String url = transcodeUrl(t, kbps, 0, "minamp-check-" + System.currentTimeMillis(), variant); String url = transcodeUrl(t, kbps, 0, "minamp-check-" + System.currentTimeMillis(), variant);
url = url.replace("/start.mp3?", "/decision?").replace("/universal/start?", "/universal/decision?"); url = url.replace("/start.mp3?", "/decision?").replace("/universal/start?", "/universal/decision?");
com.minamp.net.Http.Response r = com.minamp.net.Http.get(url, Plex.headers(null), 20000); com.minamp.net.Http.Response r = com.minamp.net.Http.get(url, server.mediaHeaders(), 20000);
String body = r.readString(); String body = r.readString();
if (r.code != 200) return "HTTP " + r.code + (Plex.snippet(body).length() > 0 ? ": " + Plex.snippet(body) : ""); if (r.code != 200) return "HTTP " + r.code + (Plex.snippet(body).length() > 0 ? ": " + Plex.snippet(body) : "");
Json.Obj mc = Json.parseObj(body).optObj("MediaContainer"); Json.Obj mc = Json.parseObj(body).optObj("MediaContainer");
+30 -1
View File
@@ -73,7 +73,28 @@ public final class Server {
public boolean isLocal() { public boolean isLocal() {
Conn c = active; Conn c = active;
return c != null && (c.kind == LOCAL || c.kind == LOCAL_HTTP); if (c == null) return false;
if (c.kind == LOCAL || c.kind == LOCAL_HTTP) return true;
return c.kind == MANUAL && isPrivateAddress(c.uri);
}
/** A typed-in address on the home network (10.x, 172.16-31.x, 192.168.x, .local) counts as home. */
static boolean isPrivateAddress(String uri) {
String h = uri;
int s = h.indexOf("://");
if (s >= 0) h = h.substring(s + 3);
int end = h.length();
for (int i = 0; i < h.length(); i++) { char ch = h.charAt(i); if (ch == ':' || ch == '/') { end = i; break; } }
h = h.substring(0, end).toLowerCase();
if (h.endsWith(".local") || h.equals("localhost")) return true;
if (h.startsWith("10.") || h.startsWith("192.168.") || h.startsWith("127.")) return true;
if (h.startsWith("172.")) {
try {
int second = Integer.parseInt(h.substring(4, h.indexOf('.', 4)));
return second >= 16 && second <= 31;
} catch (Exception e) { return false; }
}
return false;
} }
public boolean isRelay() { public boolean isRelay() {
@@ -202,9 +223,17 @@ public final class Server {
/** Absolute URL with the token in the query string (for the stream proxy and images). */ /** Absolute URL with the token in the query string (for the stream proxy and images). */
public String url(String path) throws IOException { public String url(String path) throws IOException {
if (token == null || token.length() == 0) return base() + path; // never send an empty token
return base() + path + (path.indexOf('?') >= 0 ? "&" : "?") + "X-Plex-Token=" + Plex.enc(token); return base() + path + (path.indexOf('?') >= 0 ? "&" : "?") + "X-Plex-Token=" + Plex.enc(token);
} }
/** Headers for media requests (files, transcodes): Plex wants to know which client is asking. */
public java.util.Map<String, String> mediaHeaders() {
java.util.Map<String, String> h = Plex.headers(token);
h.put("Accept", "*/*");
return h;
}
public String imageUrl(String thumb, int size) throws IOException { public String imageUrl(String thumb, int size) throws IOException {
if (thumb == null || thumb.length() == 0) return null; if (thumb == null || thumb.length() == 0) return null;
return url("/photo/:/transcode?width=" + size + "&height=" + size + "&minSize=1&upscale=1&url=" + Plex.enc(thumb)); return url("/photo/:/transcode?width=" + size + "&height=" + size + "&minSize=1&upscale=1&url=" + Plex.enc(thumb));
+26
View File
@@ -196,6 +196,32 @@ public class PlexTest {
Server.Conn c3 = s3.pick(); Server.Conn c3 = s3.pick();
check(c3.kind == Server.LOCAL_HTTP, "falls back when TLS name check fails -> " + c3.uri); check(c3.kind == Server.LOCAL_HTTP, "falls back when TLS name check fails -> " + c3.uri);
// --- manual LAN address without a token (server allows this network without sign-in)
Server lan = new Server();
lan.name = "manual"; lan.token = "";
lan.add(Server.normalizeManual("127.0.0.1"), Server.MANUAL);
lan.pick();
check(lan.isLocal(), "manual private address counts as home (original quality)");
check(lan.url("/library/parts/501/1/file.mp3").indexOf("X-Plex-Token") < 0, "no empty X-Plex-Token on media URLs");
Library lanLib = new Library(lan, "1");
Item lanTrack = lanLib.tracksOf(album).get(0);
String lid = proxy.newId();
String lurl = proxy.register(lid, java.util.Collections.singletonList(lanLib.directUrl(lanTrack)), "mp3", false, lan.mediaHeaders());
hc = (HttpURLConnection) new URL(lurl).openConnection(Proxy.NO_PROXY);
byte[] lb = read(hc.getInputStream());
check(hc.getResponseCode() == 200 && lb.length == 300000, "token-less LAN: original MP3 streams (" + hc.getResponseCode() + ")");
Item flac = lanLib.tracksOf(album).get(1);
String lid2 = proxy.newId();
String lurl2 = proxy.register(lid2, lanLib.transcodeUrls(flac, 192, 0, "lan1"), "mp3", true, lan.mediaHeaders());
hc = (HttpURLConnection) new URL(lurl2).openConnection(Proxy.NO_PROXY);
byte[] lt = read(hc.getInputStream());
check(hc.getResponseCode() == 200 && new String(lt, "ISO-8859-1").startsWith("TRANSCODE"), "token-less LAN: FLAC converts to MP3");
// and the old behaviour reproduces the phone's log exactly
Http.Response o1 = Http.get(lanLib.directUrl(lanTrack) + "?X-Plex-Token=", null, 10000); o1.readAll();
Http.Response o2 = Http.get(lanLib.transcodeUrls(flac, 192, 0, "lan2").get(0) + "&X-Plex-Token=", null, 10000); o2.readAll();
check(o1.code == 503 && o2.code == 400, "old empty-token URLs reproduce the log's 503 and 400");
check(lanLib.checkPart(lanTrack).indexOf("exists=") >= 0 || lanLib.checkPart(lanTrack).length() > 0, "file check runs: " + lanLib.checkPart(lanTrack));
// --- trust checks // --- trust checks
CertVerifier.setRootStore(roots(args[1])); // real Mozilla roots: test CA must be rejected CertVerifier.setRootStore(roots(args[1])); // real Mozilla roots: test CA must be rejected
Http.evictAll(); Http.evictAll();
+11 -2
View File
@@ -107,16 +107,25 @@ class H(BaseHTTPRequestHandler):
if n: if n:
self.rfile.read(n) self.rfile.read(n)
u = urlparse(self.path) u = urlparse(self.path)
q = parse_qs(u.query) q = parse_qs(u.query, keep_blank_values=True)
p = u.path p = u.path
host = self.headers.get("Host", "") host = self.headers.get("Host", "")
tok = self.headers.get("X-Plex-Token") or (q.get("X-Plex-Token") or [None])[0] tok = self.headers.get("X-Plex-Token") or (q.get("X-Plex-Token") or [None])[0]
empty_token_param = "X-Plex-Token" in q and q["X-Plex-Token"][0] == "" and not self.headers.get("X-Plex-Token")
log.append((method, host, self.path, tok, self.headers.get("Range"))) log.append((method, host, self.path, tok, self.headers.get("Range")))
if host.startswith("plex.tv"): if host.startswith("plex.tv"):
return self.tv(method, p, tok) return self.tv(method, p, tok)
if p == "/identity": if p == "/identity":
return self.send_json({"MediaContainer": {"machineIdentifier": MACHINE, "version": "1.40"}}) return self.send_json({"MediaContainer": {"machineIdentifier": MACHINE, "version": "1.40"}})
if tok != TOKEN: # Like a server with "allowed without auth" for its LAN: no token at all is fine from here,
# but media requests carrying an *empty* token are refused (503 files / 400 transcoder).
lan_no_auth = tok is None and self.server.server_address[1] == 32400
if empty_token_param:
if p.startswith("/library/parts/"):
return self.send_json({"error": "Service Unavailable"}, 503)
if "/transcode/" in p:
return self.send_json({"error": "Bad Request"}, 400)
if tok != TOKEN and not lan_no_auth:
return self.send_json({"error": "unauthorized"}, 401) return self.send_json({"error": "unauthorized"}, 401)
if p == "/library/sections": if p == "/library/sections":
return self.send_json({"MediaContainer": {"Directory": [ return self.send_json({"MediaContainer": {"Directory": [
+42
View File
@@ -0,0 +1,42 @@
"""Rewrites Bouncy Castle calls that Android 2.x's Dalvik can't link.
Calling an Object method (toString/equals/hashCode/getClass) through an *interface* type compiles
to invokeinterface, which Dalvik before 4.0 can't resolve: NoSuchMethodError at runtime. Casting
the receiver to Object makes javac emit invokevirtual instead. tools/scan_dalvik.py checks nothing
was missed.
"""
import sys
FIXES = [
("asn1/ASN1InputStream.java", "asn1Obj.getClass()", "((Object) asn1Obj).getClass()"),
("asn1/ConstructedBitStream.java", "asn1Obj.getClass()", "((Object) asn1Obj).getClass()"),
("asn1/ConstructedOctetStream.java", "asn1Obj.getClass()", "((Object) asn1Obj).getClass()"),
("asn1/x509/AlgorithmIdentifier.java", "return pa.equals(pb);", "return ((Object) pa).equals(pb);"),
("asn1/x509/GeneralName.java", "buf.append(obj.toString());", "buf.append(((Object) obj).toString());"),
("crypto/CryptoServicesPermission.java", "return actions.toString();", "return ((Object) actions).toString();"),
("crypto/digests/SHA256Digest.java", "digest.getClass().getName()", "((Object) digest).getClass().getName()"),
("crypto/engines/AESEngine.java", "params.getClass().getName()", "((Object) params).getClass().getName()"),
("crypto/engines/ARIAEngine.java", "params.getClass().getName()", "((Object) params).getClass().getName()"),
("crypto/engines/DESedeEngine.java", "params.getClass().getName()", "((Object) params).getClass().getName()"),
("crypto/engines/SM4Engine.java", "params.getClass().getName()", "((Object) params).getClass().getName()"),
("crypto/modes/CCMBlockCipher.java", "params.getClass().getName()", "((Object) params).getClass().getName()"),
("math/ec/ECCurve.java", "getField().equals(other.getField())", "((Object) getField()).equals(other.getField())"),
("math/ec/ECCurve.java", "return getField().hashCode()", "return ((Object) getField()).hashCode()"),
("math/field/GenericPolynomialExtensionField.java", "return subfield.equals(other.subfield) && minimalPolynomial.equals(other.minimalPolynomial);",
"return ((Object) subfield).equals(other.subfield) && ((Object) minimalPolynomial).equals(other.minimalPolynomial);"),
("math/field/GenericPolynomialExtensionField.java", "return subfield.hashCode()", "return ((Object) subfield).hashCode()"),
("math/field/GenericPolynomialExtensionField.java", "rotateLeft(minimalPolynomial.hashCode(), 16)", "rotateLeft(((Object) minimalPolynomial).hashCode(), 16)"),
("tls/crypto/impl/AbstractTlsCrypto.java", "secret.getClass().getName()", "((Object) secret).getClass().getName()"),
("tls/crypto/impl/bc/BcTlsSecret.java", "secret.getClass().getName()", "((Object) secret).getClass().getName()"),
]
root = sys.argv[1].rstrip("/") + "/com/minamp/bc/"
for rel, old, new in FIXES:
p = root + rel
s = open(p).read()
if new in s:
continue
if old not in s:
raise SystemExit("pattern not found in %s: %s" % (rel, old))
open(p, "w").write(s.replace(old, new))
print("patched %d call sites" % len(FIXES))
+46
View File
@@ -0,0 +1,46 @@
# Find invokeinterface calls to java.lang.Object methods (toString/equals/hashCode/getClass) through an
# interface that doesn't declare them itself: Dalvik before 4.0 can't resolve those (NoSuchMethodError).
import os, re, subprocess, sys, zipfile
classes = sys.argv[1]; android_jar = sys.argv[2]
OBJ = {"toString:()Ljava/lang/String;", "equals:(Ljava/lang/Object;)Z", "hashCode:()I", "getClass:()Ljava/lang/Class;"}
z = zipfile.ZipFile(android_jar)
declared = {}
def declares(iface, m):
key = (iface, m)
if key in declared: return declared[key]
path = os.path.join(classes, iface + ".class")
src = path if os.path.exists(path) else None
args = ["javap", "-p", "-s", "-cp", classes if src else android_jar, iface.replace("/", ".")]
out = subprocess.run(args, capture_output=True, text=True).stdout
name, desc = m.split(":")
found = False
lines = out.splitlines()
for i, l in enumerate(lines):
if re.search(r"\b" + name + r"\(", l) and i + 1 < len(lines) and desc in lines[i + 1]:
found = True
# superinterfaces declaring it also count
if not found:
mm = re.search(r"interface \S+ extends (.+?) \{", out)
if mm:
for sup in mm.group(1).split(","):
sup = sup.strip().split("<")[0].replace(".", "/")
if sup and declares(sup, m): found = True
declared[key] = found
return found
hits = {}
files = [os.path.join(dp, f) for dp, _, fs in os.walk(classes) for f in fs if f.endswith(".class")]
for i in range(0, len(files), 200):
out = subprocess.run(["javap", "-c", "-p"] + files[i:i+200], capture_output=True, text=True).stdout
cur = None
for line in out.splitlines():
m = re.match(r"^(?:public |final |abstract |class |interface )*.*?(?:class|interface) (\S+)", line)
if m and not line.startswith(" "): cur = m.group(1)
mi = re.search(r"invokeinterface\s+#\d+,\s*\d+\s*// InterfaceMethod (\S+)\.(\w+):(\S+)", line)
if mi:
iface, name, desc = mi.group(1), mi.group(2), mi.group(3)
sig = name + ":" + desc
if sig in OBJ and not declares(iface, sig):
hits.setdefault((iface, sig), set()).add(cur)
for (iface, sig), users in sorted(hits.items()):
print(f"{iface}.{sig} <- {len(users)} class(es): " + ", ".join(sorted(users))[:300])
print("total patterns:", len(hits))
+1
View File
@@ -51,6 +51,7 @@ s = s.replace("return Long.divideUnsigned(x, y);", """if (y < 0) { return compar
s = s.replace("return Long.parseUnsignedLong(s);", "return new java.math.BigInteger(s).longValue();") s = s.replace("return Long.parseUnsignedLong(s);", "return new java.math.BigInteger(s).longValue();")
open(p, "w").write(s) open(p, "w").write(s)
PY PY
python3 "$PROJ/tools/patch_bc_dalvik.py" bcsrc
fi fi
echo "== signing key" echo "== signing key"