@@ -85,12 +85,13 @@ use crate::{java, stacktrace, DeobfuscatedSignature, StackFrame, StackTrace, Thr
8585pub use raw:: { ProguardCache , PRGCACHE_VERSION } ;
8686
8787/// Result of looking up member mappings for a frame.
88- /// Contains: (members, prepared_frame, rewrite_rules, had_mappings, outer_source_file)
88+ /// Contains: (members, prepared_frame, rewrite_rules, had_mappings, has_line_info, outer_source_file)
8989type MemberLookupResult < ' data > = (
9090 & ' data [ raw:: Member ] ,
9191 StackFrame < ' data > ,
9292 Vec < RewriteRule < ' data > > ,
9393 bool ,
94+ bool ,
9495 Option < & ' data str > ,
9596) ;
9697
@@ -153,6 +154,22 @@ impl From<CacheErrorKind> for CacheError {
153154}
154155
155156impl < ' data > ProguardCache < ' data > {
157+ fn remap_class_only (
158+ & self ,
159+ frame : & StackFrame < ' data > ,
160+ reference_file : Option < & ' data str > ,
161+ ) -> StackFrame < ' data > {
162+ let file = synthesize_source_file ( frame. class , reference_file) . map ( Cow :: Owned ) ;
163+ StackFrame {
164+ class : frame. class ,
165+ method : frame. method ,
166+ file,
167+ line : frame. line ,
168+ parameters : frame. parameters ,
169+ method_synthesized : false ,
170+ }
171+ }
172+
156173 fn get_class ( & self , name : & str ) -> Option < & raw:: Class > {
157174 let idx = self
158175 . classes
@@ -384,11 +401,14 @@ impl<'data> ProguardCache<'data> {
384401 }
385402 }
386403
404+ let has_line_info = mapping_entries. iter ( ) . any ( |m| m. endline > 0 ) ;
405+
387406 Some ( (
388407 mapping_entries,
389408 prepared_frame,
390409 rewrite_rules,
391410 had_mappings,
411+ has_line_info,
392412 outer_source_file,
393413 ) )
394414 }
@@ -402,8 +422,14 @@ impl<'data> ProguardCache<'data> {
402422 & ' r self ,
403423 frame : & StackFrame < ' data > ,
404424 ) -> RemappedFrameIter < ' r , ' data > {
405- let Some ( ( members, prepared_frame, _rewrite_rules, _had_mappings, outer_source_file) ) =
406- self . find_members_and_rules ( frame)
425+ let Some ( (
426+ members,
427+ prepared_frame,
428+ _rewrite_rules,
429+ had_mappings,
430+ has_line_info,
431+ outer_source_file,
432+ ) ) = self . find_members_and_rules ( frame)
407433 else {
408434 return RemappedFrameIter :: empty ( ) ;
409435 } ;
@@ -413,7 +439,8 @@ impl<'data> ProguardCache<'data> {
413439 prepared_frame,
414440 members. iter ( ) ,
415441 0 ,
416- false ,
442+ had_mappings,
443+ has_line_info,
417444 outer_source_file,
418445 )
419446 }
@@ -452,9 +479,29 @@ impl<'data> ProguardCache<'data> {
452479
453480 let effective = self . prepare_frame_for_mapping ( frame, carried_outline_pos) ;
454481
455- let Some ( ( members, prepared_frame, rewrite_rules, had_mappings, outer_source_file) ) =
456- self . find_members_and_rules ( & effective)
482+ let Some ( (
483+ members,
484+ prepared_frame,
485+ rewrite_rules,
486+ had_mappings,
487+ has_line_info,
488+ outer_source_file,
489+ ) ) = self . find_members_and_rules ( & effective)
457490 else {
491+ // Even if we cannot resolve a member mapping, we may still be able to remap the class.
492+ if let Some ( class) = self . get_class ( effective. class ) {
493+ let original_class = self
494+ . read_string ( class. original_name_offset )
495+ . unwrap_or ( effective. class ) ;
496+ let outer_source_file = self . read_string ( class. file_name_offset ) . ok ( ) ;
497+ return Some ( RemappedFrameIter :: single ( self . remap_class_only (
498+ & StackFrame {
499+ class : original_class,
500+ ..effective
501+ } ,
502+ outer_source_file,
503+ ) ) ) ;
504+ }
458505 return Some ( RemappedFrameIter :: empty ( ) ) ;
459506 } ;
460507
@@ -471,6 +518,7 @@ impl<'data> ProguardCache<'data> {
471518 members. iter ( ) ,
472519 skip_count,
473520 had_mappings,
521+ has_line_info,
474522 outer_source_file,
475523 ) )
476524 }
@@ -779,10 +827,14 @@ pub struct RemappedFrameIter<'r, 'data> {
779827 StackFrame < ' data > ,
780828 std:: slice:: Iter < ' data , raw:: Member > ,
781829 ) > ,
830+ /// A single remapped frame fallback (e.g. class-only remapping).
831+ fallback : Option < StackFrame < ' data > > ,
782832 /// Number of frames to skip from rewrite rules.
783833 skip_count : usize ,
784834 /// Whether there were mapping entries (for should_skip determination).
785835 had_mappings : bool ,
836+ /// Whether this method has any line-based mappings.
837+ has_line_info : bool ,
786838 /// The source file of the outer class for synthesis.
787839 outer_source_file : Option < & ' data str > ,
788840}
@@ -791,8 +843,10 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> {
791843 fn empty ( ) -> Self {
792844 Self {
793845 inner : None ,
846+ fallback : None ,
794847 skip_count : 0 ,
795848 had_mappings : false ,
849+ has_line_info : false ,
796850 outer_source_file : None ,
797851 }
798852 }
@@ -803,16 +857,30 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> {
803857 members : std:: slice:: Iter < ' data , raw:: Member > ,
804858 skip_count : usize ,
805859 had_mappings : bool ,
860+ has_line_info : bool ,
806861 outer_source_file : Option < & ' data str > ,
807862 ) -> Self {
808863 Self {
809864 inner : Some ( ( cache, frame, members) ) ,
865+ fallback : None ,
810866 skip_count,
811867 had_mappings,
868+ has_line_info,
812869 outer_source_file,
813870 }
814871 }
815872
873+ fn single ( frame : StackFrame < ' data > ) -> Self {
874+ Self {
875+ inner : None ,
876+ fallback : Some ( frame) ,
877+ skip_count : 0 ,
878+ had_mappings : false ,
879+ has_line_info : false ,
880+ outer_source_file : None ,
881+ }
882+ }
883+
816884 /// Returns whether there were mapping entries before rewrite rules were applied.
817885 ///
818886 /// After collecting frames, if `had_mappings()` is true but the result is empty,
@@ -822,12 +890,72 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> {
822890 }
823891
824892 fn next_inner ( & mut self ) -> Option < StackFrame < ' data > > {
825- let ( cache, frame, members) = self . inner . as_mut ( ) ?;
826- if frame. parameters . is_none ( ) {
827- iterate_with_lines ( cache, frame, members, self . outer_source_file )
828- } else {
829- iterate_without_lines ( cache, frame, members, self . outer_source_file )
893+ if let Some ( frame) = self . fallback . take ( ) {
894+ return Some ( frame) ;
830895 }
896+
897+ let ( cache, mut frame, mut members) = self . inner . take ( ) ?;
898+
899+ let out = if frame. parameters . is_none ( ) {
900+ // If we have no line number, treat it as unknown. If there are base (no-line) mappings
901+ // present, prefer those over line-mapped entries.
902+ if frame. line == 0 {
903+ let selection = select_no_line_members ( members. as_slice ( ) ) ?;
904+ let mapped = match selection {
905+ NoLineSelection :: Single ( member) => {
906+ return map_member_without_lines (
907+ cache,
908+ & frame,
909+ member,
910+ self . outer_source_file ,
911+ ) ;
912+ }
913+ NoLineSelection :: IterateBase => {
914+ let mut mapped = None ;
915+ for member in members. by_ref ( ) {
916+ if member. endline == 0 {
917+ mapped = map_member_without_lines (
918+ cache,
919+ & frame,
920+ member,
921+ self . outer_source_file ,
922+ ) ;
923+ break ;
924+ }
925+ }
926+ mapped
927+ }
928+ NoLineSelection :: IterateAll => iterate_without_lines (
929+ cache,
930+ & mut frame,
931+ & mut members,
932+ self . outer_source_file ,
933+ ) ,
934+ } ;
935+ self . inner = Some ( ( cache, frame, members) ) ;
936+ return mapped;
937+ }
938+
939+ // With a concrete line number, skip base entries if there are line mappings.
940+ let mapped = iterate_with_lines (
941+ cache,
942+ & mut frame,
943+ & mut members,
944+ self . outer_source_file ,
945+ self . has_line_info ,
946+ ) ;
947+ self . inner = Some ( ( cache, frame, members) ) ;
948+ mapped
949+ } else {
950+ let mapped =
951+ iterate_without_lines ( cache, & mut frame, & mut members, self . outer_source_file ) ;
952+ self . inner = Some ( ( cache, frame, members) ) ;
953+ mapped
954+ } ;
955+
956+ // If we returned early for the unambiguous line==0 case above, `self.inner` remains `None`
957+ // which ensures the iterator terminates.
958+ out
831959 }
832960}
833961
@@ -849,8 +977,17 @@ fn iterate_with_lines<'a>(
849977 frame : & mut StackFrame < ' a > ,
850978 members : & mut std:: slice:: Iter < ' _ , raw:: Member > ,
851979 outer_source_file : Option < & str > ,
980+ has_line_info : bool ,
852981) -> Option < StackFrame < ' a > > {
853982 for member in members {
983+ // If this method has line mappings, skip base (no-line) entries when we have a concrete line.
984+ if has_line_info && frame. line > 0 && member. endline == 0 {
985+ continue ;
986+ }
987+ // If the mapping entry has no line range, preserve the input line number (if any).
988+ if member. endline == 0 {
989+ return map_member_without_lines ( cache, frame, member, outer_source_file) ;
990+ }
854991 // skip any members which do not match our frames line
855992 if member. endline > 0
856993 && ( frame. line < member. startline as usize || frame. line > member. endline as usize )
@@ -902,6 +1039,70 @@ fn iterate_with_lines<'a>(
9021039 None
9031040}
9041041
1042+ /// Selection strategy for line==0 frames.
1043+ ///
1044+ /// When line info is missing, we prefer base (no-line) mappings if they exist.
1045+ /// If all candidates resolve to the same original method, we treat it as
1046+ /// unambiguous and return a single mapping. Otherwise we iterate either over
1047+ /// base mappings (when present) or all mappings (when only line-mapped entries exist).
1048+ enum NoLineSelection < ' a > {
1049+ Single ( & ' a raw:: Member ) ,
1050+ IterateBase ,
1051+ IterateAll ,
1052+ }
1053+
1054+ fn select_no_line_members < ' a > ( members : & ' a [ raw:: Member ] ) -> Option < NoLineSelection < ' a > > {
1055+ // Prefer base entries (endline == 0) if present.
1056+ let mut base_members = members. iter ( ) . filter ( |m| m. endline == 0 ) ;
1057+ if let Some ( first_base) = base_members. next ( ) {
1058+ let all_same = base_members. all ( |m| {
1059+ m. original_class_offset == first_base. original_class_offset
1060+ && m. original_name_offset == first_base. original_name_offset
1061+ } ) ;
1062+
1063+ return Some ( if all_same {
1064+ NoLineSelection :: Single ( first_base)
1065+ } else {
1066+ NoLineSelection :: IterateBase
1067+ } ) ;
1068+ }
1069+
1070+ let first = members. first ( ) ?;
1071+ let unambiguous = members. iter ( ) . all ( |m| {
1072+ m. original_class_offset == first. original_class_offset
1073+ && m. original_name_offset == first. original_name_offset
1074+ } ) ;
1075+
1076+ Some ( if unambiguous {
1077+ NoLineSelection :: Single ( first)
1078+ } else {
1079+ NoLineSelection :: IterateAll
1080+ } )
1081+ }
1082+
1083+ fn map_member_without_lines < ' a > (
1084+ cache : & ProguardCache < ' a > ,
1085+ frame : & StackFrame < ' a > ,
1086+ member : & raw:: Member ,
1087+ outer_source_file : Option < & str > ,
1088+ ) -> Option < StackFrame < ' a > > {
1089+ let class = cache
1090+ . read_string ( member. original_class_offset )
1091+ . unwrap_or ( frame. class ) ;
1092+ let method = cache. read_string ( member. original_name_offset ) . ok ( ) ?;
1093+ let file = synthesize_source_file ( class, outer_source_file) . map ( Cow :: Owned ) ;
1094+
1095+ Some ( StackFrame {
1096+ class,
1097+ method,
1098+ file,
1099+ // Preserve input line if present when the mapping has no line info.
1100+ line : frame. line ,
1101+ parameters : frame. parameters ,
1102+ method_synthesized : member. is_synthesized ( ) ,
1103+ } )
1104+ }
1105+
9051106fn iterate_without_lines < ' a > (
9061107 cache : & ProguardCache < ' a > ,
9071108 frame : & mut StackFrame < ' a > ,
@@ -923,7 +1124,8 @@ fn iterate_without_lines<'a>(
9231124 class,
9241125 method,
9251126 file,
926- line : 0 ,
1127+ // Preserve input line if present when the mapping has no line info.
1128+ line : frame. line ,
9271129 parameters : frame. parameters ,
9281130 method_synthesized : member. is_synthesized ( ) ,
9291131 } )
0 commit comments