diff --git a/src/builder.rs b/src/builder.rs index e8b26b3..52e415b 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -49,6 +49,8 @@ impl std::ops::Deref for OriginalName<'_> { pub(crate) struct ClassInfo<'s> { /// The source file in which the class is defined. pub(crate) source_file: Option<&'s str>, + /// Whether this class was synthesized by the compiler. + pub(crate) is_synthesized: bool, } /// The receiver of a method. @@ -112,7 +114,10 @@ pub(crate) struct MethodKey<'s> { /// Information about a method in a ProGuard file. #[derive(Clone, Copy, Debug, Default)] -pub(crate) struct MethodInfo {} +pub(crate) struct MethodInfo { + /// Whether this method was synthesized by the compiler. + pub(crate) is_synthesized: bool, +} /// A member record in a Proguard file. #[derive(Clone, Copy, Debug)] @@ -167,7 +172,8 @@ impl<'s> ParsedProguardMapping<'s> { ProguardRecord::Field { .. } => {} ProguardRecord::Header { .. } => {} ProguardRecord::R8Header(_) => { - // R8 headers are already handled in the class case below. + // R8 headers can be skipped; they are already + // handled in the branches for `Class` and `Method`. } ProguardRecord::Class { original, @@ -187,8 +193,9 @@ impl<'s> ParsedProguardMapping<'s> { while let Some(ProguardRecord::R8Header(r8_header)) = records.peek() { match r8_header { R8Header::SourceFile { file_name } => { - current_class.source_file = Some(file_name); + current_class.source_file = Some(file_name) } + R8Header::Synthesized => current_class.is_synthesized = true, R8Header::Other => {} } @@ -251,8 +258,17 @@ impl<'s> ParsedProguardMapping<'s> { arguments, }; - // This does nothing for now because we are not saving any per-method information. - let _method_info: &mut MethodInfo = slf.method_infos.entry(method).or_default(); + let method_info: &mut MethodInfo = slf.method_infos.entry(method).or_default(); + + // Consume R8 headers attached to this method. + while let Some(ProguardRecord::R8Header(r8_header)) = records.peek() { + match r8_header { + R8Header::Synthesized => method_info.is_synthesized = true, + R8Header::SourceFile { .. } | R8Header::Other => {} + } + + records.next(); + } let member = Member { method, diff --git a/src/cache/debug.rs b/src/cache/debug.rs index df9e535..34984c0 100644 --- a/src/cache/debug.rs +++ b/src/cache/debug.rs @@ -35,6 +35,7 @@ impl fmt::Debug for ClassDebug<'_, '_> { .field("obfuscated_name", &self.obfuscated_name()) .field("original_name", &self.original_name()) .field("file_name", &self.file_name()) + .field("is_synthesized", &self.raw.is_synthesized()) .finish() } } @@ -105,6 +106,7 @@ impl fmt::Debug for MemberDebug<'_, '_> { .field("original_startline", &self.raw.original_startline) .field("original_endline", &self.original_endline()) .field("params", &self.params()) + .field("is_synthesized", &self.raw.is_synthesized()) .finish() } } diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 8566abd..edaaace 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -13,17 +13,25 @@ //! it is by offset into this section. //! //! ## Class entries -//! A class entry contains an obfuscated and an original name, optionally a file name, -//! and an offset and length for the class's associated records in the `members` -//! and `members_by_params` section, respectively. +//! A class entry contains +//! * an obfuscated and an original name, +//! * optionally a file name, +//! * an offset and length for the class's associated records in the `members` and `members_by_params` section, respectively, +//! * and an `is_synthesized` flag. //! //! Class entries are sorted by obfuscated name. //! //! ## Member entries -//! A member entry always contains an obfuscated and an original method name, a start -//! and end line (1- based and inclusive), and a params string. -//! It may also contain an original class name, -//! original file name, and original start and end line. +//! A member entry always contains +//! * an obfuscated and an original method name, +//! * a start and end line (1- based and inclusive), +//! * a params string, +//! * and an `is_synthesized` flag. +//! +//! It may also contain +//! * an original class name, +//! * an original file name, +//! * and original start and end lines. //! //! Member entries in `members` are sorted by the class they belong to, then by //! obfuscated method name, and finally by the order in which they were encountered @@ -268,7 +276,7 @@ impl<'data> ProguardCache<'data> { /// Finds the range of elements of `members` for which `f(m) == Ordering::Equal`. /// /// This works by first binary searching for any element fitting the criteria - /// and then linearly searching foraward and backward from that one to find + /// and then linearly searching forward and backward from that one to find /// the exact range. /// /// Obviously this only works if the criteria are consistent with the order @@ -495,6 +503,7 @@ fn iterate_with_lines<'a>( file, line, parameters: frame.parameters, + method_synthesized: member.is_synthesized(), }); } None @@ -519,6 +528,7 @@ fn iterate_without_lines<'a>( file: None, line: 0, parameters: frame.parameters, + method_synthesized: member.is_synthesized(), }) } @@ -558,6 +568,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: line: 2, file: Some("SourceFile"), parameters: None, + method_synthesized: false, }, StackFrame { class: "android.view.View", @@ -565,6 +576,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: line: 7393, file: Some("View.java"), parameters: None, + method_synthesized: false, }, ], cause: Some(Box::new(StackTrace { @@ -578,6 +590,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: line: 1, file: Some("SourceFile"), parameters: None, + method_synthesized: false, }], cause: None, })), diff --git a/src/cache/raw.rs b/src/cache/raw.rs index 74286b5..d484624 100644 --- a/src/cache/raw.rs +++ b/src/cache/raw.rs @@ -18,7 +18,7 @@ pub(crate) const PRGCACHE_MAGIC: u32 = u32::from_le_bytes(PRGCACHE_MAGIC_BYTES); /// The byte-flipped magic, which indicates an endianness mismatch. pub(crate) const PRGCACHE_MAGIC_FLIPPED: u32 = PRGCACHE_MAGIC.swap_bytes(); -pub const PRGCACHE_VERSION: u32 = 1; +pub const PRGCACHE_VERSION: u32 = 2; /// The header of a proguard cache file. #[derive(Debug, Clone, PartialEq, Eq)] @@ -56,6 +56,23 @@ pub(crate) struct Class { pub(crate) members_by_params_offset: u32, /// The number of member-by-params entries for this class. pub(crate) members_by_params_len: u32, + /// Whether this class was synthesized by the compiler. + /// + /// `0` means `false`, all other values mean `true`. + /// + /// Note: It's currently unknown what effect a synthesized + /// class has. + pub(crate) is_synthesized: u8, + + /// Reserved space. + pub(crate) _reserved: [u8; 3], +} + +impl Class { + /// Returns true if this class was synthesized by the compiler. + pub(crate) fn is_synthesized(&self) -> bool { + self.is_synthesized != 0 + } } impl Default for Class { @@ -68,6 +85,8 @@ impl Default for Class { members_len: 0, members_by_params_offset: u32::MAX, members_by_params_len: 0, + is_synthesized: 0, + _reserved: [0; 3], } } } @@ -94,6 +113,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, + /// Whether this member was synthesized by the compiler. + /// + /// `0` means `false`, all other values mean `true`. + pub(crate) is_synthesized: u8, + + /// Reserved space. + pub(crate) _reserved: [u8; 3], +} + +impl Member { + /// Returns true if this member was synthesized by the compiler. + pub(crate) fn is_synthesized(&self) -> bool { + self.is_synthesized != 0 + } } unsafe impl Pod for Header {} @@ -198,10 +231,16 @@ impl<'data> ProguardCache<'data> { .map(|(obfuscated, original)| { let obfuscated_name_offset = string_table.insert(obfuscated.as_str()) as u32; let original_name_offset = string_table.insert(original.as_str()) as u32; + let is_synthesized = parsed + .class_infos + .get(original) + .map(|ci| ci.is_synthesized) + .unwrap_or_default(); let class = ClassInProgress { class: Class { original_name_offset, obfuscated_name_offset, + is_synthesized: is_synthesized as u8, ..Default::default() }, ..Default::default() @@ -324,6 +363,13 @@ impl<'data> ProguardCache<'data> { let params_offset = string_table.insert(member.method.arguments) as u32; + let method_info = parsed + .method_infos + .get(&member.method) + .copied() + .unwrap_or_default(); + let is_synthesized = method_info.is_synthesized as u8; + Member { startline: member.startline as u32, endline: member.endline as u32, @@ -334,6 +380,8 @@ impl<'data> ProguardCache<'data> { original_endline: member.original_endline.map_or(u32::MAX, |l| l as u32), obfuscated_name_offset, params_offset, + is_synthesized, + _reserved: [0; 3], } } @@ -342,11 +390,13 @@ impl<'data> ProguardCache<'data> { /// Specifically it checks the following: /// * All string offsets in class and member entries are either `u32::MAX` or defined. /// * Member entries are ordered by the class they belong to. + /// * All `is_synthesized` fields on classes and members are either `0` or `1`. pub fn test(&self) { let mut prev_end = 0; for class in self.classes { assert!(self.read_string(class.obfuscated_name_offset).is_ok()); assert!(self.read_string(class.original_name_offset).is_ok()); + assert!(class.is_synthesized == 0 || class.is_synthesized == 1); if class.file_name_offset != u32::MAX { assert!(self.read_string(class.file_name_offset).is_ok()); @@ -362,6 +412,7 @@ impl<'data> ProguardCache<'data> { for member in members { 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); if member.params_offset != u32::MAX { assert!(self.read_string(member.params_offset).is_ok()); diff --git a/src/mapper.rs b/src/mapper.rs index fae0b5e..79ace5e 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -60,6 +60,7 @@ struct MemberMapping<'s> { original: &'s str, original_startline: usize, original_endline: Option, + is_synthesized: bool, } #[derive(Clone, Debug, Default)] @@ -73,6 +74,11 @@ struct ClassMembers<'s> { struct ClassMapping<'s> { original: &'s str, members: HashMap<&'s str, ClassMembers<'s>>, + #[expect( + unused, + reason = "It is currently unknown what effect a synthesized class has." + )] + is_synthesized: bool, } type MemberIter<'m> = std::slice::Iter<'m, MemberMapping<'m>>; @@ -121,6 +127,7 @@ fn iterate_with_lines<'a>( if member.endline > 0 && (frame.line < member.startline || frame.line > member.endline) { continue; } + // parents of inlined frames don’t have an `endline`, and // the top inlined frame need to be correctly offset. let line = if member.original_endline.is_none() @@ -130,6 +137,7 @@ fn iterate_with_lines<'a>( } else { member.original_startline + frame.line - member.startline }; + let file = if let Some(file_name) = member.original_file { if file_name == "R8$$SyntheticClass" { extract_class_name(member.original_class.unwrap_or(frame.class)) @@ -143,16 +151,19 @@ fn iterate_with_lines<'a>( } else { frame.file }; + let class = match member.original_class { Some(class) => class, _ => frame.class, }; + return Some(StackFrame { class, method: member.original, file, line, parameters: frame.parameters, + method_synthesized: member.is_synthesized, }); } None @@ -174,6 +185,7 @@ fn iterate_without_lines<'a>( file: None, line: 0, parameters: frame.parameters, + method_synthesized: member.is_synthesized, }) } @@ -229,10 +241,16 @@ impl<'s> ProguardMapper<'s> { .class_names .iter() .map(|(obfuscated, original)| { + let is_synthesized = parsed + .class_infos + .get(original) + .map(|ci| ci.is_synthesized) + .unwrap_or_default(); ( obfuscated.as_str(), ClassMapping { original: original.as_str(), + is_synthesized, ..Default::default() }, ) @@ -282,6 +300,13 @@ impl<'s> ProguardMapper<'s> { MethodReceiver::OtherClass(original_class_name) => Some(original_class_name.as_str()), }; + let method_info = parsed + .method_infos + .get(&member.method) + .copied() + .unwrap_or_default(); + let is_synthesized = method_info.is_synthesized; + MemberMapping { startline: member.startline, endline: member.endline, @@ -290,6 +315,7 @@ impl<'s> ProguardMapper<'s> { original: member.method.name.as_str(), original_startline: member.original_startline, original_endline: member.original_endline, + is_synthesized, } } @@ -346,6 +372,7 @@ impl<'s> ProguardMapper<'s> { let Some(class) = self.classes.get(frame.class) else { return RemappedFrameIter::empty(); }; + let Some(members) = class.members.get(frame.method) else { return RemappedFrameIter::empty(); }; @@ -530,6 +557,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: line: 2, file: Some("SourceFile"), parameters: None, + method_synthesized: false, }, StackFrame { class: "android.view.View", @@ -537,6 +565,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: line: 7393, file: Some("View.java"), parameters: None, + method_synthesized: false, }, ], cause: Some(Box::new(StackTrace { @@ -550,6 +579,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: line: 1, file: Some("SourceFile"), parameters: None, + method_synthesized: false, }], cause: None, })), diff --git a/src/mapping.rs b/src/mapping.rs index 1da08b3..27063dd 100644 --- a/src/mapping.rs +++ b/src/mapping.rs @@ -310,6 +310,13 @@ pub enum R8Header<'s> { #[serde(rename_all = "camelCase")] SourceFile { file_name: &'s str }, + /// A synthesized header, stating that the class or method it's attached to + /// was synthesized by the compiler. + /// + /// See . + #[serde(rename = "com.android.tools.r8.synthesized")] + Synthesized, + /// Catchall variant for headers we don't support. #[serde(other)] Other, @@ -817,6 +824,15 @@ mod tests { ); } + #[test] + fn try_parse_header_synthesized() { + let bytes = br#"# {"id":"com.android.tools.r8.synthesized"}"#; + assert_eq!( + ProguardRecord::try_parse(bytes).unwrap(), + ProguardRecord::R8Header(R8Header::Synthesized) + ); + } + #[test] fn try_parse_class() { let bytes = b"android.support.v4.app.RemoteActionCompatParcelizer -> android.support.v4.app.RemoteActionCompatParcelizer:"; @@ -1104,7 +1120,7 @@ androidx.activity.OnBackPressedCallback original_endline: Some(187), }), }), - Ok(ProguardRecord::R8Header(R8Header::Other)), + Ok(ProguardRecord::R8Header(R8Header::Synthesized)), Err(ParseError { line: b"androidx.activity.OnBackPressedCallback \n", kind: ParseErrorKind::ParseError("line is not a valid proguard record"), diff --git a/src/stacktrace.rs b/src/stacktrace.rs index 25940ea..1e61375 100644 --- a/src/stacktrace.rs +++ b/src/stacktrace.rs @@ -158,6 +158,7 @@ pub struct StackFrame<'s> { pub(crate) line: usize, pub(crate) file: Option<&'s str>, pub(crate) parameters: Option<&'s str>, + pub(crate) method_synthesized: bool, } impl<'s> StackFrame<'s> { @@ -169,6 +170,7 @@ impl<'s> StackFrame<'s> { line, file: None, parameters: None, + method_synthesized: false, } } @@ -180,6 +182,7 @@ impl<'s> StackFrame<'s> { line, file: Some(file), parameters: None, + method_synthesized: false, } } @@ -192,9 +195,16 @@ impl<'s> StackFrame<'s> { line: 0, file: None, parameters: Some(arguments), + method_synthesized: false, } } + /// Flags `self`'s method as being synthesized by the compiler according to `is_synthesized`. + pub fn with_method_synthesized(mut self, is_synthesized: bool) -> Self { + self.method_synthesized = is_synthesized; + self + } + /// Parses a StackFrame from a line of a Java StackTrace. /// /// # Examples @@ -247,6 +257,11 @@ impl<'s> StackFrame<'s> { pub fn parameters(&self) -> Option<&str> { self.parameters } + + /// Returns whether this frame's method was synthesized by the compiler. + pub fn method_synthesized(&self) -> bool { + self.method_synthesized + } } impl Display for StackFrame<'_> { @@ -283,6 +298,7 @@ pub(crate) fn parse_frame(line: &str) -> Option { file: Some(file), line, parameters: None, + method_synthesized: false, }) } @@ -390,6 +406,7 @@ mod tests { line: 5, file: Some("Util.java"), parameters: None, + method_synthesized: false, }], cause: Some(Box::new(StackTrace { exception: Some(Throwable { @@ -402,6 +419,7 @@ mod tests { line: 115, file: None, parameters: None, + method_synthesized: false, }], cause: None, })), @@ -425,6 +443,7 @@ Caused by: com.example.Other: Invalid data line: 1, file: Some("SourceFile"), parameters: None, + method_synthesized: false, }); assert_eq!(expect, stack_frame); @@ -448,6 +467,7 @@ Caused by: com.example.Other: Invalid data line: 1, file: None, parameters: None, + method_synthesized: false, }; assert_eq!( @@ -461,6 +481,7 @@ Caused by: com.example.Other: Invalid data line: 1, file: Some("SourceFile"), parameters: None, + method_synthesized: false, }; assert_eq!( diff --git a/tests/callback.rs b/tests/callback.rs index ef349a2..36d3d8b 100644 --- a/tests/callback.rs +++ b/tests/callback.rs @@ -1,11 +1,11 @@ -use proguard::{ProguardMapper, ProguardMapping, StackFrame}; +use proguard::{ProguardCache, ProguardMapper, ProguardMapping, StackFrame}; static MAPPING_CALLBACK: &[u8] = include_bytes!("res/mapping-callback.txt"); static MAPPING_CALLBACK_EXTRA_CLASS: &[u8] = include_bytes!("res/mapping-callback-extra-class.txt"); static MAPPING_CALLBACK_INNER_CLASS: &[u8] = include_bytes!("res/mapping-callback-inner-class.txt"); #[test] -fn test_method_matches_callback() { +fn test_method_matches_callback_mapper() { // see the following files for sources used when creating the mapping file: // - res/mapping-callback_EditActivity.kt let mapper = ProguardMapper::new(ProguardMapping::new(MAPPING_CALLBACK)); @@ -31,12 +31,51 @@ fn test_method_matches_callback() { "onMenuItemClick", 0, ) + .with_method_synthesized(true) ); assert_eq!(mapped.next(), None); } #[test] -fn test_method_matches_callback_extra_class() { +fn test_method_matches_callback_cache() { + // see the following files for sources used when creating the mapping file: + // - res/mapping-callback_EditActivity.kt + let mapping = ProguardMapping::new(MAPPING_CALLBACK); + let mut cache = Vec::new(); + ProguardCache::write(&mapping, &mut cache).unwrap(); + + let cache = ProguardCache::parse(&cache).unwrap(); + + cache.test(); + + let mut mapped = cache.remap_frame(&StackFrame::new( + "io.sentry.samples.instrumentation.ui.g", + "onMenuItemClick", + 28, + )); + + assert_eq!( + mapped.next().unwrap(), + StackFrame::new( + "io.sentry.samples.instrumentation.ui.EditActivity", + "onCreate$lambda$1", + 37, + ) + ); + assert_eq!( + mapped.next().unwrap(), + StackFrame::new( + "io.sentry.samples.instrumentation.ui.EditActivity$$InternalSyntheticLambda$1$ebaa538726b99bb77e0f5e7c86443911af17d6e5be2b8771952ae0caa4ff2ac7$0", + "onMenuItemClick", + 0, + ) + .with_method_synthesized(true) + ); + assert_eq!(mapped.next(), None); +} + +#[test] +fn test_method_matches_callback_extra_class_mapper() { // see the following files for sources used when creating the mapping file: // - res/mapping-callback-extra-class_EditActivity.kt // - res/mapping-callback-extra-class_TestSourceContext.kt @@ -79,12 +118,68 @@ fn test_method_matches_callback_extra_class() { "onMenuItemClick", 0, ) + .with_method_synthesized(true) + ); + assert_eq!(mapped.next(), None); +} + +#[test] +fn test_method_matches_callback_extra_class_cache() { + // see the following files for sources used when creating the mapping file: + // - res/mapping-callback-extra-class_EditActivity.kt + // - res/mapping-callback-extra-class_TestSourceContext.kt + let mapping = ProguardMapping::new(MAPPING_CALLBACK_EXTRA_CLASS); + let mut cache = Vec::new(); + ProguardCache::write(&mapping, &mut cache).unwrap(); + + let cache = ProguardCache::parse(&cache).unwrap(); + + cache.test(); + + let mut mapped = cache.remap_frame(&StackFrame::new( + "io.sentry.samples.instrumentation.ui.g", + "onMenuItemClick", + 28, + )); + + assert_eq!( + mapped.next().unwrap(), + StackFrame::new( + "io.sentry.samples.instrumentation.ui.TestSourceContext", + "test2", + 10, + ) + ); + assert_eq!( + mapped.next().unwrap(), + StackFrame::new( + "io.sentry.samples.instrumentation.ui.TestSourceContext", + "test", + 6, + ) + ); + assert_eq!( + mapped.next().unwrap(), + StackFrame::new( + "io.sentry.samples.instrumentation.ui.EditActivity", + "onCreate$lambda$1", + 38, + ) + ); + assert_eq!( + mapped.next().unwrap(), + StackFrame::new( + "io.sentry.samples.instrumentation.ui.EditActivity$$InternalSyntheticLambda$1$ebaa538726b99bb77e0f5e7c86443911af17d6e5be2b8771952ae0caa4ff2ac7$0", + "onMenuItemClick", + 0, + ) + .with_method_synthesized(true) ); assert_eq!(mapped.next(), None); } #[test] -fn test_method_matches_callback_inner_class() { +fn test_method_matches_callback_inner_class_mapper() { // see the following files for sources used when creating the mapping file: // - res/mapping-callback-inner-class_EditActivity.kt let mapper = ProguardMapper::new(ProguardMapping::new(MAPPING_CALLBACK_INNER_CLASS)); @@ -119,6 +214,54 @@ fn test_method_matches_callback_inner_class() { "onMenuItemClick", 0, ) + .with_method_synthesized(true) + ); + assert_eq!(mapped.next(), None); +} + +#[test] +fn test_method_matches_callback_inner_class_cache() { + // see the following files for sources used when creating the mapping file: + // - res/mapping-callback-inner-class_EditActivity.kt + let mapping = ProguardMapping::new(MAPPING_CALLBACK_INNER_CLASS); + let mut cache = Vec::new(); + ProguardCache::write(&mapping, &mut cache).unwrap(); + + let cache = ProguardCache::parse(&cache).unwrap(); + + cache.test(); + + let mut mapped = cache.remap_frame(&StackFrame::new( + "io.sentry.samples.instrumentation.ui.g", + "onMenuItemClick", + 28, + )); + + assert_eq!( + mapped.next().unwrap(), + StackFrame::with_file( + "io.sentry.samples.instrumentation.ui.EditActivity$InnerEditActivityClass", + "testInner", + 19, + "EditActivity.kt", + ) + ); + assert_eq!( + mapped.next().unwrap(), + StackFrame::new( + "io.sentry.samples.instrumentation.ui.EditActivity", + "onCreate$lambda$1", + 45, + ) + ); + assert_eq!( + mapped.next().unwrap(), + StackFrame::new( + "io.sentry.samples.instrumentation.ui.EditActivity$$InternalSyntheticLambda$1$ebaa538726b99bb77e0f5e7c86443911af17d6e5be2b8771952ae0caa4ff2ac7$0", + "onMenuItemClick", + 0, + ) + .with_method_synthesized(true) ); assert_eq!(mapped.next(), None); }