Skip to content

Commit

Permalink
feat: Introduce ProguardCache format (#42)
Browse files Browse the repository at this point in the history
This introduces a stable binary format similar to SymCache.
It allows lookups that are fast, if not as fast as via a `ProguardMapper`,
but doesn't require parsing the entire file into memory.
  • Loading branch information
loewenheim authored Jul 3, 2024
2 parents cf3ec0a + 055d9a4 commit 60a5ea7
Show file tree
Hide file tree
Showing 11 changed files with 1,575 additions and 12 deletions.
8 changes: 7 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ edition = "2018"
uuid = ["uuid_", "lazy_static"]

[dependencies]
uuid_ = { package = "uuid", version = "1.0.0", features = ["v5"], optional = true }
lazy_static = { version = "1.4.0", optional = true }
thiserror = "1.0.61"
uuid_ = { package = "uuid", version = "1.0.0", features = ["v5"], optional = true }
watto = { version = "0.1.0", features = ["writer", "strings"] }

[dev-dependencies]
lazy_static = "1.4.0"
Expand All @@ -24,3 +26,7 @@ criterion = "0.4"
[[bench]]
name = "proguard_parsing"
harness = false

[[bench]]
name = "proguard_mapping"
harness = false
54 changes: 54 additions & 0 deletions benches/proguard_mapping.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use proguard::{ProguardCache, ProguardMapper, ProguardMapping};

static MAPPING: &[u8] = include_bytes!("../tests/res/mapping-inlines.txt");

static RAW: &str = r#"java.lang.RuntimeException: Button press caused an exception!
at io.sentry.sample.MainActivity.t(MainActivity.java:1)
at e.a.c.a.onClick
at android.view.View.performClick(View.java:7125)
at android.view.View.performClickInternal(View.java:7102)
at android.view.View.access$3500(View.java:801)
at android.view.View$PerformClick.run(View.java:27336)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)"#;

fn benchmark_remapping(c: &mut Criterion) {
let mut cache_buf = Vec::new();
let mapping = ProguardMapping::new(MAPPING);
ProguardCache::write(&mapping, &mut cache_buf).unwrap();
let cache = ProguardCache::parse(&cache_buf).unwrap();
let mapper = ProguardMapper::new(mapping);

let mut group = c.benchmark_group("Proguard Remapping");

group.bench_function("Cache, preparsed", |b| {
b.iter(|| cache.remap_stacktrace(black_box(RAW)))
});
group.bench_function("Mapper, preparsed", |b| {
b.iter(|| mapper.remap_stacktrace(black_box(RAW)))
});

group.bench_function("Cache", |b| {
b.iter(|| {
let cache = ProguardCache::parse(black_box(&cache_buf)).unwrap();
cache.remap_stacktrace(black_box(RAW))
})
});
group.bench_function("Mapper", |b| {
b.iter(|| {
let mapper = ProguardMapper::new(black_box(ProguardMapping::new(MAPPING)));
mapper.remap_stacktrace(black_box(RAW))
})
});

group.finish();
}

criterion_group!(benches, benchmark_remapping);
criterion_main!(benches);
19 changes: 16 additions & 3 deletions benches/proguard_parsing.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use proguard::{ProguardMapper, ProguardMapping};
use proguard::{ProguardCache, ProguardMapper, ProguardMapping};

static MAPPING: &[u8] = include_bytes!("../tests/res/mapping.txt");

fn proguard_mapper(mapping: ProguardMapping) -> ProguardMapper {
ProguardMapper::new(mapping)
}

fn proguard_cache(cache: &[u8]) -> ProguardCache {
ProguardCache::parse(cache).unwrap()
}

fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("proguard mapper", |b| {
b.iter(|| proguard_mapper(black_box(ProguardMapping::new(MAPPING))))
let mut cache = Vec::new();
let mapping = ProguardMapping::new(MAPPING);
ProguardCache::write(&mapping, &mut cache).unwrap();

let mut group = c.benchmark_group("Proguard Parsing");
group.bench_function("Proguard Mapper", |b| {
b.iter(|| proguard_mapper(black_box(mapping.clone())))
});

group.bench_function("Proguard Cache", |b| {
b.iter(|| proguard_cache(black_box(&cache)))
});
}

Expand Down
200 changes: 200 additions & 0 deletions src/cache/debug.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
use std::fmt;

use crate::ProguardCache;

use super::raw;

/// A variant of a class entry in a proguard cache file with
/// nice-ish `Debug` and `Display` representations.
pub struct ClassDebug<'a, 'data> {
pub(crate) cache: &'a ProguardCache<'data>,
pub(crate) raw: &'a raw::Class,
}

impl ClassDebug<'_, '_> {
fn obfuscated_name(&self) -> &str {
self.cache
.read_string(self.raw.obfuscated_name_offset)
.unwrap()
}

fn original_name(&self) -> &str {
self.cache
.read_string(self.raw.original_name_offset)
.unwrap()
}

fn file_name(&self) -> Option<&str> {
self.cache.read_string(self.raw.file_name_offset).ok()
}
}

impl fmt::Debug for ClassDebug<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Class")
.field("obfuscated_name", &self.obfuscated_name())
.field("original_name", &self.original_name())
.field("file_name", &self.file_name())
.finish()
}
}

