Skip to main content

ddc_dec/
fwdb.rs

1//! Embedded Android framework API database.
2//!
3//! Built offline by `scripts/extract-android-api.py` from the SDK's
4//! `android.jar` (class files: names, descriptors, supers, interfaces,
5//! ACCESS FLAGS) merged with `data/api-versions.xml` (classes REMOVED
6//! from the modern jar but still referenced by old APKs — the
7//! org.apache.http family — flagged UNKNOWN so flag-sensitive consumers
8//! skip them while subtype/exists queries resolve).
9//!
10//! Runtime cost is zero-parse by design: the blob is `include_bytes!`'d
11//! and queried in place — binary search over the class table, NUL-scan
12//! string reads, section slices for members. No decompression, no
13//! deserialization, no startup work (the rejected alternatives: runtime
14//! zlib+JSON ≈ 300ms first query; generated Rust source ≈ multi-MB of
15//! literals, slower builds, bigger binary than the raw pool).
16//!
17//! Consumers: DexPool::is_subtype's framework-boundary fallback
18//! (String <: CharSequence <: Object chains the dex pool cannot see),
19//! the multi-catch / Throwable-bridge / interface-stub synthesizers
20//! (replacing hand-written tables), obscured-render field-shadow
21//! detection (View.X/Y/Z and friends), and the framework overload
22//! enumeration behind the descriptor-exact ambiguity pin.
23
24const BLOB: &[u8] = include_bytes!("../data/android-api.fwdb");
25
26const MAGIC: u32 = 0x42445746; // 'FWDB'
27const REC: usize = 36; // 9 × u32 per class record
28const SUPER_NONE: u32 = u32::MAX;
29
30// Compile-time blob validation (const panic = build failure): wrong or
31// truncated data can never ship.
32const _: () = assert!(
33    u32::from_le_bytes([BLOB[0], BLOB[1], BLOB[2], BLOB[3]]) == MAGIC
34        && u32::from_le_bytes([BLOB[4], BLOB[5], BLOB[6], BLOB[7]]) == 1
35        && BLOB.len() > 40,
36    "android-api.fwdb: bad magic/version — rerun scripts/extract-android-api.py"
37);
38
39// Class flag bits.
40pub const CF_INTERFACE: u32 = 1;
41pub const CF_ABSTRACT: u32 = 2;
42pub const CF_ENUM: u32 = 4;
43pub const CF_UNKNOWN: u32 = 1 << 31; // xml-only (removed API): no flag info
44
45// Member flag bits.
46pub const MF_PUBLIC: u32 = 1;
47pub const MF_PROTECTED: u32 = 2;
48pub const MF_PRIVATE: u32 = 4;
49pub const MF_STATIC: u32 = 8;
50pub const MF_ABSTRACT: u32 = 16;
51pub const MF_FINAL: u32 = 32;
52pub const MF_BRIDGE: u32 = 64;
53pub const MF_SYNTHETIC: u32 = 128;
54pub const MF_UNKNOWN: u32 = 1 << 31;
55
56#[inline(always)]
57fn u32_at(off: usize) -> u32 {
58    u32::from_le_bytes([BLOB[off], BLOB[off + 1], BLOB[off + 2], BLOB[off + 3]])
59}
60
61#[inline(always)]
62fn off_pool() -> usize {
63    u32_at(16) as usize
64}
65
66#[inline(always)]
67fn class_count() -> u32 {
68    u32_at(8)
69}
70
71#[inline(always)]
72fn off_classes() -> usize {
73    u32_at(20) as usize
74}
75
76#[inline(always)]
77fn off_impls() -> usize {
78    u32_at(24) as usize
79}
80
81#[inline(always)]
82fn off_methods() -> usize {
83    u32_at(28) as usize
84}
85
86#[inline(always)]
87fn off_fields() -> usize {
88    u32_at(32) as usize
89}
90
91/// String-pool read: NUL-terminated slice (the builder guarantees ASCII-
92/// compatible UTF-8; from_utf8 cannot fail on the generated data but the
93/// safe call costs one linear pass we already paid via the NUL scan).
94#[inline]
95fn s_at(off: u32) -> &'static str {
96    let start = off_pool() + off as usize;
97    let mut end = start;
98    while BLOB[end] != 0 {
99        end += 1;
100    }
101    std::str::from_utf8(&BLOB[start..end]).unwrap_or("")
102}
103
104/// Byte-wise compare of a pool string against `name` (no &str materialized).
105#[inline]
106fn cmp_name(off: u32, name: &[u8]) -> std::cmp::Ordering {
107    let start = off_pool() + off as usize;
108    let mut i = 0usize;
109    loop {
110        let b = BLOB[start + i];
111        match (b, name.get(i)) {
112            (0, None) => return std::cmp::Ordering::Equal,
113            (0, Some(_)) => return std::cmp::Ordering::Less, // pool str is a prefix
114            (_, None) => return std::cmp::Ordering::Greater,
115            (x, Some(&y)) => {
116                if x != y {
117                    return x.cmp(&y);
118                }
119                i += 1;
120            }
121        }
122    }
123}
124
125/// Binary search: internal name -> class record index.
126pub fn find(name: &str) -> Option<u32> {
127    let n = class_count();
128    let base = off_classes();
129    let nb = name.as_bytes();
130    let (mut lo, mut hi) = (0u32, n);
131    while lo < hi {
132        let mid = lo + (hi - lo) / 2;
133        let name_off = u32_at(base + REC * mid as usize);
134        match cmp_name(name_off, nb) {
135            std::cmp::Ordering::Equal => return Some(mid),
136            std::cmp::Ordering::Less => lo = mid + 1,
137            std::cmp::Ordering::Greater => hi = mid,
138        }
139    }
140    None
141}
142
143#[inline]
144fn rec(i: u32) -> [u32; 9] {
145    let b = off_classes() + REC * i as usize;
146    [
147        u32_at(b),
148        u32_at(b + 4),
149        u32_at(b + 8),
150        u32_at(b + 12),
151        u32_at(b + 16),
152        u32_at(b + 20),
153        u32_at(b + 24),
154        u32_at(b + 28),
155        u32_at(b + 32),
156    ]
157}
158
159pub fn exists(name: &str) -> bool {
160    find(name).is_some()
161}
162
163pub fn class_flags(name: &str) -> u32 {
164    find(name).map(|i| rec(i)[2]).unwrap_or(0)
165}
166
167pub fn super_of(name: &str) -> Option<&'static str> {
168    let i = find(name)?;
169    let so = rec(i)[1];
170    if so == SUPER_NONE {
171        None
172    } else {
173        Some(s_at(so))
174    }
175}
176
177/// Iterate the direct interfaces of `name`.
178pub fn for_each_interface(name: &str, mut f: impl FnMut(&'static str)) {
179    let Some(i) = find(name) else { return };
180    let r = rec(i);
181    let (start, len) = (r[3] as usize, r[4] as usize);
182    let base = off_impls();
183    for k in 0..len {
184        f(s_at(u32_at(base + 4 * (start + k))));
185    }
186}
187
188/// Iterate `(descriptor, flags)` of every method of `name`. The descriptor
189/// is the combined `"name(args)ret"` form.
190pub fn for_each_method(name: &str, mut f: impl FnMut(&'static str, u32)) {
191    let Some(i) = find(name) else { return };
192    let r = rec(i);
193    let (start, len) = (r[5] as usize, r[6] as usize);
194    let base = off_methods();
195    for k in 0..len {
196        let b = base + 8 * (start + k);
197        f(s_at(u32_at(b)), u32_at(b + 4));
198    }
199}
200
201/// Iterate `(field_name, flags)` of every field of `name`.
202pub fn for_each_field(name: &str, mut f: impl FnMut(&'static str, u32)) {
203    let Some(i) = find(name) else { return };
204    let r = rec(i);
205    let (start, len) = (r[7] as usize, r[8] as usize);
206    let base = off_fields();
207    for k in 0..len {
208        let b = base + 8 * (start + k);
209        f(s_at(u32_at(b)), u32_at(b + 4));
210    }
211}
212
213/// Framework-world subtype check: walks supers and interfaces inside the
214/// embedded DB. `sub == sup` is true by contract (callers may rely on it).
215pub fn is_subtype(sub: &str, sup: &str) -> bool {
216    if sub == sup {
217        return true;
218    }
219    if find(sub).is_none() {
220        return false;
221    }
222    // The builder strips java/lang/Object super edges (space); every
223    // class in the DB is Object-assignable (interfaces included).
224    if sup == "java/lang/Object" {
225        return true;
226    }
227    let mut stack: Vec<&str> = vec![sub];
228    let mut visited: jdc_core::FxHashSet<&str> = jdc_core::FxHashSet::default();
229    let mut hops = 0usize;
230    while let Some(c) = stack.pop() {
231        hops += 1;
232        if hops > 4096 || !visited.insert(c) {
233            continue;
234        }
235        if let Some(s) = super_of(c) {
236            if s == sup {
237                return true;
238            }
239            stack.push(s);
240        }
241        let mut hit = false;
242        for_each_interface(c, |i| {
243            if i == sup {
244                hit = true;
245            } else {
246                stack.push(i);
247            }
248        });
249        if hit {
250            return true;
251        }
252    }
253    false
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn blob_header_is_valid() {
262        assert_eq!(u32_at(0), MAGIC);
263        assert_eq!(u32_at(4), 1);
264        assert!(class_count() > 6000);
265    }
266
267    #[test]
268    fn hierarchy_queries() {
269        assert!(exists("android/view/View"));
270        assert_eq!(super_of("android/view/ViewGroup"), Some("android/view/View"));
271        assert!(is_subtype("android/widget/FrameLayout", "android/view/View"));
272        assert!(is_subtype("java/lang/String", "java/lang/Object"));
273        assert!(is_subtype("java/lang/String", "java/lang/CharSequence"));
274        assert!(!is_subtype("android/view/View", "java/lang/String"));
275        assert!(class_flags("android/view/ViewGroup") & CF_ABSTRACT != 0);
276    }
277
278    #[test]
279    fn removed_api_families_resolve_as_unknown() {
280        assert!(exists("org/apache/http/HttpResponse"));
281        assert!(class_flags("org/apache/http/HttpResponse") & CF_UNKNOWN != 0);
282    }
283
284    #[test]
285    fn member_queries() {
286        let mut has_x = false;
287        for_each_field("android/view/View", |n, f| {
288            if n == "X" && f & MF_STATIC != 0 {
289                has_x = true;
290            }
291        });
292        assert!(has_x, "View.X static field");
293        let mut ctors = 0;
294        for_each_method("java/lang/Throwable", |d, _| {
295            if d.starts_with("<init>") {
296                ctors += 1;
297            }
298        });
299        assert!(ctors >= 4);
300        let mut has_run = false;
301        for_each_method("java/lang/Runnable", |d, f| {
302            if d == "run()V" && f & MF_ABSTRACT != 0 && f & MF_STATIC == 0 {
303                has_run = true;
304            }
305        });
306        assert!(has_run, "Runnable.run abstract instance method");
307        assert!(class_flags("java/lang/Runnable") & CF_INTERFACE != 0);
308    }
309}