diff --git a/src/cache/debug.rs b/src/cache/debug.rs index 957308d..cf901a3 100644 --- a/src/cache/debug.rs +++ b/src/cache/debug.rs @@ -107,6 +107,7 @@ impl fmt::Debug for MemberDebug<'_, '_> { .field("original_endline", &self.original_endline()) .field("params", &self.params()) .field("is_synthesized", &self.raw.is_synthesized()) + .field("is_outline", &self.raw.is_outline()) .finish() } } diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 8a31d5d..b681761 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -2,15 +2,17 @@ //! //! # Structure //! A [`ProguardCache`] file comprises the following parts: -//! * A [header](ProguardCache::header), containing the version number, the numbers of class, member, and -//! member-by-params entries, and the length of the string section; +//! * A [header](ProguardCache::header), containing: +//! - the format version, +//! - the number of class, member, and member-by-params entries, +//! - the number of outline mapping pairs, +//! - and the length of the string section; //! * A [list](ProguardCache::classes) of [`Class`](raw::Class) entries; //! * A [list](ProguardCache::members) of [`Member`](raw::Member) entries; -//! * Another [list](Proguard_cache::members_by_params) of `Member` entries, sorted -//! by parameter strings; -//! * A [string section](ProguardCache::string_bytes) in which class names, method -//! names, &c. are collected. Whenever a class or member entry references a string, -//! it is by offset into this section. +//! * Another [list](Proguard_cache::members_by_params) of `Member` entries, sorted by parameter strings; +//! * A [list] of outline mapping pairs shared by all members; +//! * A [string section](ProguardCache::string_bytes) in which class names, method names, &c. are collected. +//! Whenever a class or member entry references a string, it is by offset into this section. //! //! ## Class entries //! A class entry contains @@ -26,7 +28,10 @@ //! * an obfuscated and an original method name, //! * a start and end line (1- based and inclusive), //! * a params string, -//! * and an `is_synthesized` flag. +//! * an `is_synthesized` flag, +//! * an `is_outline` flag designating outline methods, +//! * an `outline_pairs_offset` and `outline_pairs_len` which slice into the global outline +//! pairs section. //! //! It may also contain //! * an original class name, @@ -37,9 +42,15 @@ //! obfuscated method name, and finally by the order in which they were encountered //! in the original proguard file. //! -//! Member entries in `members_by_params` are sorted by the class they belong to, -//! then by obfuscated method name, then by params string, and finally -//! by the order in which they were encountered in the original proguard file. +//! Member entries in `members_by_params` are sorted by the class they belong to, then by obfuscated +//! method name, then by params string, and finally by the order in which they were encountered in the +//! original proguard file. +//! +//! ## Outline pairs section +//! The outline pairs section is a flat array of pairs mapping an outline-position to a callsite line. +//! Each [`Member`](raw::Member) that carries outline callsite information references a sub-slice of this +//! section via its `outline_pairs_offset` and `outline_pairs_len`. This keeps members fixed-size and +//! enables zero-copy parsing while supporting variable-length metadata. mod debug; mod raw; @@ -332,17 +343,164 @@ impl<'data> ProguardCache<'data> { }) } + /// Returns the outline mapping pairs slice for a given member. + fn member_outline_pairs(&self, member: &raw::Member) -> &'data [raw::OutlinePair] { + let start = member.outline_pairs_offset as usize; + let end = start + member.outline_pairs_len as usize; + if start >= self.outline_pairs.len() || end > self.outline_pairs.len() { + &self.outline_pairs[0..0] + } else { + &self.outline_pairs[start..end] + } + } + + /// If the previous frame was an outline and carried a position, attempt to + /// map that outline position to a callsite position for the given method. + fn map_outline_position( + &self, + class: &str, + method: &str, + callsite_line: usize, + pos: usize, + parameters: Option<&str>, + ) -> Option { + let class = self.get_class(class)?; + + let candidates: &[raw::Member] = if let Some(params) = parameters { + let members = self.get_class_members_by_params(class)?; + Self::find_range_by_binary_search(members, |m| { + let Ok(obfuscated_name) = self.read_string(m.obfuscated_name_offset) else { + return Ordering::Greater; + }; + let p = self.read_string(m.params_offset).unwrap_or_default(); + (obfuscated_name, p).cmp(&(method, params)) + })? + } else { + let members = self.get_class_members(class)?; + Self::find_range_by_binary_search(members, |m| { + let Ok(obfuscated_name) = self.read_string(m.obfuscated_name_offset) else { + return Ordering::Greater; + }; + obfuscated_name.cmp(method) + })? + }; + + candidates + .iter() + .filter(|m| { + m.endline == 0 + || (callsite_line >= m.startline as usize + && callsite_line <= m.endline as usize) + }) + .find_map(|m| { + self.member_outline_pairs(m) + .iter() + .find(|pair| pair.outline_pos as usize == pos) + .map(|pair| pair.callsite_line as usize) + }) + } + + /// Determines if a frame refers to an outline method, either via the + /// method-level flag or via any matching mapping entry for the frame line. + fn is_outline_frame( + &self, + class: &str, + method: &str, + line: usize, + parameters: Option<&str>, + ) -> bool { + let Some(class) = self.get_class(class) else { + return false; + }; + + let candidates: &[raw::Member] = if let Some(params) = parameters { + let Some(members) = self.get_class_members_by_params(class) else { + return false; + }; + let Some(range) = Self::find_range_by_binary_search(members, |m| { + let Ok(obfuscated_name) = self.read_string(m.obfuscated_name_offset) else { + return Ordering::Greater; + }; + let p = self.read_string(m.params_offset).unwrap_or_default(); + (obfuscated_name, p).cmp(&(method, params)) + }) else { + return false; + }; + range + } else { + let Some(members) = self.get_class_members(class) else { + return false; + }; + let Some(range) = Self::find_range_by_binary_search(members, |m| { + let Ok(obfuscated_name) = self.read_string(m.obfuscated_name_offset) else { + return Ordering::Greater; + }; + obfuscated_name.cmp(method) + }) else { + return false; + }; + range + }; + + candidates.iter().any(|m| { + m.is_outline() + && (m.endline == 0 || (line >= m.startline as usize && line <= m.endline as usize)) + }) + } + + /// Applies any carried outline position to the frame line and returns the adjusted frame. + fn prepare_frame_for_mapping<'a>( + &self, + frame: &StackFrame<'a>, + carried_outline_pos: &mut Option, + ) -> StackFrame<'a> { + let mut effective = frame.clone(); + if let Some(pos) = carried_outline_pos.take() { + if let Some(mapped) = self.map_outline_position( + effective.class, + effective.method, + effective.line, + pos, + effective.parameters, + ) { + effective.line = mapped; + } + } + + effective + } + /// Remaps a complete Java StackTrace, similar to [`Self::remap_stacktrace_typed`] but instead works on /// strings as input and output. pub fn remap_stacktrace(&self, input: &str) -> Result { let mut stacktrace = String::new(); let mut lines = input.lines(); + let mut carried_outline_pos: Option = None; + if let Some(line) = lines.next() { match stacktrace::parse_throwable(line) { None => match stacktrace::parse_frame(line) { None => writeln!(&mut stacktrace, "{line}")?, - Some(frame) => format_frames(&mut stacktrace, line, self.remap_frame(&frame))?, + Some(frame) => { + if self.is_outline_frame( + frame.class, + frame.method, + frame.line, + frame.parameters, + ) { + carried_outline_pos = Some(frame.line); + } else { + let effective_frame = + self.prepare_frame_for_mapping(&frame, &mut carried_outline_pos); + + format_frames( + &mut stacktrace, + line, + self.remap_frame(&effective_frame), + )?; + } + } }, Some(throwable) => { format_throwable(&mut stacktrace, line, self.remap_throwable(&throwable))? @@ -361,7 +519,21 @@ impl<'data> ProguardCache<'data> { format_cause(&mut stacktrace, line, self.remap_throwable(&cause))? } }, - Some(frame) => format_frames(&mut stacktrace, line, self.remap_frame(&frame))?, + Some(frame) => { + if self.is_outline_frame( + frame.class, + frame.method, + frame.line, + frame.parameters, + ) { + carried_outline_pos = Some(frame.line); + continue; + } + + let effective_frame = + self.prepare_frame_for_mapping(&frame, &mut carried_outline_pos); + format_frames(&mut stacktrace, line, self.remap_frame(&effective_frame))?; + } } } Ok(stacktrace) @@ -374,20 +546,22 @@ impl<'data> ProguardCache<'data> { .as_ref() .and_then(|t| self.remap_throwable(t)); - let frames = - trace - .frames - .iter() - .fold(Vec::with_capacity(trace.frames.len()), |mut frames, f| { - let mut peek_frames = self.remap_frame(f).peekable(); - if peek_frames.peek().is_some() { - frames.extend(peek_frames); - } else { - frames.push(f.clone()); - } + let mut carried_outline_pos: Option = None; + let mut frames: Vec> = Vec::with_capacity(trace.frames.len()); + for f in trace.frames.iter() { + if self.is_outline_frame(f.class, f.method, f.line, f.parameters) { + carried_outline_pos = Some(f.line); + continue; + } - frames - }); + let effective = self.prepare_frame_for_mapping(f, &mut carried_outline_pos); + let mut iter = self.remap_frame(&effective).peekable(); + if iter.peek().is_some() { + frames.extend(iter); + } else { + frames.push(f.clone()); + } + } let cause = trace .cause diff --git a/src/cache/raw.rs b/src/cache/raw.rs index 0a06bac..183f7b2 100644 --- a/src/cache/raw.rs +++ b/src/cache/raw.rs @@ -19,7 +19,7 @@ pub(crate) const PRGCACHE_MAGIC: u32 = u32::from_le_bytes(PRGCACHE_MAGIC_BYTES); pub(crate) const PRGCACHE_MAGIC_FLIPPED: u32 = PRGCACHE_MAGIC.swap_bytes(); /// The current version of the ProguardCache format. -pub const PRGCACHE_VERSION: u32 = 2; +pub const PRGCACHE_VERSION: u32 = 3; /// The header of a proguard cache file. #[derive(Debug, Clone, PartialEq, Eq)] @@ -35,6 +35,8 @@ pub(crate) struct Header { pub(crate) num_members: u32, /// The total number of member-by-params entries in this cache. pub(crate) num_members_by_params: u32, + /// The total number of outline mapping pairs across all members. + pub(crate) num_outline_pairs: u32, /// The number of string bytes in this cache. pub(crate) string_bytes: u32, } @@ -114,13 +116,20 @@ pub(crate) struct Member { pub(crate) original_endline: u32, /// The entry's parameter string (offset into the strings section). pub(crate) params_offset: u32, + /// Offset into the outline pairs section for this member's outline callsite mapping. + pub(crate) outline_pairs_offset: u32, + /// Number of outline pairs for this member. + pub(crate) outline_pairs_len: u32, /// Whether this member was synthesized by the compiler. /// /// `0` means `false`, all other values mean `true`. pub(crate) is_synthesized: u8, - + /// Whether this member refers to an outline method. + /// + /// `0` means `false`, all other values mean `true`. + pub(crate) is_outline: u8, /// Reserved space. - pub(crate) _reserved: [u8; 3], + pub(crate) _reserved: [u8; 2], } impl Member { @@ -128,12 +137,26 @@ impl Member { pub(crate) fn is_synthesized(&self) -> bool { self.is_synthesized != 0 } + /// Returns true if this member refers to an outline method. + pub(crate) fn is_outline(&self) -> bool { + self.is_outline != 0 + } } unsafe impl Pod for Header {} unsafe impl Pod for Class {} unsafe impl Pod for Member {} +/// A single outline mapping pair: outline position -> callsite line. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub(crate) struct OutlinePair { + pub(crate) outline_pos: u32, + pub(crate) callsite_line: u32, +} + +unsafe impl Pod for OutlinePair {} + /// The serialized `ProguardCache` binary format. #[derive(Clone, PartialEq, Eq)] pub struct ProguardCache<'data> { @@ -153,6 +176,8 @@ pub struct ProguardCache<'data> { /// These entries are sorted by class, then /// obfuscated method name, then params string. pub(crate) members_by_params: &'data [Member], + /// A flat list of outline mapping pairs. + pub(crate) outline_pairs: &'data [OutlinePair], /// The collection of all strings in the cache file. pub(crate) string_bytes: &'data [u8], } @@ -196,6 +221,11 @@ impl<'data> ProguardCache<'data> { Member::slice_from_prefix(rest, header.num_members_by_params as usize) .ok_or(CacheErrorKind::InvalidMembers)?; + let (_, rest) = watto::align_to(rest, 8).ok_or(CacheErrorKind::InvalidMembers)?; + let (outline_pairs, rest) = + OutlinePair::slice_from_prefix(rest, header.num_outline_pairs as usize) + .ok_or(CacheErrorKind::InvalidMembers)?; + let (_, string_bytes) = watto::align_to(rest, 8).ok_or(CacheErrorKind::UnexpectedStringBytes { expected: header.string_bytes as usize, @@ -215,6 +245,7 @@ impl<'data> ProguardCache<'data> { classes, members, members_by_params, + outline_pairs, string_bytes, }) } @@ -292,7 +323,6 @@ impl<'data> ProguardCache<'data> { // At this point, we know how many members/members-by-params each class has because we kept count, // but we don't know where each class's entries start. We'll rectify that below. - let mut writer = watto::Writer::new(writer); let string_bytes = string_table.into_bytes(); let num_members = classes.values().map(|c| c.class.members_len).sum::(); @@ -301,41 +331,86 @@ impl<'data> ProguardCache<'data> { .map(|c| c.class.members_by_params_len) .sum::(); + // Build output vectors first to know outline pair count. + let mut out_classes: Vec = Vec::with_capacity(classes.len()); + let mut members: Vec = Vec::with_capacity(num_members as usize); + let mut members_by_params: Vec = Vec::with_capacity(num_members_by_params as usize); + let mut outline_pairs: Vec = Vec::new(); + + for mut c in classes.into_values() { + // Set offsets relative to current vector sizes + c.class.members_offset = members.len() as u32; + c.class.members_by_params_offset = members_by_params.len() as u32; + + // Serialize members without params + for (_method, ms) in c.members.into_iter() { + for mut mp in ms.into_iter() { + let start = outline_pairs.len() as u32; + if !mp.outline_pairs.is_empty() { + mp.member.outline_pairs_offset = start; + mp.member.outline_pairs_len = mp.outline_pairs.len() as u32; + outline_pairs.extend(mp.outline_pairs.into_iter()); + } else { + mp.member.outline_pairs_offset = start; + mp.member.outline_pairs_len = 0; + } + members.push(mp.member); + } + } + + // Serialize members by params + for (_key, ms) in c.members_by_params.into_iter() { + for mut mp in ms.into_iter() { + let start = outline_pairs.len() as u32; + if !mp.outline_pairs.is_empty() { + mp.member.outline_pairs_offset = start; + mp.member.outline_pairs_len = mp.outline_pairs.len() as u32; + outline_pairs.extend(mp.outline_pairs.into_iter()); + } else { + mp.member.outline_pairs_offset = start; + mp.member.outline_pairs_len = 0; + } + members_by_params.push(mp.member); + } + } + + out_classes.push(c.class); + } + + let num_outline_pairs = outline_pairs.len() as u32; + let header = Header { magic: PRGCACHE_MAGIC, version: PRGCACHE_VERSION, - num_classes: classes.len() as u32, + num_classes: out_classes.len() as u32, num_members, num_members_by_params, + num_outline_pairs, string_bytes: string_bytes.len() as u32, }; + let mut writer = watto::Writer::new(writer); writer.write_all(header.as_bytes())?; writer.align_to(8)?; - let mut members = Vec::new(); - let mut members_by_params = Vec::new(); - - for mut c in classes.into_values() { - // We can now set the class's members_offset/members_by_params_offset. - c.class.members_offset = members.len() as u32; - c.class.members_by_params_offset = members.len() as u32; - members.extend(c.members.into_values().flat_map(|m| m.into_iter())); - members_by_params.extend( - c.members_by_params - .into_values() - .flat_map(|m| m.into_iter()), - ); - writer.write_all(c.class.as_bytes())?; + // Write classes + for c in out_classes.iter() { + writer.write_all(c.as_bytes())?; } writer.align_to(8)?; + // Write member sections writer.write_all(members.as_bytes())?; writer.align_to(8)?; writer.write_all(members_by_params.as_bytes())?; writer.align_to(8)?; + // Write outline pairs + writer.write_all(outline_pairs.as_bytes())?; + writer.align_to(8)?; + + // Write strings writer.write_all(&string_bytes)?; Ok(()) @@ -346,7 +421,7 @@ impl<'data> ProguardCache<'data> { parsed: &ParsedProguardMapping<'_>, obfuscated_name_offset: u32, member: &builder::Member, - ) -> Member { + ) -> MemberInProgress { let original_file = parsed .class_infos .get(&member.method.receiver.name()) @@ -370,8 +445,22 @@ impl<'data> ProguardCache<'data> { .copied() .unwrap_or_default(); let is_synthesized = method_info.is_synthesized as u8; + let is_outline = method_info.is_outline as u8; + + let outline_pairs: Vec = member + .outline_callsite_positions + .as_ref() + .map(|m| { + m.iter() + .map(|(k, v)| OutlinePair { + outline_pos: *k as u32, + callsite_line: *v as u32, + }) + .collect() + }) + .unwrap_or_default(); - Member { + let member = Member { startline: member.startline as u32, endline: member.endline as u32, original_class_offset, @@ -382,7 +471,15 @@ impl<'data> ProguardCache<'data> { obfuscated_name_offset, params_offset, is_synthesized, - _reserved: [0; 3], + is_outline, + outline_pairs_offset: 0, + outline_pairs_len: 0, + _reserved: [0; 2], + }; + + MemberInProgress { + member, + outline_pairs, } } @@ -414,6 +511,13 @@ impl<'data> ProguardCache<'data> { assert!(self.read_string(member.obfuscated_name_offset).is_ok()); assert!(self.read_string(member.original_name_offset).is_ok()); assert!(member.is_synthesized == 0 || member.is_synthesized == 1); + assert!(member.is_outline == 0 || member.is_outline == 1); + + // Ensure outline pair range is within bounds + let start = member.outline_pairs_offset as usize; + let len = member.outline_pairs_len as usize; + let end = start.saturating_add(len); + assert!(end <= self.outline_pairs.len()); if member.params_offset != u32::MAX { assert!(self.read_string(member.params_offset).is_ok()); @@ -441,7 +545,13 @@ struct ClassInProgress<'data> { /// The class record. class: Class, /// The members records for the class, grouped by method name. - members: BTreeMap<&'data str, Vec>, + members: BTreeMap<&'data str, Vec>, /// The member records for the class, grouped by method name and parameter string. - members_by_params: BTreeMap<(&'data str, &'data str), Vec>, + members_by_params: BTreeMap<(&'data str, &'data str), Vec>, +} + +#[derive(Debug, Clone, Default)] +struct MemberInProgress { + member: Member, + outline_pairs: Vec, } diff --git a/tests/r8.rs b/tests/r8.rs index f5540f3..502291a 100644 --- a/tests/r8.rs +++ b/tests/r8.rs @@ -1,6 +1,6 @@ use std::sync::LazyLock; -use proguard::{ProguardMapper, ProguardMapping, StackFrame}; +use proguard::{ProguardCache, ProguardMapper, ProguardMapping, StackFrame}; static MAPPING_R8: &[u8] = include_bytes!("res/mapping-r8.txt"); static MAPPING_R8_SYMBOLICATED_FILE_NAMES: &[u8] = @@ -186,6 +186,108 @@ fn test_remap_outlines() { at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1899)"#.trim(), test.unwrap().trim()); } +#[test] +fn test_remap_outlines_cache() { + let mapping = ProguardMapping::new(MAPPING_OUTLINE_COMPLEX); + + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + cache.test(); + + let test = cache.remap_stacktrace( + r#" + java.lang.IllegalStateException: Oops! + at ev.h.b(SourceFile:3) + at uu0.k.l(SourceFile:43) + at b80.f.a(SourceFile:33) + at er3.f.invoke(SourceFile:3) + at yv0.g.d(SourceFile:17) + at er3.g$a.invoke(SourceFile:36) + at h1.p0.d(SourceFile:5) + at p1.k.c(SourceFile:135) + at h1.y.A(SourceFile:111) + at h1.y.m(SourceFile:6) + at h1.e3.invoke(SourceFile:231) + at w2.r0$c.doFrame(SourceFile:7) + at w2.q0$c.doFrame(SourceFile:48) + at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1899)"#, + ); + + assert_eq!(r#" + java.lang.IllegalStateException: Oops! + at com.example.projection.MapProjectionViewController.onProjectionView(MapProjectionViewController.kt:160) + at com.example.projection.MapProjectionViewController.createProjectionMarkerInternal(MapProjectionViewController.kt:133) + at com.example.projection.MapProjectionViewController.createProjectionMarker(MapProjectionViewController.kt:79) + at com.example.MapAnnotations.createProjectionMarker(MapAnnotations.kt:63) + at com.example.mapcomponents.marker.currentlocation.DotRendererDelegate.createCurrentLocationProjectionMarker(DotRendererDelegate.kt:101) + at com.example.mapcomponents.marker.currentlocation.DotRendererDelegate.render(DotRendererDelegate.kt:34) + at com.example.mapcomponents.marker.currentlocation.CurrentLocationRenderer.render(CurrentLocationRenderer.kt:39) + at com.example.map.internal.CurrentLocationMarkerMapCollectionKt$CurrentLocationMarkerMapCollection$1$1$mapReadyCallback$1.invoke(CurrentLocationMarkerMapCollection.kt:36) + at com.example.map.internal.CurrentLocationMarkerMapCollectionKt$CurrentLocationMarkerMapCollection$1$1$mapReadyCallback$1.invoke(CurrentLocationMarkerMapCollection.kt:36) + at com.example.mapbox.MapboxMapView.addMapReadyCallback(MapboxMapView.kt:368) + at com.example.map.internal.CurrentLocationMarkerMapCollectionKt$CurrentLocationMarkerMapCollection$1$1.invoke(CurrentLocationMarkerMapCollection.kt:40) + at com.example.map.internal.CurrentLocationMarkerMapCollectionKt$CurrentLocationMarkerMapCollection$1$1.invoke(CurrentLocationMarkerMapCollection.kt:35) + at androidx.compose.runtime.DisposableEffectImpl.onRemembered(Effects.kt:85) + at androidx.compose.runtime.internal.RememberEventDispatcher.dispatchRememberList(RememberEventDispatcher.kt:253) + at androidx.compose.runtime.internal.RememberEventDispatcher.dispatchRememberObservers(RememberEventDispatcher.kt:225) + at androidx.compose.runtime.CompositionImpl.applyChangesInLocked(Composition.kt:1122) + at androidx.compose.runtime.CompositionImpl.applyChanges(Composition.kt:1149) + at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2.invokeSuspend$lambda$22(Recomposer.kt:705) + at androidx.compose.ui.platform.AndroidUiFrameClock$withFrameNanos$2$callback$1.doFrame(AndroidUiFrameClock.android.kt:39) + at androidx.compose.ui.platform.AndroidUiDispatcher.performFrameDispatch(AndroidUiDispatcher.android.kt:108) + at androidx.compose.ui.platform.AndroidUiDispatcher.access$performFrameDispatch(AndroidUiDispatcher.android.kt:41) + at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.doFrame(AndroidUiDispatcher.android.kt:69) + at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1899)"#.trim(), test.unwrap().trim()); +} + +#[test] +fn test_outline_header_parsing_cache() { + let mapping = ProguardMapping::new(MAPPING_OUTLINE); + assert!(mapping.is_valid()); + + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + cache.test(); + + // Test that we can remap the outline class + let class = cache.remap_class("a"); + assert_eq!(class, Some("outline.Class")); + + // Test that we can remap the other class + let class = cache.remap_class("b"); + assert_eq!(class, Some("some.Class")); +} + +#[test] +fn test_outline_frame_retracing_cache() { + let mapping = ProguardMapping::new(MAPPING_OUTLINE); + + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + cache.test(); + + // Test retracing a frame from the outline class + let mut mapped = cache.remap_frame(&StackFrame::new("a", "a", 1)); + + assert_eq!( + mapped.next().unwrap(), + StackFrame::new("outline.Class", "outline", 1) + ); + assert_eq!(mapped.next(), None); + + // Test retracing a frame from the class with outlineCallsite + let mut mapped = cache.remap_frame(&StackFrame::new("b", "s", 27)); + + assert_eq!( + mapped.next().unwrap(), + StackFrame::new("some.Class", "outlineCaller", 0) + ); + assert_eq!(mapped.next(), None); +} + #[test] fn test_outline_header_parsing() { let mapping = ProguardMapping::new(MAPPING_OUTLINE);