impl fmt::Display for ClassDebug<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} -> {}:", self.original_name(), self.obfuscated_name())?;
if let Some(file_name) = self.file_name() {
writeln!(f)?;
write!(f, r##"# {{"id":"sourceFile","fileName":"{file_name}"}}"##)?;
}
Ok(())
}
}

/// A variant of a member entry in a proguard cache file with
/// nice-ish `Debug` and `Display` representations.
pub struct MemberDebug<'a, 'data> {
pub(crate) cache: &'a ProguardCache<'data>,
pub(crate) raw: &'a raw::Member,
}

impl MemberDebug<'_, '_> {
fn original_class(&self) -> Option<&str> {
self.cache.read_string(self.raw.original_class_offset).ok()
}

fn original_file(&self) -> Option<&str> {
self.cache.read_string(self.raw.original_file_offset).ok()
}

fn params(&self) -> &str {
self.cache
.read_string(self.raw.params_offset)
.unwrap_or_default()
}

fn obfuscated_name(&self) -> &str {
self.cache
.read_string(self.raw.obfuscated_name_offset)
.unwrap()
}

fn original_name(&self) -> &str {
self.cache
.read_string(self.raw.original_name_offset)
.unwrap()
}

fn original_endline(&self) -> Option<u32> {
if self.raw.original_endline != u32::MAX {
Some(self.raw.original_endline)
} else {
None
}
}
}

impl fmt::Debug for MemberDebug<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Member")
.field("obfuscated_name", &self.obfuscated_name())
.field("startline", &self.raw.startline)
.field("endline", &self.raw.endline)
.field("original_name", &self.original_name())
.field("original_class", &self.original_class())
.field("original_file", &self.original_file())
.field("original_startline", &self.raw.original_startline)
.field("original_endline", &self.original_endline())
.field("params", &self.params())
.finish()
}
}

impl fmt::Display for MemberDebug<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// XXX: We could print the actual return type here if we saved it in the format.
// Wonder if it's worth it, since we'd only use it in this display impl.
write!(f, " {}:{}:<ret> ", self.raw.startline, self.raw.endline)?;
if let Some(original_class) = self.original_class() {
write!(f, "{original_class}.")?;
}
write!(
f,
"{}({}):{}",
self.original_name(),
self.params(),
self.raw.original_startline
)?;
if let Some(end) = self.original_endline() {
write!(f, ":{end}")?;
}
write!(f, " -> {}", self.obfuscated_name())?;
Ok(())
}
}

pub struct CacheDebug<'a, 'data> {
cache: &'a ProguardCache<'data>,
}

impl fmt::Display for CacheDebug<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for class in self.cache.classes {
writeln!(
f,
"{}",
ClassDebug {
raw: class,
cache: self.cache
}
)?;
let Some(members) = self.cache.get_class_members(class) else {
continue;
};

for member in members {
writeln!(
f,
"{}",
MemberDebug {
raw: member,
cache: self.cache
}
)?;
}
}
Ok(())
}
}

impl<'data> ProguardCache<'data> {
/// Returns an iterator over class entries in this cache file that can be debug printed.
pub fn debug_classes<'r>(&'r self) -> impl Iterator<Item = ClassDebug<'r, 'data>> {
self.classes.iter().map(move |c| ClassDebug {
cache: self,
raw: c,
})
}

/// Returns an iterator over member entries in this cache file that can be debug printed.
pub fn debug_members<'r>(&'r self) -> impl Iterator<Item = MemberDebug<'r, 'data>> {
self.members.iter().map(move |m| MemberDebug {
cache: self,
raw: m,
})
}

/// Returns an iterator over by-params member entries in this cache file that can be debug printed.
pub fn debug_members_by_params<'r>(&'r self) -> impl Iterator<Item = MemberDebug<'r, 'data>> {
self.members_by_params.iter().map(move |m| MemberDebug {
cache: self,
raw: m,
})
}

/// Creates a view of the cache that implements `Display`.
///
/// The `Display` impl is very similar to the original proguard format.
pub fn display(&self) -> CacheDebug {
CacheDebug { cache: self }
}
}
Loading

0 comments on commit 60a5ea7

Please sign in to comment.