diff --git a/src/cache/mod.rs b/src/cache/mod.rs index c2c1ac9..ac8e0a7 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -168,7 +168,7 @@ impl<'data> ProguardCache<'data> { class: frame.class, method: frame.method, file, - line: frame.line, + line: Some(frame.line.unwrap_or(0)), parameters: frame.parameters, method_synthesized: false, } @@ -389,9 +389,9 @@ impl<'data> ProguardCache<'data> { if prepared_frame.parameters.is_none() { for member in mapping_entries { // Check if this member would produce a frame (line matching) + let pf_line = prepared_frame.line.unwrap_or(0); if member.endline == 0 - || (prepared_frame.line >= member.startline as usize - && prepared_frame.line <= member.endline as usize) + || (pf_line >= member.startline as usize && pf_line <= member.endline as usize) { had_mappings = true; rewrite_rules.extend(self.decode_rewrite_rules(member)); @@ -477,7 +477,7 @@ impl<'data> ProguardCache<'data> { 'r: 'data, { if self.is_outline_frame(frame.class, frame.method) { - *carried_outline_pos = Some(frame.line); + *carried_outline_pos = Some(frame.line.unwrap_or(0)); return None; } @@ -678,11 +678,11 @@ impl<'data> ProguardCache<'data> { if let Some(mapped) = self.map_outline_position( effective.class, effective.method, - effective.line, + effective.line.unwrap_or(0), pos, effective.parameters, ) { - effective.line = mapped; + effective.line = Some(mapped); } } @@ -903,7 +903,7 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> { let out = if frame.parameters.is_none() { // If we have no line number, treat it as unknown. If there are base (no-line) mappings // present, prefer those over line-mapped entries. - if frame.line == 0 { + if frame.line.unwrap_or(0) == 0 { let selection = select_no_line_members(members.as_slice())?; let mapped = match selection { NoLineSelection::Single(member) => { @@ -983,9 +983,10 @@ fn iterate_with_lines<'a>( outer_source_file: Option<&str>, has_line_info: bool, ) -> Option> { + let frame_line = frame.line.unwrap_or(0); for member in members { // If this method has line mappings, skip base (no-line) entries when we have a concrete line. - if has_line_info && frame.line > 0 && member.endline == 0 { + if has_line_info && frame_line > 0 && member.endline == 0 { continue; } // If the mapping entry has no line range, preserve the input line number (if any). @@ -994,7 +995,7 @@ fn iterate_with_lines<'a>( } // skip any members which do not match our frames line if member.endline > 0 - && (frame.line < member.startline as usize || frame.line > member.endline as usize) + && (frame_line < member.startline as usize || frame_line > member.endline as usize) { continue; } @@ -1005,7 +1006,7 @@ fn iterate_with_lines<'a>( { member.original_startline as usize } else { - member.original_startline as usize + frame.line - member.startline as usize + member.original_startline as usize + frame_line - member.startline as usize }; let class = cache @@ -1035,7 +1036,7 @@ fn iterate_with_lines<'a>( class, method, file, - line, + line: Some(line), parameters: frame.parameters, method_synthesized: member.is_synthesized(), }); @@ -1105,12 +1106,12 @@ fn map_member_without_lines<'a>( class, method, file, - line: resolve_no_line_output_line( - frame.line, + line: Some(resolve_no_line_output_line( + frame.line.unwrap_or(0), original_startline, member.startline as usize, member.endline as usize, - ), + )), parameters: frame.parameters, method_synthesized: member.is_synthesized(), }) @@ -1142,12 +1143,12 @@ fn iterate_without_lines<'a>( class, method, file, - line: resolve_no_line_output_line( - frame.line, + line: Some(resolve_no_line_output_line( + frame.line.unwrap_or(0), original_startline, member.startline as usize, member.endline as usize, - ), + )), parameters: frame.parameters, method_synthesized: member.is_synthesized(), }) @@ -1207,7 +1208,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: StackFrame { class: "com.example.MainFragment$g", method: "onClick", - line: 2, + line: Some(2), file: Some(Cow::Borrowed("SourceFile")), parameters: None, method_synthesized: false, @@ -1215,7 +1216,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: StackFrame { class: "android.view.View", method: "performClick", - line: 7393, + line: Some(7393), file: Some(Cow::Borrowed("View.java")), parameters: None, method_synthesized: false, @@ -1229,7 +1230,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: frames: vec![StackFrame { class: "com.example.MainFragment$g", method: "onClick", - line: 1, + line: Some(1), file: Some(Cow::Borrowed("SourceFile")), parameters: None, method_synthesized: false, @@ -1421,7 +1422,7 @@ some.Other -> b: StackFrame { class: "a", method: "call", - line: 4, + line: Some(4), file: Some(Cow::Borrowed("SourceFile")), parameters: None, method_synthesized: false, @@ -1429,7 +1430,7 @@ some.Other -> b: StackFrame { class: "b", method: "run", - line: 5, + line: Some(5), file: Some(Cow::Borrowed("SourceFile")), parameters: None, method_synthesized: false, @@ -1445,6 +1446,6 @@ some.Other -> b: assert_eq!(remapped.frames.len(), 1); assert_eq!(remapped.frames[0].class, "some.Other"); assert_eq!(remapped.frames[0].method, "method"); - assert_eq!(remapped.frames[0].line, 30); + assert_eq!(remapped.frames[0].line, Some(30)); } } diff --git a/src/mapper.rs b/src/mapper.rs index 4e419ed..6f7e18e 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -140,7 +140,8 @@ fn map_member_with_lines<'a>( frame: &StackFrame<'a>, member: &MemberMapping<'a>, ) -> Option> { - if member.endline > 0 && (frame.line < member.startline || frame.line > member.endline) { + let frame_line = frame.line.unwrap_or(0); + if member.endline > 0 && (frame_line < member.startline || frame_line > member.endline) { return None; } @@ -151,7 +152,7 @@ fn map_member_with_lines<'a>( { member.original_startline } else { - member.original_startline + frame.line - member.startline + member.original_startline + frame_line - member.startline }; let class = member.original_class.unwrap_or(frame.class); @@ -172,7 +173,7 @@ fn map_member_with_lines<'a>( class, method: member.original, file, - line, + line: Some(line), parameters: frame.parameters, method_synthesized: member.is_synthesized, }) @@ -191,7 +192,7 @@ fn map_member_without_lines<'a>( None }; let line = resolve_no_line_output_line( - frame.line, + frame.line.unwrap_or(0), original_startline, member.startline, member.endline, @@ -202,7 +203,7 @@ fn map_member_without_lines<'a>( file, // Preserve input line if present (e.g. "Unknown Source:7") when the mapping itself // has no line information. This matches R8 retrace behavior. - line, + line: Some(line), parameters: frame.parameters, method_synthesized: member.is_synthesized, } @@ -214,7 +215,7 @@ fn remap_class_only<'a>(frame: &StackFrame<'a>, reference_file: Option<&str>) -> class: frame.class, method: frame.method, file, - line: frame.line, + line: Some(frame.line.unwrap_or(0)), parameters: frame.parameters, method_synthesized: false, } @@ -295,9 +296,10 @@ fn iterate_with_lines<'a>( members: &mut core::slice::Iter<'_, MemberMapping<'a>>, has_line_info: bool, ) -> Option> { + let frame_line = frame.line.unwrap_or(0); for member in members { // If this method has line mappings, skip base (no-line) entries when we have a concrete line. - if has_line_info && frame.line > 0 && member.endline == 0 { + if has_line_info && frame_line > 0 && member.endline == 0 { continue; } // If the mapping entry has no line range, preserve the input line number (if any). @@ -522,11 +524,11 @@ impl<'s> ProguardMapper<'s> { if let Some(mapped) = self.map_outline_position( effective.class, effective.method, - effective.line, + effective.line.unwrap_or(0), pos, effective.parameters, ) { - effective.line = mapped; + effective.line = Some(mapped); } } @@ -584,7 +586,7 @@ impl<'s> ProguardMapper<'s> { // If the stacktrace has no line number, treat it as unknown and remap without // applying line filters. If there are base (no-line) mappings present, prefer those. - if frame.line == 0 { + if frame.line.unwrap_or(0) == 0 { let selection = select_no_line_members(mapping_entries, has_line_info); match selection { Some(NoLineSelection::Single(member)) => { @@ -744,7 +746,7 @@ impl<'s> ProguardMapper<'s> { if let Some(frame) = stacktrace::parse_frame(line) { if self.is_outline_frame(frame.class, frame.method) { - carried_outline_pos = Some(frame.line); + carried_outline_pos = Some(frame.line.unwrap_or(0)); continue; } @@ -810,7 +812,7 @@ impl<'s> ProguardMapper<'s> { let mut next_frame_can_rewrite = exception_descriptor.is_some(); for f in trace.frames.iter() { if self.is_outline_frame(f.class, f.method) { - carried_outline_pos = Some(f.line); + carried_outline_pos = Some(f.line.unwrap_or(0)); continue; } @@ -913,7 +915,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: StackFrame { class: "com.example.MainFragment$g", method: "onClick", - line: 2, + line: Some(2), file: Some(Cow::Borrowed("SourceFile")), parameters: None, method_synthesized: false, @@ -921,7 +923,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: StackFrame { class: "android.view.View", method: "performClick", - line: 7393, + line: Some(7393), file: Some(Cow::Borrowed("View.java")), parameters: None, method_synthesized: false, @@ -935,7 +937,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: frames: vec![StackFrame { class: "com.example.MainFragment$g", method: "onClick", - line: 1, + line: Some(1), file: Some(Cow::Borrowed("SourceFile")), parameters: None, method_synthesized: false, @@ -1055,7 +1057,7 @@ some.Class -> a: frames: vec![StackFrame { class: "a", method: "a", - line: 4, + line: Some(4), file: Some(Cow::Borrowed("SourceFile")), parameters: None, method_synthesized: false, @@ -1069,7 +1071,7 @@ some.Class -> a: assert_eq!(remapped.frames.len(), 1); assert_eq!(remapped.frames[0].class, "some.Class"); assert_eq!(remapped.frames[0].method, "caller"); - assert_eq!(remapped.frames[0].line, 7); + assert_eq!(remapped.frames[0].line, Some(7)); } #[test] @@ -1176,7 +1178,7 @@ some.Other -> b: StackFrame { class: "a", method: "call", - line: 4, + line: Some(4), file: Some(Cow::Borrowed("SourceFile")), parameters: None, method_synthesized: false, @@ -1184,7 +1186,7 @@ some.Other -> b: StackFrame { class: "b", method: "run", - line: 5, + line: Some(5), file: Some(Cow::Borrowed("SourceFile")), parameters: None, method_synthesized: false, @@ -1200,6 +1202,6 @@ some.Other -> b: assert_eq!(remapped.frames.len(), 1); assert_eq!(remapped.frames[0].class, "some.Other"); assert_eq!(remapped.frames[0].method, "method"); - assert_eq!(remapped.frames[0].line, 30); + assert_eq!(remapped.frames[0].line, Some(30)); } } diff --git a/src/stacktrace.rs b/src/stacktrace.rs index 9e56806..053ade1 100644 --- a/src/stacktrace.rs +++ b/src/stacktrace.rs @@ -154,7 +154,7 @@ fn parse_stacktrace(content: &str) -> Option> { pub struct StackFrame<'s> { pub(crate) class: &'s str, pub(crate) method: &'s str, - pub(crate) line: usize, + pub(crate) line: Option, pub(crate) file: Option>, pub(crate) parameters: Option<&'s str>, pub(crate) method_synthesized: bool, @@ -166,7 +166,7 @@ impl<'s> StackFrame<'s> { Self { class, method, - line, + line: Some(line), file: None, parameters: None, method_synthesized: false, @@ -178,7 +178,7 @@ impl<'s> StackFrame<'s> { Self { class, method, - line, + line: Some(line), file: Some(Cow::Borrowed(file)), parameters: None, method_synthesized: false, @@ -191,7 +191,7 @@ impl<'s> StackFrame<'s> { Self { class, method, - line: 0, + line: None, file: None, parameters: Some(arguments), method_synthesized: false, @@ -248,7 +248,9 @@ impl<'s> StackFrame<'s> { } /// The line of the StackFrame, 1-based. - pub fn line(&self) -> usize { + /// + /// Returns `None` if the frame has no line information. + pub fn line(&self) -> Option { self.line } @@ -266,11 +268,14 @@ impl<'s> StackFrame<'s> { impl Display for StackFrame<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { let file_name = self.file.as_deref().unwrap_or(""); - write!( - f, - "at {}.{}({}:{})", - self.class, self.method, file_name, self.line - ) + match self.line { + Some(line) => write!( + f, + "at {}.{}({}:{})", + self.class, self.method, file_name, line + ), + None => write!(f, "at {}.{}({})", self.class, self.method, file_name), + } } } @@ -287,8 +292,8 @@ pub(crate) fn parse_frame(line: &str) -> Option> { let (method_split, file_split) = line[3..line.len() - 1].split_once('(')?; let (class, method) = method_split.rsplit_once('.')?; let (file, line) = match file_split.rsplit_once(':') { - Some((file, line)) => (file, line.parse().unwrap_or(0)), - None => (file_split, 0), + Some((file, line)) => (file, Some(line.parse().unwrap_or(0))), + None => (file_split, None), }; Some(StackFrame { @@ -402,7 +407,7 @@ mod tests { frames: vec![StackFrame { class: "com.example.Util", method: "show", - line: 5, + line: Some(5), file: Some(Cow::Borrowed("Util.java")), parameters: None, method_synthesized: false, @@ -415,7 +420,7 @@ mod tests { frames: vec![StackFrame { class: "com.example.Parser", method: "parse", - line: 115, + line: Some(115), file: None, parameters: None, method_synthesized: false, @@ -439,7 +444,7 @@ Caused by: com.example.Other: Invalid data let expect = Some(StackFrame { class: "com.example.MainFragment", method: "onClick", - line: 1, + line: Some(1), file: Some(Cow::Borrowed("SourceFile")), parameters: None, method_synthesized: false, @@ -463,7 +468,7 @@ Caused by: com.example.Other: Invalid data let frame = StackFrame { class: "com.example.MainFragment", method: "onClick", - line: 1, + line: Some(1), file: None, parameters: None, method_synthesized: false, @@ -477,7 +482,7 @@ Caused by: com.example.Other: Invalid data let frame = StackFrame { class: "com.example.MainFragment", method: "onClick", - line: 1, + line: Some(1), file: Some(Cow::Borrowed("SourceFile")), parameters: None, method_synthesized: false, diff --git a/tests/r8-inline.rs b/tests/r8-inline.rs index 0f70b69..6dc7ac9 100644 --- a/tests/r8-inline.rs +++ b/tests/r8-inline.rs @@ -83,13 +83,13 @@ fn test_inline_with_line_numbers_frame() { assert_eq!(frames.len(), 4); assert_eq!(frames[0].method(), "method3"); - assert_eq!(frames[0].line(), 81); + assert_eq!(frames[0].line(), Some(81)); assert_eq!(frames[1].method(), "method2"); - assert_eq!(frames[1].line(), 88); + assert_eq!(frames[1].line(), Some(88)); assert_eq!(frames[2].method(), "method1"); - assert_eq!(frames[2].line(), 96); + assert_eq!(frames[2].line(), Some(96)); assert_eq!(frames[3].method(), "main"); - assert_eq!(frames[3].line(), 102); + assert_eq!(frames[3].line(), Some(102)); } // ============================================================================= @@ -493,15 +493,15 @@ fn test_inline_preamble_no_original() { let frames: Vec<_> = mapper.remap_frame(&StackFrame::new("a", "a", 2)).collect(); assert_eq!(frames.len(), 1); assert_eq!(frames[0].method(), "caller"); - assert_eq!(frames[0].line(), 10); + assert_eq!(frames[0].line(), Some(10)); // Test line 5 - should be in inline range (4:5) let frames: Vec<_> = mapper.remap_frame(&StackFrame::new("a", "a", 5)).collect(); assert_eq!(frames.len(), 2); assert_eq!(frames[0].method(), "inlined"); - assert_eq!(frames[0].line(), 21); + assert_eq!(frames[0].line(), Some(21)); assert_eq!(frames[1].method(), "caller"); - assert_eq!(frames[1].line(), 11); + assert_eq!(frames[1].line(), Some(11)); } // ============================================================================= @@ -580,15 +580,15 @@ fn test_inline_frame_depth_one() { let frames: Vec<_> = mapper.remap_frame(&StackFrame::new("a", "a", 1)).collect(); assert_eq!(frames.len(), 1); assert_eq!(frames[0].method(), "foo"); - assert_eq!(frames[0].line(), 10); + assert_eq!(frames[0].line(), Some(10)); // Line 2 - one level of inlining let frames: Vec<_> = mapper.remap_frame(&StackFrame::new("a", "a", 2)).collect(); assert_eq!(frames.len(), 2); assert_eq!(frames[0].method(), "bar"); - assert_eq!(frames[0].line(), 20); + assert_eq!(frames[0].line(), Some(20)); assert_eq!(frames[1].method(), "foo"); - assert_eq!(frames[1].line(), 11); + assert_eq!(frames[1].line(), Some(11)); } #[test] @@ -605,11 +605,11 @@ fn test_inline_frame_depth_two() { let frames: Vec<_> = mapper.remap_frame(&StackFrame::new("a", "a", 2)).collect(); assert_eq!(frames.len(), 3); assert_eq!(frames[0].method(), "baz"); - assert_eq!(frames[0].line(), 30); + assert_eq!(frames[0].line(), Some(30)); assert_eq!(frames[1].method(), "bar"); - assert_eq!(frames[1].line(), 21); + assert_eq!(frames[1].line(), Some(21)); assert_eq!(frames[2].method(), "foo"); - assert_eq!(frames[2].line(), 11); + assert_eq!(frames[2].line(), Some(11)); } #[test] @@ -631,11 +631,11 @@ fn test_inline_frame_depth_two_cache() { let frames: Vec<_> = cache.remap_frame(&frame).collect(); assert_eq!(frames.len(), 3); assert_eq!(frames[0].method(), "baz"); - assert_eq!(frames[0].line(), 30); + assert_eq!(frames[0].line(), Some(30)); assert_eq!(frames[1].method(), "bar"); - assert_eq!(frames[1].line(), 21); + assert_eq!(frames[1].line(), Some(21)); assert_eq!(frames[2].method(), "foo"); - assert_eq!(frames[2].line(), 11); + assert_eq!(frames[2].line(), Some(11)); } // ============================================================================= @@ -655,15 +655,15 @@ fn test_inline_with_line_range() { let frames: Vec<_> = mapper.remap_frame(&StackFrame::new("a", "a", 3)).collect(); assert_eq!(frames.len(), 1); assert_eq!(frames[0].method(), "outer"); - assert_eq!(frames[0].line(), 12); // 10 + (3-1) = 12 + assert_eq!(frames[0].line(), Some(12)); // 10 + (3-1) = 12 // Line 8 - in inline range let frames: Vec<_> = mapper.remap_frame(&StackFrame::new("a", "a", 8)).collect(); assert_eq!(frames.len(), 2); assert_eq!(frames[0].method(), "inner"); - assert_eq!(frames[0].line(), 22); // 20 + (8-6) = 22 + assert_eq!(frames[0].line(), Some(22)); // 20 + (8-6) = 22 assert_eq!(frames[1].method(), "outer"); - assert_eq!(frames[1].line(), 15); + assert_eq!(frames[1].line(), Some(15)); } // ============================================================================= @@ -689,15 +689,15 @@ com.example.Main -> a: assert_eq!(frames[0].class(), "com.example.lib.Library"); assert_eq!(frames[0].method(), "work"); - assert_eq!(frames[0].line(), 100); + assert_eq!(frames[0].line(), Some(100)); assert_eq!(frames[1].class(), "com.example.util.Utils"); assert_eq!(frames[1].method(), "helper"); - assert_eq!(frames[1].line(), 51); + assert_eq!(frames[1].line(), Some(51)); assert_eq!(frames[2].class(), "com.example.Main"); assert_eq!(frames[2].method(), "main"); - assert_eq!(frames[2].line(), 11); + assert_eq!(frames[2].line(), Some(11)); } #[test] @@ -783,7 +783,7 @@ fn test_inline_with_zero_original_line() { // Should have 2 frames - the inline chain assert_eq!(frames.len(), 2); assert_eq!(frames[0].method(), "main"); - assert_eq!(frames[0].line(), 0); + assert_eq!(frames[0].line(), Some(0)); assert_eq!(frames[1].method(), "caller"); - assert_eq!(frames[1].line(), 10); + assert_eq!(frames[1].line(), Some(10)); } diff --git a/tests/r8.rs b/tests/r8.rs index cfe58b1..c534587 100644 --- a/tests/r8.rs +++ b/tests/r8.rs @@ -410,16 +410,16 @@ fn rewrite_frame_complex_stacktrace_typed() { assert_eq!(frames.len(), 4); assert_eq!(frames[0].class(), "com.example.flow.Initializer"); assert_eq!(frames[0].method(), "start"); - assert_eq!(frames[0].line(), 42); + assert_eq!(frames[0].line(), Some(42)); assert_eq!(frames[1].class(), "com.example.flow.StreamRouter$Inline"); assert_eq!(frames[1].method(), "internalDispatch"); - assert_eq!(frames[1].line(), 30); + assert_eq!(frames[1].line(), Some(30)); assert_eq!(frames[2].class(), "com.example.flow.StreamRouter"); assert_eq!(frames[2].method(), "dispatch"); - assert_eq!(frames[2].line(), 12); + assert_eq!(frames[2].line(), Some(12)); assert_eq!(frames[3].class(), "com.example.flow.UiBridge"); assert_eq!(frames[3].method(), "render"); - assert_eq!(frames[3].line(), 200); + assert_eq!(frames[3].line(), Some(200)); // Caused by exception (also not in mapping) let cause = remapped.cause().unwrap(); @@ -430,10 +430,10 @@ fn rewrite_frame_complex_stacktrace_typed() { assert_eq!(cause_frames.len(), 2); assert_eq!(cause_frames[0].class(), "com.example.flow.StreamRouter"); assert_eq!(cause_frames[0].method(), "dispatch"); - assert_eq!(cause_frames[0].line(), 12); + assert_eq!(cause_frames[0].line(), Some(12)); assert_eq!(cause_frames[1].class(), "com.example.flow.UiBridge"); assert_eq!(cause_frames[1].method(), "render"); - assert_eq!(cause_frames[1].line(), 200); + assert_eq!(cause_frames[1].line(), Some(200)); } #[test] @@ -541,7 +541,7 @@ fn test_method_with_zero_zero_and_line_specific_mappings() { ); assert_eq!(frame.method(), "obtainDropShadowRenderer-eZhPAX0"); // Should map to line 70 (from the 1:4: mapping), not line 68 (from the 0:0: mapping) - assert_eq!(frame.line(), 70); + assert_eq!(frame.line(), Some(70)); assert_eq!(mapped.next(), None); } @@ -574,6 +574,6 @@ fn test_method_with_zero_zero_and_line_specific_mappings_cache() { ); assert_eq!(remapped_frame.method(), "obtainDropShadowRenderer-eZhPAX0"); // Should map to line 70 (from the 1:4: mapping), not line 68 (from the 0:0: mapping) - assert_eq!(remapped_frame.line(), 70); + assert_eq!(remapped_frame.line(), Some(70)); assert_eq!(mapped.next(), None); }