Skip to content

Commit efdfbb2

Browse files
authored
feat(r8-tests): Add R8 ambiguous tests (#71)
Part of #40 There are failing tests in this PR that need fixes in the mapper/cache itself, so I will open a follow-up PR that targets this branch. ## R8 Retrace Test Coverage - [x] Inlining (13 tests) - inline frames, line numbers, source files, rewriteFrame rules - [x] Ambiguous Methods (21 tests) - multiple methods → same obfuscated name, <OR> alternatives - [x] Outlines (4 tests) - outline callsites, nested outlines, inline-in-outline - [ ] Synthetic/Lambda (3 tests) - lambda methods, synthetic bridges - [ ] Source File Edge Cases (7 tests) - colons, unicode, multiple dots, synthesized names - [ ] Line Number Handling (10 tests) - spans, preambles, invalid ranges, missing lines - [ ] Exception Handling (6 tests) - obfuscated exceptions, suppressed, circular refs - [ ] Method Overloading (4 tests) - overloads with/without ranges - [ ] Special Formats (4 tests) - Java 9+ modules, logcat format, auto-detect
1 parent 40d6195 commit efdfbb2

File tree

7 files changed

+737
-36
lines changed

7 files changed

+737
-36
lines changed

src/cache/mod.rs

Lines changed: 214 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,13 @@ use crate::{java, stacktrace, DeobfuscatedSignature, StackFrame, StackTrace, Thr
8585
pub 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)
8989
type 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

155156
impl<'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+
9051106
fn 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
})

src/cache/raw.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -343,12 +343,7 @@ impl<'data> ProguardCache<'data> {
343343
.entry(obfuscated_method.as_str())
344344
.or_default();
345345

346-
let has_line_info = members.all.iter().any(|m| m.endline > 0);
347346
for member in members.all.iter() {
348-
// Skip members without line information if there are members with line information
349-
if has_line_info && member.startline == 0 && member.endline == 0 {
350-
continue;
351-
}
352347
method_mappings.push(Self::resolve_mapping(
353348
&mut string_table,
354349
&parsed,

0 commit comments

Comments
 (0)