Skip to main content

asc_engine/
classes.rs

1//! Class enumeration (`listclass`, MCP `listclass`).
2
3use crate::{Engine, EngineError, Result, load_entry, names};
4use asc_dex::container::logical_dex_iter;
5use asc_dex::{Dex, DexError, mutf8};
6use std::sync::Mutex;
7use std::sync::atomic::{AtomicUsize, Ordering};
8use std::time::Instant;
9
10/// A class definition and the (logical) DEX that defines it.
11#[derive(Debug, Clone, PartialEq, Eq)]
12#[non_exhaustive]
13pub struct ClassListing {
14    /// The class descriptor (`Lcom/poc/Main;`).
15    pub descriptor: String,
16    /// The (logical) DEX that defines it.
17    pub dex_name: String,
18}
19
20/// The classes one logical DEX defines, in `class_def` order.
21#[derive(Debug, Clone, Default, PartialEq, Eq)]
22#[non_exhaustive]
23pub struct DexClasses {
24    /// The (logical) DEX name.
25    pub dex_name: String,
26    /// Descriptors of the classes it defines (those matching the prefix).
27    pub descriptors: Vec<String>,
28    /// Why the DEX could not be enumerated at all (`descriptors` is empty).
29    pub error: Option<DexError>,
30    /// `class_def`s whose descriptor could not be read; they are skipped.
31    pub bad_class_defs: usize,
32    /// The first of those, as `(class_def index, error)`.
33    pub first_bad_class_def: Option<(u32, DexError)>,
34}
35
36impl DexClasses {
37    /// The first problem as an error, if any.
38    ///
39    /// # Errors
40    ///
41    /// [`EngineError::BadDex`] naming [`DexClasses::error`] or, failing that,
42    /// [`DexClasses::first_bad_class_def`].
43    pub fn check(&self) -> Result<()> {
44        if let Some(e) = &self.error {
45            return Err(EngineError::BadDex { dex: self.dex_name.clone(), reason: e.to_string() });
46        }
47        if let Some((i, e)) = &self.first_bad_class_def {
48            return Err(EngineError::BadDex { dex: self.dex_name.clone(), reason: format!("class_def {i}: {e}") });
49        }
50        Ok(())
51    }
52}
53
54/// The classes of one DEX entry: its logical DEXes, or why it could not be read.
55#[derive(Debug)]
56#[non_exhaustive]
57pub struct EntryClasses {
58    /// The APK entry name.
59    pub entry_name: String,
60    /// Its logical DEXes, or the error loading the entry.
61    pub result: Result<Vec<DexClasses>>,
62}
63
64/// A descriptor prefix ready to test raw descriptors against.
65enum Prefix {
66    /// ASCII: compared on the raw bytes, no decoding.
67    Bytes(Vec<u8>),
68    /// Non-ASCII: compared on the decoded name, so a non-canonical encoding of
69    /// the same characters (which the listing decodes like any other) matches.
70    Decoded(String),
71}
72
73impl Prefix {
74    fn new(prefix: &str) -> Prefix {
75        if prefix.is_ascii() { Prefix::Bytes(prefix.as_bytes().to_vec()) } else { Prefix::Decoded(prefix.to_string()) }
76    }
77
78    /// The decoded descriptor when `raw` starts with this prefix.
79    fn matches(&self, raw: &[u8]) -> Option<String> {
80        match self {
81            Prefix::Bytes(p) => raw.starts_with(p).then(|| mutf8::decode(raw)),
82            Prefix::Decoded(p) => Some(mutf8::decode(raw)).filter(|name| name.starts_with(p.as_str())),
83        }
84    }
85}
86
87/// Enumerate the classes of one logical DEX whose descriptor starts with
88/// `prefix` (a descriptor prefix, used as is; `None` keeps every class).
89/// Unreadable `class_def`s are counted and skipped.
90pub fn dex_classes(dex_name: &str, buf: &[u8], prefix: Option<&str>) -> DexClasses {
91    dex_classes_with(dex_name, buf, prefix.map(Prefix::new).as_ref(), false)
92}
93
94/// [`dex_classes`]; `strict` stops at the first unreadable `class_def`.
95fn dex_classes_with(dex_name: &str, buf: &[u8], prefix: Option<&Prefix>, strict: bool) -> DexClasses {
96    let mut out = DexClasses { dex_name: dex_name.to_string(), ..Default::default() };
97    let dex = match Dex::with_name(buf, dex_name) {
98        Ok(d) => d,
99        Err(e) => {
100            out.error = Some(e);
101            return out;
102        }
103    };
104    // A corrupt size must not drive billions of failing lookups. No
105    // class_defs at all is fine whatever the offset says.
106    let defs = dex.header().class_defs;
107    if defs.size == 0 {
108        return out;
109    }
110    if defs.off as u64 + defs.size as u64 * asc_dex::ClassDef::SIZE as u64 > buf.len() as u64 {
111        out.error = Some(DexError::BadRange { what: "class_defs", offset: defs.off as usize });
112        return out;
113    }
114    for (i, def) in dex.class_defs().enumerate() {
115        let bytes = def.and_then(|d| dex.type_bytes(d.class_idx));
116        match bytes {
117            Ok(b) => match prefix {
118                None => out.descriptors.push(mutf8::decode(b)),
119                Some(p) => out.descriptors.extend(p.matches(b)),
120            },
121            Err(e) => {
122                out.bad_class_defs += 1;
123                if out.first_bad_class_def.is_none() {
124                    out.first_bad_class_def = Some((i as u32, e));
125                }
126                if strict {
127                    break;
128                }
129            }
130        }
131    }
132    out
133}
134
135impl Engine {
136    /// Enumerate the classes of every root DEX entry whose descriptor starts
137    /// with `prefix` (a Dalvik descriptor prefix, used as is).
138    ///
139    /// Entries are read in parallel, largest first; the result follows the
140    /// central-directory order, and each entry's logical DEXes and classes
141    /// keep their file order. `progress(done, total)` runs after each entry.
142    /// Problems are reported per entry / DEX, never as a failure of the call;
143    /// only an unreadable APK fails it.
144    ///
145    /// # Errors
146    ///
147    /// [`EngineError::Apk`] when the APK cannot be opened.
148    pub fn scan_classes(&self, prefix: Option<&str>, progress: impl Fn(usize, usize) + Sync) -> Result<Vec<EntryClasses>> {
149        self.scan(prefix, false, progress)
150    }
151
152    /// [`Engine::scan_classes`]; `strict` stops each DEX at its first bad
153    /// `class_def` (the caller fails on it anyway).
154    fn scan(&self, prefix: Option<&str>, strict: bool, progress: impl Fn(usize, usize) + Sync) -> Result<Vec<EntryClasses>> {
155        let t0 = Instant::now();
156        let apk = self.open()?;
157        let entries = apk.dex_entries_in_order()?;
158        let key = self.apk_key(&apk);
159        let total = entries.len();
160        let mut order: Vec<usize> = (0..total).collect();
161        order.sort_by_key(|&i| std::cmp::Reverse(entries[i].comp_size));
162        let prefix = prefix.map(Prefix::new);
163        let slots: Vec<Mutex<Option<EntryClasses>>> = (0..total).map(|_| Mutex::new(None)).collect();
164        let next = AtomicUsize::new(0);
165        let done = AtomicUsize::new(0);
166        let workers = self.threads().min(total).max(1);
167        std::thread::scope(|s| {
168            for _ in 0..workers {
169                let (apk, entries, order, slots, key, prefix) = (&apk, &entries, &order, &slots, key.as_ref(), prefix.as_ref());
170                let (next, done, progress) = (&next, &done, &progress);
171                s.spawn(move || {
172                    while let Some(&i) = order.get(next.fetch_add(1, Ordering::Relaxed)) {
173                        let entry = &entries[i];
174                        let result = load_entry(apk, key, entry, self.cache.as_deref(), &self.stats, None).map(|(loaded, _)| {
175                            logical_dex_iter(&entry.name, loaded.bytes())
176                                .map(|(name, buf)| dex_classes_with(&name, &buf, prefix, strict))
177                                .collect()
178                        });
179                        *slots[i].lock().expect("entry slot mutex poisoned") =
180                            Some(EntryClasses { entry_name: entry.name.clone(), result });
181                        progress(done.fetch_add(1, Ordering::Relaxed) + 1, total);
182                    }
183                });
184            }
185        });
186        let out: Vec<EntryClasses> =
187            slots.into_iter().map(|s| s.into_inner().expect("entry slot mutex poisoned").expect("every entry is scanned")).collect();
188        if self.debug {
189            let mut count = 0;
190            for e in &out {
191                for d in e.result.iter().flatten() {
192                    count += d.descriptors.len();
193                    self.log(format!("[APK] '{}' class_count={}", d.dex_name, d.descriptors.len()));
194                }
195            }
196            self.log(format!("[APK] listclass total={} us count={count} workers={workers}", t0.elapsed().as_micros()));
197        }
198        Ok(out)
199    }
200
201    /// Every class of the APK, optionally restricted to a package / class
202    /// prefix (`com.poc`, `com/poc` or `Lcom/poc`), in DEX order. Strict: the
203    /// first unreadable entry, unparsable DEX or bad `class_def` fails the call.
204    ///
205    /// # Errors
206    ///
207    /// [`EngineError::Query`] for an empty prefix, the error of the first
208    /// entry that cannot be read, and [`EngineError::BadDex`] for the first
209    /// bad DEX or `class_def` ([`DexClasses::check`]).
210    pub fn list_classes(&self, prefix: Option<&str>) -> Result<Vec<ClassListing>> {
211        let prefix = prefix.map(names::class_prefix).transpose()?;
212        let mut out = Vec::new();
213        for entry in self.scan(prefix.as_deref(), true, |_, _| {})? {
214            for dex in entry.result? {
215                dex.check()?;
216                let dex_name = dex.dex_name;
217                out.extend(dex.descriptors.into_iter().map(|descriptor| ClassListing { descriptor, dex_name: dex_name.clone() }));
218            }
219        }
220        Ok(out)
221    }
222}
223
224/// The nested class of `outer` in `dex` whose `dalvik.annotation.InnerClass`
225/// name is `name` (or what `name` stands for, see
226/// [`names::denormalize_identifier`]): source shows an obfuscated
227/// `Outer$a` by that name. `None` when there is none or the DEX cannot be read.
228pub(crate) fn inner_class_named(dex: &[u8], outer: &str, name: &str) -> Option<String> {
229    let prefix = format!("{}$", outer.strip_suffix(';')?);
230    let dex = Dex::parse(dex).ok()?;
231    let mut wanted = names::denormalize_identifier(name);
232    wanted.push(name.to_string());
233    (0..dex.class_def_count()).find_map(|i| {
234        let def = dex.class_def(i).ok()?;
235        let descriptor = dex.type_descriptor(def.class_idx).ok()?;
236        if !descriptor.starts_with(&prefix) {
237            return None;
238        }
239        inner_class_annotation_name(&dex, def.annotations_off).filter(|n| wanted.contains(n)).map(|_| descriptor)
240    })
241}
242
243/// The `name` of the `Ldalvik/annotation/InnerClass;` annotation of the
244/// class whose annotations directory is at `annotations_off`.
245fn inner_class_annotation_name(dex: &Dex<'_>, annotations_off: u32) -> Option<String> {
246    use asc_dex::encoded::{EncodedValue, parse_annotation_item};
247    if annotations_off == 0 {
248        return None;
249    }
250    let buf = dex.buf();
251    let dir = asc_dex::annotations::AnnotationsDirectory::parse(buf, annotations_off as usize).ok()?;
252    if dir.class_annotations_off == 0 {
253        return None;
254    }
255    for item_off in asc_dex::annotations::parse_annotation_set(buf, dir.class_annotations_off as usize).ok()? {
256        let (item, _) = parse_annotation_item(buf, item_off as usize).ok()?;
257        if dex.type_bytes(item.annotation.type_idx).ok()? != b"Ldalvik/annotation/InnerClass;" {
258            continue;
259        }
260        return item.annotation.elements.iter().find_map(|(name_idx, value)| match value {
261            EncodedValue::String(s) if dex.string_bytes(*name_idx).ok()? == b"name" => dex.string(*s).ok(),
262            _ => None,
263        });
264    }
265    None
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn dex_classes_filters_by_raw_prefix_and_reports_bad_defs() {
274        let dex = asc_fixture::make_dex();
275        let all = dex_classes("classes.dex", &dex, None);
276        assert_eq!(all.descriptors, ["Lexample/Test;"]);
277        assert!(all.check().is_ok());
278        assert!(dex_classes("classes.dex", &dex, Some("Lexample/")).descriptors.len() == 1);
279        assert!(dex_classes("classes.dex", &dex, Some("Lother")).descriptors.is_empty());
280
281        let mut bad = dex.clone();
282        let off = u32::from_le_bytes(bad[0x64..0x68].try_into().unwrap()) as usize;
283        bad[off..off + 4].copy_from_slice(&u32::MAX.to_le_bytes());
284        let r = dex_classes("classes.dex", &bad, None);
285        assert!(r.descriptors.is_empty());
286        assert_eq!(r.bad_class_defs, 1);
287        let msg = r.check().unwrap_err().to_string();
288        assert!(msg.starts_with("classes.dex: class_def 0: type index"), "{msg}");
289
290        let mut huge = dex.clone();
291        huge[0x60..0x64].copy_from_slice(&u32::MAX.to_le_bytes());
292        let r = dex_classes("classes.dex", &huge, None);
293        assert!(matches!(r.error, Some(DexError::BadRange { what: "class_defs", .. })));
294
295        // No class_defs: the offset is not looked at.
296        let mut none = dex.clone();
297        none[0x60..0x68].copy_from_slice(&[0, 0, 0, 0, 0x00, 0xff, 0xff, 0xff]);
298        let r = dex_classes("classes.dex", &none, None);
299        assert_eq!(r.error, None);
300        assert!(r.descriptors.is_empty() && r.check().is_ok());
301
302        let r = dex_classes("x.dex", b"not a dex", None);
303        assert!(r.error.is_some() && r.check().is_err());
304
305        let only = asc_fixture::make_class_only_dex(&["Lb/B;", "La/A;"], 0, false);
306        assert_eq!(
307            dex_classes("c.dex", &only, None),
308            DexClasses { dex_name: "c.dex".into(), descriptors: vec!["Lb/B;".into(), "La/A;".into()], ..Default::default() }
309        );
310    }
311
312    #[test]
313    fn strict_stops_at_the_first_bad_class_def() {
314        let only = asc_fixture::make_class_only_dex(&["La/A;", "Lb/B;", "Lc/C;"], 0, false);
315        let off = u32::from_le_bytes(only[0x64..0x68].try_into().unwrap()) as usize;
316        let mut bad = only.clone();
317        for i in 0..2 {
318            bad[off + i * 32..off + i * 32 + 4].copy_from_slice(&u32::MAX.to_le_bytes());
319        }
320        let lenient = dex_classes("c.dex", &bad, None);
321        assert_eq!((lenient.bad_class_defs, lenient.descriptors.as_slice()), (2, &["Lc/C;".to_string()][..]));
322        let strict = dex_classes_with("c.dex", &bad, None, true);
323        assert_eq!((strict.bad_class_defs, strict.descriptors.len()), (1, 0));
324        assert_eq!(strict.check().unwrap_err().to_string(), lenient.check().unwrap_err().to_string());
325    }
326
327    #[test]
328    fn non_ascii_prefix_matches_decoded_names() {
329        let name = "L\u{1f600}/A;";
330        let canonical = asc_fixture::make_class_only_dex(&[name], 0, false);
331        // The same class with its descriptor stored as 4-byte UTF-8 (same
332        // byte length as the placeholder), which the listing decodes alike.
333        let mut four = asc_fixture::make_class_only_dex(&["Lxxxx/A;"], 0, false);
334        let at = four.windows(8).position(|w| w == b"Lxxxx/A;").unwrap();
335        four[at + 1..at + 5].copy_from_slice("\u{1f600}".as_bytes());
336        for data in [&canonical, &four] {
337            assert_eq!(dex_classes("c.dex", data, None).descriptors, [name]);
338            assert_eq!(dex_classes("c.dex", data, Some("L\u{1f600}")).descriptors, [name]);
339            assert_eq!(dex_classes("c.dex", data, Some("L\u{1f600}/A")).descriptors, [name]);
340            assert!(dex_classes("c.dex", data, Some("L\u{1f601}")).descriptors.is_empty());
341        }
342    }
343}