1use crate::resources::{FileIdent, file_ident};
14use crate::{DexView, Engine, EngineError, Job, NOT_CANCELLED, Result, WriteJob, cancelled, names};
15use asc_cache::{LockExt, Tier};
16use asc_dex::Dex;
17use asc_findrefs::ClassRelation;
18use std::collections::{HashMap, HashSet, VecDeque};
19use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
20use std::sync::{Arc, Mutex};
21use std::time::Instant;
22
23mod packed;
24
25use packed::{Builder, Packed};
26
27const NO_INDEX: u32 = u32::MAX;
29
30pub(crate) const GRAPH_KIND: &str = "classgraph";
32pub(crate) const GRAPH_NAME: &str = "apk";
34pub(crate) const GRAPH_FORMAT: u32 = 1;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Lineage {
40 Subclasses,
42 Supers,
44}
45
46impl Lineage {
47 pub fn as_str(self) -> &'static str {
49 match self {
50 Lineage::Subclasses => "subclasses",
51 Lineage::Supers => "supers",
52 }
53 }
54
55 fn word(self, rel: ClassRelation) -> &'static str {
57 match (self, rel) {
58 (Lineage::Subclasses, ClassRelation::Extends) => "subclass",
59 (Lineage::Subclasses, ClassRelation::Implements) => "implementer",
60 (Lineage::Supers, rel) => rel.verb(),
61 }
62 }
63}
64
65#[derive(Debug, Clone)]
67#[non_exhaustive]
68pub struct HierarchyQuery {
69 pub class: String,
71 pub direction: Lineage,
73 pub depth: usize,
75 pub max_nodes: usize,
77 pub method: Option<String>,
80}
81
82impl HierarchyQuery {
83 #[must_use]
85 pub fn depth(self, depth: usize) -> Self {
86 HierarchyQuery { depth, ..self }
87 }
88
89 #[must_use]
91 pub fn max_nodes(self, max_nodes: usize) -> Self {
92 HierarchyQuery { max_nodes, ..self }
93 }
94
95 #[must_use]
97 pub fn method(self, method: Option<String>) -> Self {
98 HierarchyQuery { method, ..self }
99 }
100}
101
102impl HierarchyQuery {
103 pub fn new(class: impl Into<String>, direction: Lineage) -> HierarchyQuery {
106 HierarchyQuery { class: class.into(), direction, depth: 16, max_nodes: 2000, method: None }
107 }
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
112#[non_exhaustive]
113pub struct HierarchyRow {
114 pub depth: usize,
116 pub parent: Option<usize>,
118 pub class: String,
120 pub relation: Option<ClassRelation>,
123 pub dex: Option<String>,
125 pub repeated: bool,
127 pub defines: Vec<String>,
129}
130
131#[derive(Debug, Clone)]
133#[non_exhaustive]
134pub struct HierarchyTree {
135 pub direction: Lineage,
137 pub depth: usize,
139 pub rows: Vec<HierarchyRow>,
141 pub classes: usize,
143 pub truncated: bool,
145 pub method: Option<String>,
147 pub errors: Vec<String>,
149}
150
151impl HierarchyTree {
152 pub fn lines(&self) -> Vec<String> {
157 self.rows
158 .iter()
159 .map(|r| {
160 let mut s = " ".repeat(r.depth);
161 match r.relation {
162 Some(rel) if r.depth > 0 => s.push_str(&format!("{} {} {}", r.depth, self.direction.word(rel), r.class)),
163 _ => s.push_str(&format!("0 {}", r.class)),
164 }
165 match &r.dex {
166 Some(d) => s.push_str(&format!(" {d}")),
167 None => s.push_str(" [not in APK]"),
168 }
169 for d in &r.defines {
170 s.push_str(&format!(" [defines {d}]"));
171 }
172 if r.repeated {
173 s.push_str(" (repeated)");
174 }
175 s
176 })
177 .collect()
178 }
179
180 pub fn definition_lines(&self) -> Vec<String> {
183 let word = match self.direction {
184 Lineage::Subclasses => "override",
185 Lineage::Supers => "inherited",
186 };
187 let mut seen = HashSet::new();
188 let mut out = Vec::new();
189 for r in self.rows.iter().filter(|r| r.depth > 0 && seen.insert(&r.class)) {
190 for d in &r.defines {
191 out.push(format!("{word} {}->{d} {}", r.class, r.dex.as_deref().unwrap_or("-")));
192 }
193 }
194 out
195 }
196
197 pub fn footer(&self) -> String {
199 let mut s =
200 format!("# {} rows, {} distinct classes ({}, depth {})", self.rows.len(), self.classes, self.direction.as_str(), self.depth);
201 if let Some(m) = &self.method {
202 let n = self.definition_lines().len();
203 s.push_str(&format!("; {n} define {m}"));
204 }
205 if self.truncated {
206 s.push_str("; truncated (raise max_nodes or lower depth)");
207 }
208 s
209 }
210}
211
212pub(crate) struct ClassGraph(Packed);
215
216struct DefinedClass {
218 class: String,
219 superclass: Option<String>,
220 interfaces: Vec<String>,
221 def_idx: u32,
223 annotations: Vec<String>,
225}
226
227fn dex_classes(v: &DexView) -> Result<Vec<DefinedClass>> {
228 let dex = Dex::with_name(&v.data, &v.name)?;
229 let mut out = Vec::with_capacity(dex.class_def_count() as usize);
230 for i in 0..dex.class_def_count() {
231 let def = dex.class_def(i)?;
232 let sup = (def.superclass_idx != NO_INDEX).then(|| dex.type_descriptor(def.superclass_idx)).transpose()?;
233 let ifaces = if def.interfaces_off == 0 {
234 Vec::new()
235 } else {
236 dex.type_list(def.interfaces_off)?.into_iter().map(|t| dex.type_descriptor(t as u32)).collect::<std::result::Result<_, _>>()?
237 };
238 let annotations = class_annotations(&dex, def.annotations_off)?;
239 out.push(DefinedClass { class: dex.type_descriptor(def.class_idx)?, superclass: sup, interfaces: ifaces, def_idx: i, annotations });
240 }
241 Ok(out)
242}
243
244fn class_annotations(dex: &Dex<'_>, off: u32) -> Result<Vec<String>> {
248 if off == 0 {
249 return Ok(Vec::new());
250 }
251 let buf = dex.buf();
252 let set_off = buf
253 .get(off as usize..off as usize + 4)
254 .map(|b| u32::from_le_bytes(b.try_into().expect("4 bytes")))
255 .ok_or(asc_dex::DexError::BadRange { what: "annotations_directory_item", offset: off as usize })?;
256 if set_off == 0 {
257 return Ok(Vec::new());
258 }
259 let mut out = Vec::new();
260 for item_off in asc_dex::annotations::parse_annotation_set(buf, set_off as usize)? {
261 let (item, _) = asc_dex::encoded::parse_annotation_item(buf, item_off as usize)?;
262 out.push(dex.type_descriptor(item.annotation.type_idx)?);
263 }
264 Ok(out)
265}
266
267impl ClassGraph {
268 pub(crate) fn heap_bytes(&self) -> usize {
270 self.0.heap_bytes()
271 }
272
273 fn build(views: &[DexView], threads: usize, errors: &mut Vec<String>, cancel: &AtomicBool) -> Option<ClassGraph> {
275 let next = AtomicUsize::new(0);
276 let parsed: Mutex<Vec<(usize, Vec<DefinedClass>)>> = Mutex::new(Vec::new());
277 let errs = Mutex::new(Vec::new());
278 std::thread::scope(|s| {
279 for _ in 0..threads.min(views.len()).max(1) {
280 s.spawn(|| {
281 loop {
282 if cancel.load(Ordering::Relaxed) {
283 break;
284 }
285 let i = next.fetch_add(1, Ordering::Relaxed);
286 let Some(v) = views.get(i) else { break };
287 match dex_classes(v) {
288 Ok(c) => parsed.lock().expect("parsed classes mutex poisoned").push((i, c)),
289 Err(e) => errs.lock().expect("errors mutex poisoned").push(format!("{}: {e}", v.name)),
290 }
291 }
292 });
293 }
294 });
295 if cancel.load(Ordering::Relaxed) {
296 return None;
297 }
298 errors.extend(errs.into_inner().expect("errors mutex poisoned"));
299 let mut parsed = parsed.into_inner().expect("parsed classes mutex poisoned");
300 parsed.sort_by_key(|(i, _)| *i);
302 let mut b = Builder::default();
303 for (view, classes) in parsed {
304 for DefinedClass { class, superclass, interfaces, def_idx, annotations } in classes {
305 let c = b.intern(&class);
306 if !b.define(c, view as u32, def_idx) {
307 continue; }
309 for a in annotations {
310 b.annotate(c, a);
311 }
312 let parents =
313 superclass.iter().map(|s| (s, ClassRelation::Extends)).chain(interfaces.iter().map(|i| (i, ClassRelation::Implements)));
314 for (p, rel) in parents {
315 let p = b.intern(p);
316 b.edge(c, p, rel);
317 }
318 }
319 }
320 Some(ClassGraph(b.finish(views.iter().map(|v| v.name.as_str()))))
321 }
322
323 pub(crate) fn bytes(&self) -> &[u8] {
325 self.0.bytes()
326 }
327
328 fn from_mapped(d: asc_cache::Derived) -> Option<ClassGraph> {
331 Packed::from_mapped(d).map(ClassGraph)
332 }
333
334 fn id(&self, name: &str) -> Option<u32> {
335 self.0.id(name)
336 }
337
338 fn name(&self, id: u32) -> &str {
339 self.0.name(id)
340 }
341
342 fn neighbours(&self, id: u32, dir: Lineage) -> impl Iterator<Item = (u32, ClassRelation)> + '_ {
344 self.0.edges_of(id, dir == Lineage::Subclasses).map(move |(c, p, rel)| match dir {
345 Lineage::Supers => (p, rel),
346 Lineage::Subclasses => (c, rel),
347 })
348 }
349
350 pub(crate) fn reachable(&self, class: &str, dir: Lineage, limit: usize) -> Vec<String> {
353 let Some(root) = self.id(class) else { return Vec::new() };
354 let mut seen = HashSet::from([root]);
355 let mut queue = VecDeque::from([root]);
356 let mut out = Vec::new();
357 while let Some(c) = queue.pop_front() {
358 for (n, _) in self.neighbours(c, dir) {
359 if out.len() >= limit {
360 return out;
361 }
362 if seen.insert(n) {
363 out.push(self.name(n).to_string());
364 queue.push_back(n);
365 }
366 }
367 }
368 out
369 }
370
371 fn dex_of(&self, id: u32) -> Option<&str> {
372 let (view, _) = self.0.defined(id);
373 (view != 0).then(|| self.0.dex_name(view as usize - 1))
374 }
375
376 pub(crate) fn defined_classes_matching(&self, re: ®ex::bytes::Regex) -> Vec<String> {
379 (0..self.0.classes() as u32)
380 .filter(|&id| self.0.defined(id).0 != 0)
381 .map(|id| self.name(id))
382 .filter(|name| re.is_match(name.as_bytes()))
383 .map(String::from)
384 .collect()
385 }
386
387 pub(crate) fn classes_annotated(&self, annotation: &str) -> Vec<String> {
390 let mut out: Vec<String> = self.0.annotated(annotation).map(|id| self.name(id).to_string()).collect();
391 out.sort_unstable();
392 out
393 }
394
395 pub(crate) fn defines(&self, views: &[DexView], class: &str, name: &str, proto: Option<&str>) -> Result<Vec<String>> {
398 let Some(id) = self.id(class) else { return Ok(Vec::new()) };
399 let (view, def_idx) = self.0.defined(id);
400 if view == 0 {
401 return Ok(Vec::new());
402 }
403 let v = &views[view as usize - 1];
404 let dex = Dex::with_name(&v.data, &v.name)?;
405 let def = dex.class_def(def_idx)?;
406 let data = dex.class_data(def.class_data_off)?;
407 let mut out = Vec::new();
408 for m in data.direct_methods.iter().chain(&data.virtual_methods) {
409 let mid = dex.method_id(m.method_idx)?;
410 if dex.string_bytes(mid.name_idx)? != name.as_bytes() {
411 continue;
412 }
413 let p = asc_smali::proto_descriptor(&dex, mid.proto_idx as u32)?;
414 if proto.is_none_or(|want| names::proto_matches(&p, want)) {
415 out.push(format!("{name}{p}"));
416 }
417 }
418 Ok(out)
419 }
420
421 pub(crate) fn definition(&self, class: &str) -> Option<(usize, u32)> {
424 let (view, def_idx) = self.0.defined(self.id(class)?);
425 (view != 0).then(|| (view as usize - 1, def_idx))
426 }
427
428 pub(crate) fn defining(&self, class: &str, max_classes: usize, mut defines: impl FnMut(&str) -> Result<bool>) -> Result<Defining> {
432 let Some(root) = self.id(class) else { return Ok(Defining { count: 0, complete: true }) };
433 let mut seen = HashSet::from([root]);
434 let mut queue = VecDeque::from([root]);
435 let mut count = usize::from(defines(class)?);
436 while let Some(c) = queue.pop_front() {
437 for (n, _) in self.neighbours(c, Lineage::Subclasses) {
438 if seen.len() >= max_classes {
439 return Ok(Defining { count, complete: false });
440 }
441 if seen.insert(n) {
442 count += usize::from(defines(self.name(n))?);
443 queue.push_back(n);
444 }
445 }
446 }
447 Ok(Defining { count, complete: true })
448 }
449
450 pub(crate) fn inheritors(&self, class: &str, limit: usize, mut defines: impl FnMut(&str) -> Result<bool>) -> Result<Vec<String>> {
454 let Some(root) = self.id(class) else { return Ok(Vec::new()) };
455 let mut seen = HashSet::from([root]);
456 let mut queue = VecDeque::from([root]);
457 let mut out = vec![class.to_string()];
458 while let Some(c) = queue.pop_front() {
459 for (n, _) in self.neighbours(c, Lineage::Subclasses) {
460 if out.len() >= limit {
461 return Ok(out);
462 }
463 if seen.insert(n) && !defines(self.name(n))? {
464 out.push(self.name(n).to_string());
465 queue.push_back(n);
466 }
467 }
468 }
469 Ok(out)
470 }
471}
472
473#[derive(Debug, Clone, Copy)]
476pub(crate) struct Defining {
477 pub count: usize,
478 pub complete: bool,
480}
481
482pub(crate) type CachedGraph = (Option<FileIdent>, Arc<ClassGraph>);
484
485pub(crate) fn split_method(method: &str) -> Result<(String, Option<String>)> {
489 match method.find('(') {
490 Some(_) => match asc_smali::normalize_method_signature(method) {
491 Ok(sig) => {
492 let p = sig.find('(').expect("normalised signature has a paren");
493 Ok((sig[..p].to_string(), Some(sig[p..].to_string())))
494 }
495 Err(e) => names::partial_proto(method).map(|(n, proto)| (n, Some(proto))).ok_or_else(|| e.into()),
496 },
497 None => Ok((method.to_string(), None)),
498 }
499}
500
501impl Engine {
502 pub(crate) fn class_graph(&self, views: &[DexView], errors: &mut Vec<String>, cancel: &AtomicBool) -> Result<Arc<ClassGraph>> {
507 let apk = self.open()?;
508 let ident = file_ident(&apk);
509 let kept = || {
510 let slot = self.class_graph.lock_or_default();
511 slot.as_ref().filter(|(id, _)| ident.is_some() && *id == ident).map(|(_, g)| Arc::clone(g))
512 };
513 if let Some(g) = kept() {
514 return Ok(g);
515 }
516 let _building = self.graph_build.lock_or_default();
522 if let Some(g) = kept() {
523 return Ok(g);
524 }
525 let install = |g: &Arc<ClassGraph>| *self.class_graph.lock_or_default() = Some((ident, Arc::clone(g)));
526 let disk = self.cache.as_ref().and_then(|c| c.disk.as_deref()).filter(|d| d.uses(Tier::Derived));
527 let key = disk.and_then(|_| self.apk_key(&apk));
528 if let (Some(disk), Some(key)) = (disk, &key) {
529 let stored = disk.get_derived(key, GRAPH_NAME, GRAPH_KIND, GRAPH_FORMAT).and_then(ClassGraph::from_mapped);
530 if let Some(g) = stored.filter(|g| g.0.dex_names().eq(views.iter().map(|v| v.name.as_str()))) {
533 self.stats.derived_hits.fetch_add(1, Ordering::Relaxed);
534 let g = Arc::new(g);
535 install(&g);
536 return Ok(g);
537 }
538 }
539 let t0 = Instant::now();
540 let errors_before = errors.len();
541 let g = Arc::new(ClassGraph::build(views, self.threads(), errors, cancel).ok_or_else(cancelled)?);
542 let cost = t0.elapsed();
543 if let (Some(disk), Some(key)) = (disk, key)
544 && errors.len() == errors_before
545 && disk.admits_derived(cost)
546 {
547 let job = WriteJob { key, apk: Arc::from(apk.path()), kind: Job::Graph { graph: g.clone(), cost } };
548 self.cache.as_ref().expect("a disk implies a cache").put_async(job);
549 }
550 install(&g);
551 Ok(g)
552 }
553
554 pub fn hierarchy(&self, q: &HierarchyQuery) -> Result<HierarchyTree> {
563 self.hierarchy_cancellable(q, &NOT_CANCELLED)
564 }
565
566 pub fn hierarchy_cancellable(&self, q: &HierarchyQuery, cancel: &AtomicBool) -> Result<HierarchyTree> {
573 let method = q.method.as_deref().map(split_method).transpose()?;
574 let (views, mut errors) = self.load_views(None, cancel)?;
575 let g = self.class_graph(&views, &mut errors, cancel)?;
576 let Some(root) = g.id(&q.class).or_else(|| names::denormalize_class_candidates(&q.class).iter().find_map(|c| g.id(c))) else {
579 return Err(EngineError::Query(format!("no class {} in this APK", q.class)));
580 };
581 let max_nodes = q.max_nodes.max(1);
583 let mut depth_of: HashMap<u32, usize> = HashMap::from([(root, 0)]);
584 let mut children: HashMap<u32, Vec<(u32, ClassRelation)>> = HashMap::new();
585 let mut frontier = vec![root];
586 let mut truncated = false;
587 for level in 0..q.depth {
588 let mut following = Vec::new();
589 for &c in &frontier {
590 let mut kept = Vec::new();
591 for (n, rel) in g.neighbours(c, q.direction) {
592 if depth_of.contains_key(&n) {
593 kept.push((n, rel));
594 } else if depth_of.len() < max_nodes {
595 depth_of.insert(n, level + 1);
596 following.push(n);
597 kept.push((n, rel));
598 } else {
599 truncated = true;
600 }
601 }
602 children.insert(c, kept);
603 }
604 if following.is_empty() {
605 break;
606 }
607 frontier = following;
608 }
609 truncated |= frontier.iter().any(|&c| !children.contains_key(&c) && g.neighbours(c, q.direction).next().is_some());
611
612 let mut defines: HashMap<u32, Vec<String>> = HashMap::new();
613 if let Some((name, proto)) = &method {
614 for &c in depth_of.keys() {
615 if cancel.load(Ordering::Relaxed) {
616 return Err(cancelled());
617 }
618 match g.defines(&views, g.name(c), name, proto.as_deref()) {
619 Ok(d) if !d.is_empty() => {
620 defines.insert(c, d);
621 }
622 Ok(_) => {}
623 Err(e) => errors.push(format!("{}: {e}", g.name(c))),
624 }
625 }
626 if defines.is_empty()
630 && let Some(raw) = q.method.as_deref()
631 {
632 for cand in names::denormalize_method_candidates(Some(g.name(root)), raw) {
633 let Ok((name, proto)) = split_method(&cand) else { continue };
634 for &c in depth_of.keys() {
635 if let Ok(d) = g.defines(&views, g.name(c), &name, proto.as_deref())
636 && !d.is_empty()
637 {
638 defines.insert(c, d);
639 }
640 }
641 if !defines.is_empty() {
642 break;
643 }
644 }
645 }
646 }
647
648 let mut rows = Vec::new();
650 let mut expanded = HashSet::new();
651 let mut stack: Vec<(u32, usize, Option<usize>, Option<ClassRelation>)> = vec![(root, 0, None, None)];
652 while let Some((c, depth, parent, relation)) = stack.pop() {
653 let expand_here = depth_of.get(&c) == Some(&depth) && !expanded.contains(&c);
654 let has_children = children.get(&c).is_some_and(|n| !n.is_empty());
655 rows.push(HierarchyRow {
656 depth,
657 parent,
658 class: g.name(c).to_string(),
659 relation,
660 dex: g.dex_of(c).map(String::from),
661 repeated: !expand_here && has_children,
662 defines: defines.get(&c).cloned().unwrap_or_default(),
663 });
664 if expand_here {
665 expanded.insert(c);
666 let me = rows.len() - 1;
667 let mut kids: Vec<_> = children.get(&c).cloned().unwrap_or_default();
668 kids.sort_by(|a, b| g.name(b.0).cmp(g.name(a.0)));
669 for (n, rel) in kids {
670 stack.push((n, depth + 1, Some(me), Some(rel)));
671 }
672 }
673 }
674 errors.sort();
675 errors.dedup();
676 Ok(HierarchyTree {
677 direction: q.direction,
678 depth: q.depth,
679 rows,
680 classes: depth_of.len(),
681 truncated,
682 method: q.method.clone(),
683 errors,
684 })
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691 use std::collections::BTreeSet;
692
693 fn sample() -> ClassGraph {
696 let mut b = Builder::default();
697 let [a, bb, c, i, d] = ["LA;", "LB;", "LC;", "LI;", "Lé;"].map(|n| b.intern(n));
698 assert!(b.define(a, 0, 0) && b.define(bb, 0, 1) && b.define(c, 1, 0) && b.define(d, 0, 2));
699 assert!(!b.define(c, 0, 9), "the first definition wins");
700 b.edge(bb, a, ClassRelation::Extends);
701 b.edge(bb, i, ClassRelation::Implements);
702 b.edge(c, bb, ClassRelation::Extends);
703 b.edge(c, bb, ClassRelation::Extends);
704 b.annotate(c, "LM;".into());
705 b.annotate(bb, "LM;".into());
706 b.annotate(bb, "LN;".into());
707 ClassGraph(b.finish(["classes.dex", "lib.jar/classes.dex"].into_iter()))
708 }
709
710 fn check(g: &ClassGraph) {
711 assert_eq!(g.reachable("LA;", Lineage::Subclasses, 10), ["LB;", "LC;"]);
712 assert_eq!(g.reachable("LC;", Lineage::Supers, 10), ["LB;", "LA;", "LI;"]);
713 assert_eq!(g.reachable("LC;", Lineage::Supers, 2), ["LB;", "LA;"]);
714 assert!(g.reachable("LZ;", Lineage::Supers, 10).is_empty());
715 assert_eq!(g.classes_annotated("LM;"), ["LB;", "LC;"]);
716 assert_eq!(g.classes_annotated("LN;"), ["LB;"]);
717 assert!(g.classes_annotated("LL;").is_empty() && g.classes_annotated("LZ;").is_empty());
718 assert_eq!(g.dex_of(g.id("LC;").unwrap()), Some("lib.jar/classes.dex"));
719 assert_eq!(g.dex_of(g.id("LI;").unwrap()), None);
720 assert_eq!(g.name(g.id("Lé;").unwrap()), "Lé;");
721 let re = regex::bytes::Regex::new("^L[A-C];$").unwrap();
722 assert_eq!(
723 g.defined_classes_matching(&re).into_iter().collect::<BTreeSet<_>>(),
724 BTreeSet::from(["LA;".into(), "LB;".into(), "LC;".into()])
725 );
726 assert_eq!(g.0.dex_names().collect::<Vec<_>>(), ["classes.dex", "lib.jar/classes.dex"]);
727 }
728
729 struct Store {
731 _dir: tempfile::TempDir,
732 disk: asc_cache::DiskCache,
733 key: asc_cache::ApkKey,
734 }
735
736 impl Store {
737 fn new() -> Store {
738 let dir = tempfile::tempdir().unwrap();
739 let disk = asc_cache::DiskCache::open(dir.path().join("cache")).with_capacity(1 << 30);
740 let apk = dir.path().join("a.apk");
741 std::fs::write(&apk, asc_fixture::make_apk(&[("classes.dex", &asc_fixture::make_rich_dex())], true)).unwrap();
742 let key = asc_cache::ApkKey::for_apk(&asc_apk::Apk::open(&apk).unwrap()).unwrap();
743 Store { _dir: dir, disk, key }
744 }
745
746 fn mapped(&self, bytes: &[u8]) -> Option<ClassGraph> {
747 self.disk.put_derived(&self.key, GRAPH_NAME, GRAPH_KIND, GRAPH_FORMAT, std::time::Duration::ZERO, bytes).unwrap();
748 ClassGraph::from_mapped(self.disk.get_derived(&self.key, GRAPH_NAME, GRAPH_KIND, GRAPH_FORMAT).unwrap())
749 }
750 }
751
752 #[test]
753 fn graphs_answer_alike_built_and_mapped() {
754 let store = Store::new();
755 let g = sample();
756 check(&g);
757 let back = store.mapped(g.bytes()).unwrap();
758 check(&back);
759 assert_eq!(back.heap_bytes(), 0);
760 assert!(g.heap_bytes() >= g.bytes().len());
761 let empty = ClassGraph(Builder::default().finish(std::iter::empty()));
763 assert!(store.mapped(empty.bytes()).unwrap().reachable("LA;", Lineage::Supers, 1).is_empty());
764 let mut b = Builder::default();
766 let names: Vec<String> = (0..5000).map(|i| format!("Lc{i};")).collect();
767 let ids: Vec<u32> = names.iter().map(|n| b.intern(n)).collect();
768 assert_eq!(names.iter().map(|n| b.intern(n)).collect::<Vec<_>>(), ids);
769 let g = store.mapped(ClassGraph(b.finish(std::iter::empty())).bytes()).unwrap();
770 assert!(names.iter().zip(&ids).all(|(n, &id)| g.id(n) == Some(id) && g.name(id) == n));
771 }
772
773 #[test]
774 fn damaged_bytes_never_index_out_of_bounds() {
775 let store = Store::new();
776 let bytes = sample().bytes().to_vec();
777 for n in 0..bytes.len() {
778 assert!(store.mapped(&bytes[..n]).is_none(), "truncated to {n}");
779 }
780 let any = regex::bytes::Regex::new(".").unwrap();
783 for at in 0..bytes.len() {
784 let mut b = bytes.clone();
785 b[at] ^= 0xa5;
786 if let Some(g) = store.mapped(&b) {
787 for name in ["LA;", "LB;", "LC;", "LI;", "Lé;"] {
788 g.reachable(name, Lineage::Subclasses, 10);
789 g.reachable(name, Lineage::Supers, 10);
790 g.id(name).map(|id| g.dex_of(id));
791 }
792 g.classes_annotated("LM;");
793 g.defined_classes_matching(&any);
794 }
795 }
796 }
797
798 fn poison<T>(lock: &Mutex<T>) {
800 let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
801 let _held = lock.lock();
802 panic!("a request panics under the lock");
803 }));
804 assert!(caught.is_err() && lock.is_poisoned());
805 }
806
807 #[test]
808 fn a_panic_under_a_cache_lock_costs_a_rebuild_not_the_engine() {
809 let dir = tempfile::tempdir().unwrap();
810 let apk = dir.path().join("a.apk");
811 std::fs::write(&apk, asc_fixture::make_apk(&[("classes.dex", &asc_fixture::make_rich_dex())], true)).unwrap();
812 let engine = Engine::new(&apk).with_threads(2).with_cache(crate::CacheConfig { memory_bytes: 64 << 20, ..Default::default() });
813 let q = HierarchyQuery { class: "Lexample/Custom;".into(), direction: Lineage::Supers, depth: 4, max_nodes: 100, method: None };
814 let before = engine.hierarchy(&q).unwrap().rows;
815 assert!(before.len() > 1, "{before:?}");
816 assert!(engine.resident_bytes() > 0);
817 poison(&engine.class_graph);
820 poison(&engine.graph_build);
821 poison(&engine.resources);
822 poison(&engine.opened);
823 poison(&engine.key_cache);
824 poison(engine.cache.as_ref().and_then(|c| c.memory.as_deref()).expect("a memory cache"));
825 assert_eq!(engine.resident_bytes(), 0);
827 assert_eq!(engine.memory_cache_bytes(), 0);
828 assert_eq!(engine.hierarchy(&q).unwrap().rows, before);
829 assert!(engine.resident_bytes() > 0);
830 assert!(!engine.class_graph.is_poisoned() && !engine.graph_build.is_poisoned());
831 engine.release_memory_cache();
832 assert_eq!((engine.resident_bytes(), engine.memory_cache_bytes()), (0, 0));
833 }
834}