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