From 952ff9956549000705a34b6e6cdc1b0f61c3cdb9 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Wed, 25 Jun 2025 11:31:20 +0200 Subject: [PATCH 01/10] Parse synthesized headers --- src/mapping.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/mapping.rs b/src/mapping.rs index 1da08b3..31f82e6 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,13 @@ mod tests { ); } + #[test] + fn try_parse_header_synthesized() { + let bytes = br#"# {"id":"com.android.tools.r8.synthesized"}"#; + let parsed = ProguardRecord::try_parse(bytes); + assert_eq!(parsed, Ok(ProguardRecord::R8Header(R8Header::Synthesized))); + } + #[test] fn try_parse_class() { let bytes = b"android.support.v4.app.RemoteActionCompatParcelizer -> android.support.v4.app.RemoteActionCompatParcelizer:"; From d6dcdab00a3b45a0abec4158b41ea166057135ee Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Wed, 25 Jun 2025 16:52:37 +0200 Subject: [PATCH 02/10] Handle synthesized in mapper --- src/builder.rs | 26 +++++++++++++++++++++----- src/cache/mod.rs | 7 +++++++ src/mapper.rs | 43 ++++++++++++++++++++++++++++++++++++++----- src/mapping.rs | 8 +++++--- src/stacktrace.rs | 19 +++++++++++++++++++ tests/callback.rs | 10 ++++++++++ 6 files changed, 100 insertions(+), 13 deletions(-) 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/mod.rs b/src/cache/mod.rs index 8566abd..09b8edc 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -495,6 +495,8 @@ fn iterate_with_lines<'a>( file, line, parameters: frame.parameters, + // TODO + is_synthesized: false, }); } None @@ -519,6 +521,8 @@ fn iterate_without_lines<'a>( file: None, line: 0, parameters: frame.parameters, + // TODO + is_synthesized: false, }) } @@ -558,6 +562,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: line: 2, file: Some("SourceFile"), parameters: None, + is_synthesized: false, }, StackFrame { class: "android.view.View", @@ -565,6 +570,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: line: 7393, file: Some("View.java"), parameters: None, + is_synthesized: false, }, ], cause: Some(Box::new(StackTrace { @@ -578,6 +584,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: line: 1, file: Some("SourceFile"), parameters: None, + is_synthesized: false, }], cause: None, })), diff --git a/src/mapper.rs b/src/mapper.rs index fae0b5e..a7b5389 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,7 @@ struct ClassMembers<'s> { struct ClassMapping<'s> { original: &'s str, members: HashMap<&'s str, ClassMembers<'s>>, + is_synthesized: bool, } type MemberIter<'m> = std::slice::Iter<'m, MemberMapping<'m>>; @@ -81,15 +83,20 @@ type MemberIter<'m> = std::slice::Iter<'m, MemberMapping<'m>>; #[derive(Clone, Debug, Default)] pub struct RemappedFrameIter<'m> { inner: Option<(StackFrame<'m>, MemberIter<'m>)>, + synthesized_class: bool, } impl<'m> RemappedFrameIter<'m> { fn empty() -> Self { - Self { inner: None } + Self { + inner: None, + synthesized_class: false, + } } - fn members(frame: StackFrame<'m>, members: MemberIter<'m>) -> Self { + fn members(frame: StackFrame<'m>, members: MemberIter<'m>, synthesized_class: bool) -> Self { Self { inner: Some((frame, members)), + synthesized_class, } } } @@ -99,9 +106,9 @@ impl<'m> Iterator for RemappedFrameIter<'m> { fn next(&mut self) -> Option { let (frame, ref mut members) = self.inner.as_mut()?; if frame.parameters.is_none() { - iterate_with_lines(frame, members) + iterate_with_lines(frame, members, self.synthesized_class) } else { - iterate_without_lines(frame, members) + iterate_without_lines(frame, members, self.synthesized_class) } } } @@ -115,12 +122,14 @@ fn extract_class_name(full_path: &str) -> Option<&str> { fn iterate_with_lines<'a>( frame: &mut StackFrame<'a>, members: &mut core::slice::Iter<'_, MemberMapping<'a>>, + synthesized_class: bool, ) -> Option> { for member in members { // skip any members which do not match our frames line 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 +139,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 +153,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, + is_synthesized: member.is_synthesized || synthesized_class, }); } None @@ -161,6 +174,7 @@ fn iterate_with_lines<'a>( fn iterate_without_lines<'a>( frame: &mut StackFrame<'a>, members: &mut core::slice::Iter<'_, MemberMapping<'a>>, + synthesized_class: bool, ) -> Option> { let member = members.next()?; @@ -174,6 +188,7 @@ fn iterate_without_lines<'a>( file: None, line: 0, parameters: frame.parameters, + is_synthesized: member.is_synthesized || synthesized_class, }) } @@ -229,10 +244,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 +303,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 +318,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 +375,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(); }; @@ -363,7 +393,7 @@ impl<'s> ProguardMapper<'s> { members.all_mappings.iter() }; - RemappedFrameIter::members(frame, mappings) + RemappedFrameIter::members(frame, mappings, class.is_synthesized) } /// Remaps a throwable which is the first line of a full stacktrace. @@ -530,6 +560,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: line: 2, file: Some("SourceFile"), parameters: None, + is_synthesized: false, }, StackFrame { class: "android.view.View", @@ -537,6 +568,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: line: 7393, file: Some("View.java"), parameters: None, + is_synthesized: false, }, ], cause: Some(Box::new(StackTrace { @@ -550,6 +582,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: line: 1, file: Some("SourceFile"), parameters: None, + is_synthesized: false, }], cause: None, })), diff --git a/src/mapping.rs b/src/mapping.rs index 31f82e6..27063dd 100644 --- a/src/mapping.rs +++ b/src/mapping.rs @@ -827,8 +827,10 @@ mod tests { #[test] fn try_parse_header_synthesized() { let bytes = br#"# {"id":"com.android.tools.r8.synthesized"}"#; - let parsed = ProguardRecord::try_parse(bytes); - assert_eq!(parsed, Ok(ProguardRecord::R8Header(R8Header::Synthesized))); + assert_eq!( + ProguardRecord::try_parse(bytes).unwrap(), + ProguardRecord::R8Header(R8Header::Synthesized) + ); } #[test] @@ -1118,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..7d319d4 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) is_synthesized: bool, } impl<'s> StackFrame<'s> { @@ -169,6 +170,7 @@ impl<'s> StackFrame<'s> { line, file: None, parameters: None, + is_synthesized: false, } } @@ -180,6 +182,7 @@ impl<'s> StackFrame<'s> { line, file: Some(file), parameters: None, + is_synthesized: false, } } @@ -192,9 +195,15 @@ impl<'s> StackFrame<'s> { line: 0, file: None, parameters: Some(arguments), + is_synthesized: false, } } + pub fn with_synthesized(mut self, is_synthesized: bool) -> Self { + self.is_synthesized = is_synthesized; + self + } + /// Parses a StackFrame from a line of a Java StackTrace. /// /// # Examples @@ -247,6 +256,10 @@ impl<'s> StackFrame<'s> { pub fn parameters(&self) -> Option<&str> { self.parameters } + + pub fn is_synthesized(&self) -> bool { + self.is_synthesized + } } impl Display for StackFrame<'_> { @@ -283,6 +296,7 @@ pub(crate) fn parse_frame(line: &str) -> Option { file: Some(file), line, parameters: None, + is_synthesized: false, }) } @@ -390,6 +404,7 @@ mod tests { line: 5, file: Some("Util.java"), parameters: None, + is_synthesized: false, }], cause: Some(Box::new(StackTrace { exception: Some(Throwable { @@ -402,6 +417,7 @@ mod tests { line: 115, file: None, parameters: None, + is_synthesized: false, }], cause: None, })), @@ -425,6 +441,7 @@ Caused by: com.example.Other: Invalid data line: 1, file: Some("SourceFile"), parameters: None, + is_synthesized: false, }); assert_eq!(expect, stack_frame); @@ -448,6 +465,7 @@ Caused by: com.example.Other: Invalid data line: 1, file: None, parameters: None, + is_synthesized: false, }; assert_eq!( @@ -461,6 +479,7 @@ Caused by: com.example.Other: Invalid data line: 1, file: Some("SourceFile"), parameters: None, + is_synthesized: false, }; assert_eq!( diff --git a/tests/callback.rs b/tests/callback.rs index ef349a2..d9a4db4 100644 --- a/tests/callback.rs +++ b/tests/callback.rs @@ -16,6 +16,7 @@ fn test_method_matches_callback() { 28, )); + // The remapped frames should all be synthesized because the class is. assert_eq!( mapped.next().unwrap(), StackFrame::new( @@ -23,6 +24,7 @@ fn test_method_matches_callback() { "onCreate$lambda$1", 37, ) + .with_synthesized(true) ); assert_eq!( mapped.next().unwrap(), @@ -31,6 +33,7 @@ fn test_method_matches_callback() { "onMenuItemClick", 0, ) + .with_synthesized(true) ); assert_eq!(mapped.next(), None); } @@ -55,6 +58,7 @@ fn test_method_matches_callback_extra_class() { "test2", 10, ) + .with_synthesized(true) ); assert_eq!( mapped.next().unwrap(), @@ -63,6 +67,7 @@ fn test_method_matches_callback_extra_class() { "test", 6, ) + .with_synthesized(true) ); assert_eq!( mapped.next().unwrap(), @@ -71,6 +76,7 @@ fn test_method_matches_callback_extra_class() { "onCreate$lambda$1", 38, ) + .with_synthesized(true) ); assert_eq!( mapped.next().unwrap(), @@ -79,6 +85,7 @@ fn test_method_matches_callback_extra_class() { "onMenuItemClick", 0, ) + .with_synthesized(true) ); assert_eq!(mapped.next(), None); } @@ -103,6 +110,7 @@ fn test_method_matches_callback_inner_class() { 19, "EditActivity.kt", ) + .with_synthesized(true) ); assert_eq!( mapped.next().unwrap(), @@ -111,6 +119,7 @@ fn test_method_matches_callback_inner_class() { "onCreate$lambda$1", 45, ) + .with_synthesized(true) ); assert_eq!( mapped.next().unwrap(), @@ -119,6 +128,7 @@ fn test_method_matches_callback_inner_class() { "onMenuItemClick", 0, ) + .with_synthesized(true) ); assert_eq!(mapped.next(), None); } From 854106047a245ebd97e129a2f9f8f584374c25bc Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Thu, 26 Jun 2025 14:05:31 +0200 Subject: [PATCH 03/10] Implement synthesized annotations in cache --- src/cache/debug.rs | 2 ++ src/cache/mod.rs | 48 ++++++++++++++++++++++++++++++---------------- src/cache/raw.rs | 44 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 18 deletions(-) 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 09b8edc..b86dc1a 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 @@ -243,7 +251,7 @@ impl<'data> ProguardCache<'data> { }) else { return RemappedFrameIter::empty(); }; - RemappedFrameIter::members(self, frame, members.iter()) + RemappedFrameIter::members(self, frame, members.iter(), class.is_synthesized()) } else { let Some(members) = self.get_class_members(class) else { return RemappedFrameIter::empty(); @@ -261,14 +269,14 @@ impl<'data> ProguardCache<'data> { return RemappedFrameIter::empty(); }; - RemappedFrameIter::members(self, frame, members.iter()) + RemappedFrameIter::members(self, frame, members.iter(), class.is_synthesized()) } } /// 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 @@ -410,20 +418,26 @@ pub struct RemappedFrameIter<'r, 'data> { StackFrame<'data>, std::slice::Iter<'data, raw::Member>, )>, + synthesized_class: bool, } impl<'data> RemappedFrameIter<'_, 'data> { fn empty() -> Self { - Self { inner: None } + Self { + inner: None, + synthesized_class: false, + } } fn members( cache: &'data ProguardCache<'data>, frame: StackFrame<'data>, members: std::slice::Iter<'data, raw::Member>, + synthesized_class: bool, ) -> Self { Self { inner: Some((cache, frame, members)), + synthesized_class, } } } @@ -434,9 +448,9 @@ impl<'data> Iterator for RemappedFrameIter<'_, 'data> { fn next(&mut self) -> Option { let (cache, frame, members) = self.inner.as_mut()?; if frame.parameters.is_none() { - iterate_with_lines(cache, frame, members) + iterate_with_lines(cache, frame, members, self.synthesized_class) } else { - iterate_without_lines(cache, frame, members) + iterate_without_lines(cache, frame, members, self.synthesized_class) } } } @@ -445,6 +459,7 @@ fn iterate_with_lines<'a>( cache: &ProguardCache<'a>, frame: &mut StackFrame<'a>, members: &mut std::slice::Iter<'_, raw::Member>, + synthesized_class: bool, ) -> Option> { for member in members { // skip any members which do not match our frames line @@ -495,8 +510,7 @@ fn iterate_with_lines<'a>( file, line, parameters: frame.parameters, - // TODO - is_synthesized: false, + is_synthesized: (member.is_synthesized != false as u32) || synthesized_class, }); } None @@ -506,6 +520,7 @@ fn iterate_without_lines<'a>( cache: &ProguardCache<'a>, frame: &mut StackFrame<'a>, members: &mut std::slice::Iter<'_, raw::Member>, + synthesized_class: bool, ) -> Option> { let member = members.next()?; @@ -521,8 +536,7 @@ fn iterate_without_lines<'a>( file: None, line: 0, parameters: frame.parameters, - // TODO - is_synthesized: false, + is_synthesized: (member.is_synthesized != false as u32) || synthesized_class, }) } diff --git a/src/cache/raw.rs b/src/cache/raw.rs index 74286b5..f40dfe9 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,17 @@ 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`. + pub(crate) is_synthesized: u32, +} + +impl Class { + /// Returns true if this class was synthesized by the compiler. + pub(crate) fn is_synthesized(&self) -> bool { + self.is_synthesized != false as u32 + } } impl Default for Class { @@ -68,6 +79,7 @@ impl Default for Class { members_len: 0, members_by_params_offset: u32::MAX, members_by_params_len: 0, + is_synthesized: false as u32, } } } @@ -94,6 +106,17 @@ 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: u32, +} + +impl Member { + /// Returns true if this member was synthesized by the compiler. + pub(crate) fn is_synthesized(&self) -> bool { + self.is_synthesized != false as u32 + } } unsafe impl Pod for Header {} @@ -198,10 +221,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 u32, ..Default::default() }, ..Default::default() @@ -324,6 +353,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 u32; + Member { startline: member.startline as u32, endline: member.endline as u32, @@ -334,6 +370,7 @@ impl<'data> ProguardCache<'data> { original_endline: member.original_endline.map_or(u32::MAX, |l| l as u32), obfuscated_name_offset, params_offset, + is_synthesized, } } @@ -342,11 +379,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 == false as u32 || class.is_synthesized == true as u32); if class.file_name_offset != u32::MAX { assert!(self.read_string(class.file_name_offset).is_ok()); @@ -362,6 +401,9 @@ 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 == false as u32 || member.is_synthesized == true as u32 + ); if member.params_offset != u32::MAX { assert!(self.read_string(member.params_offset).is_ok()); From e5aeec10984618e1b343d824f71403c3160dde1d Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Thu, 26 Jun 2025 14:07:03 +0200 Subject: [PATCH 04/10] Docs --- src/cache/mod.rs | 4 ++-- src/stacktrace.rs | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index b86dc1a..05b041f 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -510,7 +510,7 @@ fn iterate_with_lines<'a>( file, line, parameters: frame.parameters, - is_synthesized: (member.is_synthesized != false as u32) || synthesized_class, + is_synthesized: member.is_synthesized() || synthesized_class, }); } None @@ -536,7 +536,7 @@ fn iterate_without_lines<'a>( file: None, line: 0, parameters: frame.parameters, - is_synthesized: (member.is_synthesized != false as u32) || synthesized_class, + is_synthesized: member.is_synthesized() || synthesized_class, }) } diff --git a/src/stacktrace.rs b/src/stacktrace.rs index 7d319d4..581cae1 100644 --- a/src/stacktrace.rs +++ b/src/stacktrace.rs @@ -199,6 +199,7 @@ impl<'s> StackFrame<'s> { } } + /// Flags `self` as being synthesized by the compiler according to `is_synthesized`. pub fn with_synthesized(mut self, is_synthesized: bool) -> Self { self.is_synthesized = is_synthesized; self @@ -257,6 +258,7 @@ impl<'s> StackFrame<'s> { self.parameters } + /// Returns whether this frame was synthesized by the compiler. pub fn is_synthesized(&self) -> bool { self.is_synthesized } From 32b57f7bf4f51bb467a6852d9f49d699466249d1 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Thu, 26 Jun 2025 14:33:48 +0200 Subject: [PATCH 05/10] Add cache tests to callback.rs --- tests/callback.rs | 167 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 163 insertions(+), 4 deletions(-) diff --git a/tests/callback.rs b/tests/callback.rs index d9a4db4..42fe5ca 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)); @@ -39,7 +39,49 @@ fn test_method_matches_callback() { } #[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, + )); + + // The remapped frames should all be synthesized because the class is. + assert_eq!( + mapped.next().unwrap(), + StackFrame::with_file( + "io.sentry.samples.instrumentation.ui.EditActivity", + "onCreate$lambda$1", + 37, + "EditActivity", + ) + .with_synthesized(true) + ); + assert_eq!( + mapped.next().unwrap(), + StackFrame::with_file( + "io.sentry.samples.instrumentation.ui.EditActivity$$InternalSyntheticLambda$1$ebaa538726b99bb77e0f5e7c86443911af17d6e5be2b8771952ae0caa4ff2ac7$0", + "onMenuItemClick", + 0, + "EditActivity", + ) + .with_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 @@ -51,6 +93,7 @@ fn test_method_matches_callback_extra_class() { 28, )); + // The remapped frames should all be synthesized because the class is. assert_eq!( mapped.next().unwrap(), StackFrame::new( @@ -91,7 +134,70 @@ fn test_method_matches_callback_extra_class() { } #[test] -fn test_method_matches_callback_inner_class() { +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, + )); + + // The remapped frames should all be synthesized because the class is. + assert_eq!( + mapped.next().unwrap(), + StackFrame::with_file( + "io.sentry.samples.instrumentation.ui.TestSourceContext", + "test2", + 10, + "TestSourceContext", + ) + .with_synthesized(true) + ); + assert_eq!( + mapped.next().unwrap(), + StackFrame::with_file( + "io.sentry.samples.instrumentation.ui.TestSourceContext", + "test", + 6, + "TestSourceContext", + ) + .with_synthesized(true) + ); + assert_eq!( + mapped.next().unwrap(), + StackFrame::with_file( + "io.sentry.samples.instrumentation.ui.EditActivity", + "onCreate$lambda$1", + 38, + "EditActivity", + ) + .with_synthesized(true) + ); + assert_eq!( + mapped.next().unwrap(), + StackFrame::with_file( + "io.sentry.samples.instrumentation.ui.EditActivity$$InternalSyntheticLambda$1$ebaa538726b99bb77e0f5e7c86443911af17d6e5be2b8771952ae0caa4ff2ac7$0", + "onMenuItemClick", + 0, + "EditActivity", + ) + .with_synthesized(true) + ); + assert_eq!(mapped.next(), None); +} + +#[test] +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)); @@ -102,6 +208,59 @@ fn test_method_matches_callback_inner_class() { 28, )); + // The remapped frames should all be synthesized because the class is. + assert_eq!( + mapped.next().unwrap(), + StackFrame::with_file( + "io.sentry.samples.instrumentation.ui.EditActivity$InnerEditActivityClass", + "testInner", + 19, + "EditActivity", + ) + .with_synthesized(true) + ); + assert_eq!( + mapped.next().unwrap(), + StackFrame::with_file( + "io.sentry.samples.instrumentation.ui.EditActivity", + "onCreate$lambda$1", + 45, + "EditActivity", + ) + .with_synthesized(true) + ); + assert_eq!( + mapped.next().unwrap(), + StackFrame::with_file( + "io.sentry.samples.instrumentation.ui.EditActivity$$InternalSyntheticLambda$1$ebaa538726b99bb77e0f5e7c86443911af17d6e5be2b8771952ae0caa4ff2ac7$0", + "onMenuItemClick", + 0, + "EditActivity", + ) + .with_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, + )); + + // The remapped frames should all be synthesized because the class is. assert_eq!( mapped.next().unwrap(), StackFrame::with_file( From 6fa5e8c30956d8f0f7f86cdf9ffa2a5b28684cb0 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Fri, 27 Jun 2025 10:11:36 +0200 Subject: [PATCH 06/10] cache: Simplify boolean flags --- src/cache/raw.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/cache/raw.rs b/src/cache/raw.rs index f40dfe9..ea9d4a7 100644 --- a/src/cache/raw.rs +++ b/src/cache/raw.rs @@ -65,7 +65,7 @@ pub(crate) struct Class { impl Class { /// Returns true if this class was synthesized by the compiler. pub(crate) fn is_synthesized(&self) -> bool { - self.is_synthesized != false as u32 + self.is_synthesized != 0 } } @@ -79,7 +79,7 @@ impl Default for Class { members_len: 0, members_by_params_offset: u32::MAX, members_by_params_len: 0, - is_synthesized: false as u32, + is_synthesized: 0, } } } @@ -115,7 +115,7 @@ pub(crate) struct Member { impl Member { /// Returns true if this member was synthesized by the compiler. pub(crate) fn is_synthesized(&self) -> bool { - self.is_synthesized != false as u32 + self.is_synthesized != 0 } } @@ -385,7 +385,7 @@ impl<'data> ProguardCache<'data> { 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 == false as u32 || class.is_synthesized == true as u32); + 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()); @@ -401,9 +401,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 == false as u32 || member.is_synthesized == true as u32 - ); + assert!(member.is_synthesized == 0 || member.is_synthesized == 1); if member.params_offset != u32::MAX { assert!(self.read_string(member.params_offset).is_ok()); From 63dc59b9ab06d7c9a6b7fbc0de9b9622efd3cc1c Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Mon, 30 Jun 2025 12:59:34 +0200 Subject: [PATCH 07/10] cache: Use u8 for is_synthesized fields --- src/cache/raw.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/cache/raw.rs b/src/cache/raw.rs index ea9d4a7..4b6fc96 100644 --- a/src/cache/raw.rs +++ b/src/cache/raw.rs @@ -59,7 +59,10 @@ pub(crate) struct Class { /// Whether this class was synthesized by the compiler. /// /// `0` means `false`, all other values mean `true`. - pub(crate) is_synthesized: u32, + pub(crate) is_synthesized: u8, + + /// Reserved space. + pub(crate) _reserved: [u8; 3], } impl Class { @@ -80,6 +83,7 @@ impl Default for Class { members_by_params_offset: u32::MAX, members_by_params_len: 0, is_synthesized: 0, + _reserved: [0; 3], } } } @@ -109,7 +113,10 @@ pub(crate) struct Member { /// Whether this member was synthesized by the compiler. /// /// `0` means `false`, all other values mean `true`. - pub(crate) is_synthesized: u32, + pub(crate) is_synthesized: u8, + + /// Reserved space. + pub(crate) _reserved: [u8; 3], } impl Member { @@ -230,7 +237,7 @@ impl<'data> ProguardCache<'data> { class: Class { original_name_offset, obfuscated_name_offset, - is_synthesized: is_synthesized as u32, + is_synthesized: is_synthesized as u8, ..Default::default() }, ..Default::default() @@ -358,7 +365,7 @@ impl<'data> ProguardCache<'data> { .get(&member.method) .copied() .unwrap_or_default(); - let is_synthesized = method_info.is_synthesized as u32; + let is_synthesized = method_info.is_synthesized as u8; Member { startline: member.startline as u32, @@ -371,6 +378,7 @@ impl<'data> ProguardCache<'data> { obfuscated_name_offset, params_offset, is_synthesized, + _reserved: [0; 3], } } From 2021c9ced77e5ee1e5254b5ba06bf99bbf100b85 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Mon, 30 Jun 2025 13:24:23 +0200 Subject: [PATCH 08/10] Don't consider all methods of synthesized classes synthesized --- src/cache/mod.rs | 22 +++++++--------------- src/cache/raw.rs | 3 +++ src/mapper.rs | 22 ++++++++-------------- tests/callback.rs | 18 ------------------ 4 files changed, 18 insertions(+), 47 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 05b041f..8b9fb42 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -251,7 +251,7 @@ impl<'data> ProguardCache<'data> { }) else { return RemappedFrameIter::empty(); }; - RemappedFrameIter::members(self, frame, members.iter(), class.is_synthesized()) + RemappedFrameIter::members(self, frame, members.iter()) } else { let Some(members) = self.get_class_members(class) else { return RemappedFrameIter::empty(); @@ -269,7 +269,7 @@ impl<'data> ProguardCache<'data> { return RemappedFrameIter::empty(); }; - RemappedFrameIter::members(self, frame, members.iter(), class.is_synthesized()) + RemappedFrameIter::members(self, frame, members.iter()) } } @@ -418,26 +418,20 @@ pub struct RemappedFrameIter<'r, 'data> { StackFrame<'data>, std::slice::Iter<'data, raw::Member>, )>, - synthesized_class: bool, } impl<'data> RemappedFrameIter<'_, 'data> { fn empty() -> Self { - Self { - inner: None, - synthesized_class: false, - } + Self { inner: None } } fn members( cache: &'data ProguardCache<'data>, frame: StackFrame<'data>, members: std::slice::Iter<'data, raw::Member>, - synthesized_class: bool, ) -> Self { Self { inner: Some((cache, frame, members)), - synthesized_class, } } } @@ -448,9 +442,9 @@ impl<'data> Iterator for RemappedFrameIter<'_, 'data> { fn next(&mut self) -> Option { let (cache, frame, members) = self.inner.as_mut()?; if frame.parameters.is_none() { - iterate_with_lines(cache, frame, members, self.synthesized_class) + iterate_with_lines(cache, frame, members) } else { - iterate_without_lines(cache, frame, members, self.synthesized_class) + iterate_without_lines(cache, frame, members) } } } @@ -459,7 +453,6 @@ fn iterate_with_lines<'a>( cache: &ProguardCache<'a>, frame: &mut StackFrame<'a>, members: &mut std::slice::Iter<'_, raw::Member>, - synthesized_class: bool, ) -> Option> { for member in members { // skip any members which do not match our frames line @@ -510,7 +503,7 @@ fn iterate_with_lines<'a>( file, line, parameters: frame.parameters, - is_synthesized: member.is_synthesized() || synthesized_class, + is_synthesized: member.is_synthesized(), }); } None @@ -520,7 +513,6 @@ fn iterate_without_lines<'a>( cache: &ProguardCache<'a>, frame: &mut StackFrame<'a>, members: &mut std::slice::Iter<'_, raw::Member>, - synthesized_class: bool, ) -> Option> { let member = members.next()?; @@ -536,7 +528,7 @@ fn iterate_without_lines<'a>( file: None, line: 0, parameters: frame.parameters, - is_synthesized: member.is_synthesized() || synthesized_class, + is_synthesized: member.is_synthesized(), }) } diff --git a/src/cache/raw.rs b/src/cache/raw.rs index 4b6fc96..d484624 100644 --- a/src/cache/raw.rs +++ b/src/cache/raw.rs @@ -59,6 +59,9 @@ pub(crate) struct Class { /// 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. diff --git a/src/mapper.rs b/src/mapper.rs index a7b5389..253921e 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -74,6 +74,7 @@ struct ClassMembers<'s> { struct ClassMapping<'s> { original: &'s str, members: HashMap<&'s str, ClassMembers<'s>>, + // Note: It's currently unknown what effect a synthesized class has. is_synthesized: bool, } @@ -83,20 +84,15 @@ type MemberIter<'m> = std::slice::Iter<'m, MemberMapping<'m>>; #[derive(Clone, Debug, Default)] pub struct RemappedFrameIter<'m> { inner: Option<(StackFrame<'m>, MemberIter<'m>)>, - synthesized_class: bool, } impl<'m> RemappedFrameIter<'m> { fn empty() -> Self { - Self { - inner: None, - synthesized_class: false, - } + Self { inner: None } } - fn members(frame: StackFrame<'m>, members: MemberIter<'m>, synthesized_class: bool) -> Self { + fn members(frame: StackFrame<'m>, members: MemberIter<'m>) -> Self { Self { inner: Some((frame, members)), - synthesized_class, } } } @@ -106,9 +102,9 @@ impl<'m> Iterator for RemappedFrameIter<'m> { fn next(&mut self) -> Option { let (frame, ref mut members) = self.inner.as_mut()?; if frame.parameters.is_none() { - iterate_with_lines(frame, members, self.synthesized_class) + iterate_with_lines(frame, members) } else { - iterate_without_lines(frame, members, self.synthesized_class) + iterate_without_lines(frame, members) } } } @@ -122,7 +118,6 @@ fn extract_class_name(full_path: &str) -> Option<&str> { fn iterate_with_lines<'a>( frame: &mut StackFrame<'a>, members: &mut core::slice::Iter<'_, MemberMapping<'a>>, - synthesized_class: bool, ) -> Option> { for member in members { // skip any members which do not match our frames line @@ -165,7 +160,7 @@ fn iterate_with_lines<'a>( file, line, parameters: frame.parameters, - is_synthesized: member.is_synthesized || synthesized_class, + is_synthesized: member.is_synthesized, }); } None @@ -174,7 +169,6 @@ fn iterate_with_lines<'a>( fn iterate_without_lines<'a>( frame: &mut StackFrame<'a>, members: &mut core::slice::Iter<'_, MemberMapping<'a>>, - synthesized_class: bool, ) -> Option> { let member = members.next()?; @@ -188,7 +182,7 @@ fn iterate_without_lines<'a>( file: None, line: 0, parameters: frame.parameters, - is_synthesized: member.is_synthesized || synthesized_class, + is_synthesized: member.is_synthesized, }) } @@ -393,7 +387,7 @@ impl<'s> ProguardMapper<'s> { members.all_mappings.iter() }; - RemappedFrameIter::members(frame, mappings, class.is_synthesized) + RemappedFrameIter::members(frame, mappings) } /// Remaps a throwable which is the first line of a full stacktrace. diff --git a/tests/callback.rs b/tests/callback.rs index 42fe5ca..d089dc9 100644 --- a/tests/callback.rs +++ b/tests/callback.rs @@ -16,7 +16,6 @@ fn test_method_matches_callback_mapper() { 28, )); - // The remapped frames should all be synthesized because the class is. assert_eq!( mapped.next().unwrap(), StackFrame::new( @@ -24,7 +23,6 @@ fn test_method_matches_callback_mapper() { "onCreate$lambda$1", 37, ) - .with_synthesized(true) ); assert_eq!( mapped.next().unwrap(), @@ -56,7 +54,6 @@ fn test_method_matches_callback_cache() { 28, )); - // The remapped frames should all be synthesized because the class is. assert_eq!( mapped.next().unwrap(), StackFrame::with_file( @@ -65,7 +62,6 @@ fn test_method_matches_callback_cache() { 37, "EditActivity", ) - .with_synthesized(true) ); assert_eq!( mapped.next().unwrap(), @@ -93,7 +89,6 @@ fn test_method_matches_callback_extra_class_mapper() { 28, )); - // The remapped frames should all be synthesized because the class is. assert_eq!( mapped.next().unwrap(), StackFrame::new( @@ -101,7 +96,6 @@ fn test_method_matches_callback_extra_class_mapper() { "test2", 10, ) - .with_synthesized(true) ); assert_eq!( mapped.next().unwrap(), @@ -110,7 +104,6 @@ fn test_method_matches_callback_extra_class_mapper() { "test", 6, ) - .with_synthesized(true) ); assert_eq!( mapped.next().unwrap(), @@ -119,7 +112,6 @@ fn test_method_matches_callback_extra_class_mapper() { "onCreate$lambda$1", 38, ) - .with_synthesized(true) ); assert_eq!( mapped.next().unwrap(), @@ -152,7 +144,6 @@ fn test_method_matches_callback_extra_class_cache() { 28, )); - // The remapped frames should all be synthesized because the class is. assert_eq!( mapped.next().unwrap(), StackFrame::with_file( @@ -161,7 +152,6 @@ fn test_method_matches_callback_extra_class_cache() { 10, "TestSourceContext", ) - .with_synthesized(true) ); assert_eq!( mapped.next().unwrap(), @@ -171,7 +161,6 @@ fn test_method_matches_callback_extra_class_cache() { 6, "TestSourceContext", ) - .with_synthesized(true) ); assert_eq!( mapped.next().unwrap(), @@ -181,7 +170,6 @@ fn test_method_matches_callback_extra_class_cache() { 38, "EditActivity", ) - .with_synthesized(true) ); assert_eq!( mapped.next().unwrap(), @@ -208,7 +196,6 @@ fn test_method_matches_callback_inner_class_mapper() { 28, )); - // The remapped frames should all be synthesized because the class is. assert_eq!( mapped.next().unwrap(), StackFrame::with_file( @@ -217,7 +204,6 @@ fn test_method_matches_callback_inner_class_mapper() { 19, "EditActivity", ) - .with_synthesized(true) ); assert_eq!( mapped.next().unwrap(), @@ -227,7 +213,6 @@ fn test_method_matches_callback_inner_class_mapper() { 45, "EditActivity", ) - .with_synthesized(true) ); assert_eq!( mapped.next().unwrap(), @@ -260,7 +245,6 @@ fn test_method_matches_callback_inner_class_cache() { 28, )); - // The remapped frames should all be synthesized because the class is. assert_eq!( mapped.next().unwrap(), StackFrame::with_file( @@ -269,7 +253,6 @@ fn test_method_matches_callback_inner_class_cache() { 19, "EditActivity.kt", ) - .with_synthesized(true) ); assert_eq!( mapped.next().unwrap(), @@ -278,7 +261,6 @@ fn test_method_matches_callback_inner_class_cache() { "onCreate$lambda$1", 45, ) - .with_synthesized(true) ); assert_eq!( mapped.next().unwrap(), From e686960f4092852fe5412bc5482ef12c551bace9 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Mon, 7 Jul 2025 11:53:22 +0200 Subject: [PATCH 09/10] Fix some tests and add annotation --- src/mapper.rs | 5 ++++- tests/callback.rs | 26 +++++++++----------------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/mapper.rs b/src/mapper.rs index 253921e..54a54b5 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -74,7 +74,10 @@ struct ClassMembers<'s> { struct ClassMapping<'s> { original: &'s str, members: HashMap<&'s str, ClassMembers<'s>>, - // Note: It's currently unknown what effect a synthesized class has. + #[expect( + unused, + reason = "It is currently unknown what effect a synthesized class has." + )] is_synthesized: bool, } diff --git a/tests/callback.rs b/tests/callback.rs index d089dc9..38b99c4 100644 --- a/tests/callback.rs +++ b/tests/callback.rs @@ -56,20 +56,18 @@ fn test_method_matches_callback_cache() { assert_eq!( mapped.next().unwrap(), - StackFrame::with_file( + StackFrame::new( "io.sentry.samples.instrumentation.ui.EditActivity", "onCreate$lambda$1", 37, - "EditActivity", ) ); assert_eq!( mapped.next().unwrap(), - StackFrame::with_file( + StackFrame::new( "io.sentry.samples.instrumentation.ui.EditActivity$$InternalSyntheticLambda$1$ebaa538726b99bb77e0f5e7c86443911af17d6e5be2b8771952ae0caa4ff2ac7$0", "onMenuItemClick", 0, - "EditActivity", ) .with_synthesized(true) ); @@ -146,38 +144,34 @@ fn test_method_matches_callback_extra_class_cache() { assert_eq!( mapped.next().unwrap(), - StackFrame::with_file( + StackFrame::new( "io.sentry.samples.instrumentation.ui.TestSourceContext", "test2", 10, - "TestSourceContext", ) ); assert_eq!( mapped.next().unwrap(), - StackFrame::with_file( + StackFrame::new( "io.sentry.samples.instrumentation.ui.TestSourceContext", "test", 6, - "TestSourceContext", ) ); assert_eq!( mapped.next().unwrap(), - StackFrame::with_file( + StackFrame::new( "io.sentry.samples.instrumentation.ui.EditActivity", "onCreate$lambda$1", 38, - "EditActivity", ) ); assert_eq!( mapped.next().unwrap(), - StackFrame::with_file( + StackFrame::new( "io.sentry.samples.instrumentation.ui.EditActivity$$InternalSyntheticLambda$1$ebaa538726b99bb77e0f5e7c86443911af17d6e5be2b8771952ae0caa4ff2ac7$0", "onMenuItemClick", 0, - "EditActivity", ) .with_synthesized(true) ); @@ -202,25 +196,23 @@ fn test_method_matches_callback_inner_class_mapper() { "io.sentry.samples.instrumentation.ui.EditActivity$InnerEditActivityClass", "testInner", 19, - "EditActivity", + "EditActivity.kt", ) ); assert_eq!( mapped.next().unwrap(), - StackFrame::with_file( + StackFrame::new( "io.sentry.samples.instrumentation.ui.EditActivity", "onCreate$lambda$1", 45, - "EditActivity", ) ); assert_eq!( mapped.next().unwrap(), - StackFrame::with_file( + StackFrame::new( "io.sentry.samples.instrumentation.ui.EditActivity$$InternalSyntheticLambda$1$ebaa538726b99bb77e0f5e7c86443911af17d6e5be2b8771952ae0caa4ff2ac7$0", "onMenuItemClick", 0, - "EditActivity", ) .with_synthesized(true) ); From 6e27ff46374cd161b8de7de76628004dc3d3fe4c Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Mon, 7 Jul 2025 16:13:14 +0200 Subject: [PATCH 10/10] Rename StackFrame synthesized methods --- src/cache/mod.rs | 10 +++++----- src/mapper.rs | 10 +++++----- src/stacktrace.rs | 32 ++++++++++++++++---------------- tests/callback.rs | 12 ++++++------ 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 8b9fb42..edaaace 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -503,7 +503,7 @@ fn iterate_with_lines<'a>( file, line, parameters: frame.parameters, - is_synthesized: member.is_synthesized(), + method_synthesized: member.is_synthesized(), }); } None @@ -528,7 +528,7 @@ fn iterate_without_lines<'a>( file: None, line: 0, parameters: frame.parameters, - is_synthesized: member.is_synthesized(), + method_synthesized: member.is_synthesized(), }) } @@ -568,7 +568,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: line: 2, file: Some("SourceFile"), parameters: None, - is_synthesized: false, + method_synthesized: false, }, StackFrame { class: "android.view.View", @@ -576,7 +576,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: line: 7393, file: Some("View.java"), parameters: None, - is_synthesized: false, + method_synthesized: false, }, ], cause: Some(Box::new(StackTrace { @@ -590,7 +590,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: line: 1, file: Some("SourceFile"), parameters: None, - is_synthesized: false, + method_synthesized: false, }], cause: None, })), diff --git a/src/mapper.rs b/src/mapper.rs index 54a54b5..79ace5e 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -163,7 +163,7 @@ fn iterate_with_lines<'a>( file, line, parameters: frame.parameters, - is_synthesized: member.is_synthesized, + method_synthesized: member.is_synthesized, }); } None @@ -185,7 +185,7 @@ fn iterate_without_lines<'a>( file: None, line: 0, parameters: frame.parameters, - is_synthesized: member.is_synthesized, + method_synthesized: member.is_synthesized, }) } @@ -557,7 +557,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: line: 2, file: Some("SourceFile"), parameters: None, - is_synthesized: false, + method_synthesized: false, }, StackFrame { class: "android.view.View", @@ -565,7 +565,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: line: 7393, file: Some("View.java"), parameters: None, - is_synthesized: false, + method_synthesized: false, }, ], cause: Some(Box::new(StackTrace { @@ -579,7 +579,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: line: 1, file: Some("SourceFile"), parameters: None, - is_synthesized: false, + method_synthesized: false, }], cause: None, })), diff --git a/src/stacktrace.rs b/src/stacktrace.rs index 581cae1..1e61375 100644 --- a/src/stacktrace.rs +++ b/src/stacktrace.rs @@ -158,7 +158,7 @@ pub struct StackFrame<'s> { pub(crate) line: usize, pub(crate) file: Option<&'s str>, pub(crate) parameters: Option<&'s str>, - pub(crate) is_synthesized: bool, + pub(crate) method_synthesized: bool, } impl<'s> StackFrame<'s> { @@ -170,7 +170,7 @@ impl<'s> StackFrame<'s> { line, file: None, parameters: None, - is_synthesized: false, + method_synthesized: false, } } @@ -182,7 +182,7 @@ impl<'s> StackFrame<'s> { line, file: Some(file), parameters: None, - is_synthesized: false, + method_synthesized: false, } } @@ -195,13 +195,13 @@ impl<'s> StackFrame<'s> { line: 0, file: None, parameters: Some(arguments), - is_synthesized: false, + method_synthesized: false, } } - /// Flags `self` as being synthesized by the compiler according to `is_synthesized`. - pub fn with_synthesized(mut self, is_synthesized: bool) -> Self { - self.is_synthesized = is_synthesized; + /// 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 } @@ -258,9 +258,9 @@ impl<'s> StackFrame<'s> { self.parameters } - /// Returns whether this frame was synthesized by the compiler. - pub fn is_synthesized(&self) -> bool { - self.is_synthesized + /// Returns whether this frame's method was synthesized by the compiler. + pub fn method_synthesized(&self) -> bool { + self.method_synthesized } } @@ -298,7 +298,7 @@ pub(crate) fn parse_frame(line: &str) -> Option { file: Some(file), line, parameters: None, - is_synthesized: false, + method_synthesized: false, }) } @@ -406,7 +406,7 @@ mod tests { line: 5, file: Some("Util.java"), parameters: None, - is_synthesized: false, + method_synthesized: false, }], cause: Some(Box::new(StackTrace { exception: Some(Throwable { @@ -419,7 +419,7 @@ mod tests { line: 115, file: None, parameters: None, - is_synthesized: false, + method_synthesized: false, }], cause: None, })), @@ -443,7 +443,7 @@ Caused by: com.example.Other: Invalid data line: 1, file: Some("SourceFile"), parameters: None, - is_synthesized: false, + method_synthesized: false, }); assert_eq!(expect, stack_frame); @@ -467,7 +467,7 @@ Caused by: com.example.Other: Invalid data line: 1, file: None, parameters: None, - is_synthesized: false, + method_synthesized: false, }; assert_eq!( @@ -481,7 +481,7 @@ Caused by: com.example.Other: Invalid data line: 1, file: Some("SourceFile"), parameters: None, - is_synthesized: false, + method_synthesized: false, }; assert_eq!( diff --git a/tests/callback.rs b/tests/callback.rs index 38b99c4..36d3d8b 100644 --- a/tests/callback.rs +++ b/tests/callback.rs @@ -31,7 +31,7 @@ fn test_method_matches_callback_mapper() { "onMenuItemClick", 0, ) - .with_synthesized(true) + .with_method_synthesized(true) ); assert_eq!(mapped.next(), None); } @@ -69,7 +69,7 @@ fn test_method_matches_callback_cache() { "onMenuItemClick", 0, ) - .with_synthesized(true) + .with_method_synthesized(true) ); assert_eq!(mapped.next(), None); } @@ -118,7 +118,7 @@ fn test_method_matches_callback_extra_class_mapper() { "onMenuItemClick", 0, ) - .with_synthesized(true) + .with_method_synthesized(true) ); assert_eq!(mapped.next(), None); } @@ -173,7 +173,7 @@ fn test_method_matches_callback_extra_class_cache() { "onMenuItemClick", 0, ) - .with_synthesized(true) + .with_method_synthesized(true) ); assert_eq!(mapped.next(), None); } @@ -214,7 +214,7 @@ fn test_method_matches_callback_inner_class_mapper() { "onMenuItemClick", 0, ) - .with_synthesized(true) + .with_method_synthesized(true) ); assert_eq!(mapped.next(), None); } @@ -261,7 +261,7 @@ fn test_method_matches_callback_inner_class_cache() { "onMenuItemClick", 0, ) - .with_synthesized(true) + .with_method_synthesized(true) ); assert_eq!(mapped.next(), None); }