const BLOB: &[u8] = include_bytes!("../data/android-api.fwdb");
const MAGIC: u32 = 0x42445746; const REC: usize = 36; const SUPER_NONE: u32 = u32::MAX;
const _: () = assert!(
u32::from_le_bytes([BLOB[0], BLOB[1], BLOB[2], BLOB[3]]) == MAGIC
&& u32::from_le_bytes([BLOB[4], BLOB[5], BLOB[6], BLOB[7]]) == 1
&& BLOB.len() > 40,
"android-api.fwdb: bad magic/version — rerun scripts/extract-android-api.py"
);
pub const CF_INTERFACE: u32 = 1;
pub const CF_ABSTRACT: u32 = 2;
pub const CF_ENUM: u32 = 4;
pub const CF_UNKNOWN: u32 = 1 << 31;
pub const MF_PUBLIC: u32 = 1;
pub const MF_PROTECTED: u32 = 2;
pub const MF_PRIVATE: u32 = 4;
pub const MF_STATIC: u32 = 8;
pub const MF_ABSTRACT: u32 = 16;
pub const MF_FINAL: u32 = 32;
pub const MF_BRIDGE: u32 = 64;
pub const MF_SYNTHETIC: u32 = 128;
pub const MF_UNKNOWN: u32 = 1 << 31;
#[inline(always)]
fn u32_at(off: usize) -> u32 {
u32::from_le_bytes([BLOB[off], BLOB[off + 1], BLOB[off + 2], BLOB[off + 3]])
}
#[inline(always)]
fn off_pool() -> usize {
u32_at(16) as usize
}
#[inline(always)]
fn class_count() -> u32 {
u32_at(8)
}
#[inline(always)]
fn off_classes() -> usize {
u32_at(20) as usize
}
#[inline(always)]
fn off_impls() -> usize {
u32_at(24) as usize
}
#[inline(always)]
fn off_methods() -> usize {
u32_at(28) as usize
}
#[inline(always)]
fn off_fields() -> usize {
u32_at(32) as usize
}
#[inline]
fn s_at(off: u32) -> &'static str {
let start = off_pool() + off as usize;
let mut end = start;
while BLOB[end] != 0 {
end += 1;
}
std::str::from_utf8(&BLOB[start..end]).unwrap_or("")
}
#[inline]
fn cmp_name(off: u32, name: &[u8]) -> std::cmp::Ordering {
let start = off_pool() + off as usize;
let mut i = 0usize;
loop {
let b = BLOB[start + i];
match (b, name.get(i)) {
(0, None) => return std::cmp::Ordering::Equal,
(0, Some(_)) => return std::cmp::Ordering::Less, (_, None) => return std::cmp::Ordering::Greater,
(x, Some(&y)) => {
if x != y {
return x.cmp(&y);
}
i += 1;
}
}
}
}
pub fn find(name: &str) -> Option<u32> {
let n = class_count();
let base = off_classes();
let nb = name.as_bytes();
let (mut lo, mut hi) = (0u32, n);
while lo < hi {
let mid = lo + (hi - lo) / 2;
let name_off = u32_at(base + REC * mid as usize);
match cmp_name(name_off, nb) {
std::cmp::Ordering::Equal => return Some(mid),
std::cmp::Ordering::Less => lo = mid + 1,
std::cmp::Ordering::Greater => hi = mid,
}
}
None
}
#[inline]
fn rec(i: u32) -> [u32; 9] {
let b = off_classes() + REC * i as usize;
[
u32_at(b),
u32_at(b + 4),
u32_at(b + 8),
u32_at(b + 12),
u32_at(b + 16),
u32_at(b + 20),
u32_at(b + 24),
u32_at(b + 28),
u32_at(b + 32),
]
}
pub fn exists(name: &str) -> bool {
find(name).is_some()
}
pub fn class_flags(name: &str) -> u32 {
find(name).map(|i| rec(i)[2]).unwrap_or(0)
}
pub fn super_of(name: &str) -> Option<&'static str> {
let i = find(name)?;
let so = rec(i)[1];
if so == SUPER_NONE {
None
} else {
Some(s_at(so))
}
}
pub fn for_each_interface(name: &str, mut f: impl FnMut(&'static str)) {
let Some(i) = find(name) else { return };
let r = rec(i);
let (start, len) = (r[3] as usize, r[4] as usize);
let base = off_impls();
for k in 0..len {
f(s_at(u32_at(base + 4 * (start + k))));
}
}
pub fn for_each_method(name: &str, mut f: impl FnMut(&'static str, u32)) {
let Some(i) = find(name) else { return };
let r = rec(i);
let (start, len) = (r[5] as usize, r[6] as usize);
let base = off_methods();
for k in 0..len {
let b = base + 8 * (start + k);
f(s_at(u32_at(b)), u32_at(b + 4));
}
}
pub fn for_each_field(name: &str, mut f: impl FnMut(&'static str, u32)) {
let Some(i) = find(name) else { return };
let r = rec(i);
let (start, len) = (r[7] as usize, r[8] as usize);
let base = off_fields();
for k in 0..len {
let b = base + 8 * (start + k);
f(s_at(u32_at(b)), u32_at(b + 4));
}
}
pub fn is_subtype(sub: &str, sup: &str) -> bool {
if sub == sup {
return true;
}
if find(sub).is_none() {
return false;
}
if sup == "java/lang/Object" {
return true;
}
let mut stack: Vec<&str> = vec![sub];
let mut visited: jdc_core::FxHashSet<&str> = jdc_core::FxHashSet::default();
let mut hops = 0usize;
while let Some(c) = stack.pop() {
hops += 1;
if hops > 4096 || !visited.insert(c) {
continue;
}
if let Some(s) = super_of(c) {
if s == sup {
return true;
}
stack.push(s);
}
let mut hit = false;
for_each_interface(c, |i| {
if i == sup {
hit = true;
} else {
stack.push(i);
}
});
if hit {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blob_header_is_valid() {
assert_eq!(u32_at(0), MAGIC);
assert_eq!(u32_at(4), 1);
assert!(class_count() > 6000);
}
#[test]
fn hierarchy_queries() {
assert!(exists("android/view/View"));
assert_eq!(super_of("android/view/ViewGroup"), Some("android/view/View"));
assert!(is_subtype("android/widget/FrameLayout", "android/view/View"));
assert!(is_subtype("java/lang/String", "java/lang/Object"));
assert!(is_subtype("java/lang/String", "java/lang/CharSequence"));
assert!(!is_subtype("android/view/View", "java/lang/String"));
assert!(class_flags("android/view/ViewGroup") & CF_ABSTRACT != 0);
}
#[test]
fn removed_api_families_resolve_as_unknown() {
assert!(exists("org/apache/http/HttpResponse"));
assert!(class_flags("org/apache/http/HttpResponse") & CF_UNKNOWN != 0);
}
#[test]
fn member_queries() {
let mut has_x = false;
for_each_field("android/view/View", |n, f| {
if n == "X" && f & MF_STATIC != 0 {
has_x = true;
}
});
assert!(has_x, "View.X static field");
let mut ctors = 0;
for_each_method("java/lang/Throwable", |d, _| {
if d.starts_with("<init>") {
ctors += 1;
}
});
assert!(ctors >= 4);
let mut has_run = false;
for_each_method("java/lang/Runnable", |d, f| {
if d == "run()V" && f & MF_ABSTRACT != 0 && f & MF_STATIC == 0 {
has_run = true;
}
});
assert!(has_run, "Runnable.run abstract instance method");
assert!(class_flags("java/lang/Runnable") & CF_INTERFACE != 0);
}
}