Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 222 additions & 12 deletions src/cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,13 @@ use crate::{java, stacktrace, DeobfuscatedSignature, StackFrame, StackTrace, Thr
pub use raw::{ProguardCache, PRGCACHE_VERSION};

/// Result of looking up member mappings for a frame.
/// Contains: (members, prepared_frame, rewrite_rules, had_mappings, outer_source_file)
/// Contains: (members, prepared_frame, rewrite_rules, had_mappings, has_line_info, outer_source_file)
type MemberLookupResult<'data> = (
&'data [raw::Member],
StackFrame<'data>,
Vec<RewriteRule<'data>>,
bool,
bool,
Option<&'data str>,
);

Expand Down Expand Up @@ -384,11 +385,14 @@ impl<'data> ProguardCache<'data> {
}
}

let has_line_info = mapping_entries.iter().any(|m| m.endline > 0);

Some((
mapping_entries,
prepared_frame,
rewrite_rules,
had_mappings,
has_line_info,
outer_source_file,
))
}
Expand All @@ -402,8 +406,14 @@ impl<'data> ProguardCache<'data> {
&'r self,
frame: &StackFrame<'data>,
) -> RemappedFrameIter<'r, 'data> {
let Some((members, prepared_frame, _rewrite_rules, _had_mappings, outer_source_file)) =
self.find_members_and_rules(frame)
let Some((
members,
prepared_frame,
_rewrite_rules,
had_mappings,
has_line_info,
outer_source_file,
)) = self.find_members_and_rules(frame)
else {
return RemappedFrameIter::empty();
};
Expand All @@ -413,7 +423,8 @@ impl<'data> ProguardCache<'data> {
prepared_frame,
members.iter(),
0,
false,
had_mappings,
has_line_info,
outer_source_file,
)
}
Expand Down Expand Up @@ -452,9 +463,32 @@ impl<'data> ProguardCache<'data> {

let effective = self.prepare_frame_for_mapping(frame, carried_outline_pos);

let Some((members, prepared_frame, rewrite_rules, had_mappings, outer_source_file)) =
self.find_members_and_rules(&effective)
let Some((
members,
prepared_frame,
rewrite_rules,
had_mappings,
has_line_info,
outer_source_file,
)) = self.find_members_and_rules(&effective)
else {
// Even if we cannot resolve a member mapping, we may still be able to remap the class.
if let Some(class) = self.get_class(effective.class) {
let original_class = self
.read_string(class.original_name_offset)
.unwrap_or(effective.class);
let outer_source_file = self.read_string(class.file_name_offset).ok();
let file =
synthesize_source_file(original_class, outer_source_file).map(Cow::Owned);
return Some(RemappedFrameIter::single(StackFrame {
class: original_class,
method: effective.method,
file,
line: effective.line,
parameters: effective.parameters,
method_synthesized: false,
}));
}
return Some(RemappedFrameIter::empty());
};

Expand All @@ -471,6 +505,7 @@ impl<'data> ProguardCache<'data> {
members.iter(),
skip_count,
had_mappings,
has_line_info,
outer_source_file,
))
}
Expand Down Expand Up @@ -779,10 +814,14 @@ pub struct RemappedFrameIter<'r, 'data> {
StackFrame<'data>,
std::slice::Iter<'data, raw::Member>,
)>,
/// A single remapped frame fallback (e.g. class-only remapping).
fallback: Option<StackFrame<'data>>,
/// Number of frames to skip from rewrite rules.
skip_count: usize,
/// Whether there were mapping entries (for should_skip determination).
had_mappings: bool,
/// Whether this method has any line-based mappings.
has_line_info: bool,
/// The source file of the outer class for synthesis.
outer_source_file: Option<&'data str>,
}
Expand All @@ -791,8 +830,10 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> {
fn empty() -> Self {
Self {
inner: None,
fallback: None,
skip_count: 0,
had_mappings: false,
has_line_info: false,
outer_source_file: None,
}
}
Expand All @@ -803,16 +844,30 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> {
members: std::slice::Iter<'data, raw::Member>,
skip_count: usize,
had_mappings: bool,
has_line_info: bool,
outer_source_file: Option<&'data str>,
) -> Self {
Self {
inner: Some((cache, frame, members)),
fallback: None,
skip_count,
had_mappings,
has_line_info,
outer_source_file,
}
}

fn single(frame: StackFrame<'data>) -> Self {
Self {
inner: None,
fallback: Some(frame),
skip_count: 0,
had_mappings: false,
has_line_info: false,
outer_source_file: None,
}
}

/// Returns whether there were mapping entries before rewrite rules were applied.
///
/// After collecting frames, if `had_mappings()` is true but the result is empty,
Expand All @@ -822,12 +877,108 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> {
}

fn next_inner(&mut self) -> Option<StackFrame<'data>> {
let (cache, frame, members) = self.inner.as_mut()?;
if frame.parameters.is_none() {
iterate_with_lines(cache, frame, members, self.outer_source_file)
} else {
iterate_without_lines(cache, frame, members, self.outer_source_file)
if let Some(frame) = self.fallback.take() {
return Some(frame);
}

let (cache, mut frame, mut members) = self.inner.take()?;

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 {
let remaining = members.as_slice();
// Prefer base entries (endline == 0) if present.
let mut base_members = remaining.iter().filter(|m| m.endline == 0);
if let Some(first_base) = base_members.next() {
// If all base entries resolve to the same original method name, deduplicate.
let all_same = base_members.all(|m| {
m.original_class_offset == first_base.original_class_offset
&& m.original_name_offset == first_base.original_name_offset
});

if all_same {
let class = cache
.read_string(first_base.original_class_offset)
.unwrap_or(frame.class);
let method = cache.read_string(first_base.original_name_offset).ok()?;
let file =
synthesize_source_file(class, self.outer_source_file).map(Cow::Owned);

return Some(StackFrame {
class,
method,
file,
// Preserve input line if present when the mapping has no line info.
line: frame.line,
parameters: frame.parameters,
method_synthesized: first_base.is_synthesized(),
});
}

// Multiple distinct base entries: iterate them (skip line-mapped entries).
let mapped = iterate_without_lines_preferring_base(
cache,
&mut frame,
&mut members,
self.outer_source_file,
);
self.inner = Some((cache, frame, members));
return mapped;
}

// No base entries: fall back to existing behavior (may yield multiple candidates).
let first = remaining.first()?;
let unambiguous = remaining.iter().all(|m| {
m.original_class_offset == first.original_class_offset
&& m.original_name_offset == first.original_name_offset
});

if unambiguous {
let class = cache
.read_string(first.original_class_offset)
.unwrap_or(frame.class);
let method = cache.read_string(first.original_name_offset).ok()?;
let file =
synthesize_source_file(class, self.outer_source_file).map(Cow::Owned);

return Some(StackFrame {
class,
method,
file,
// Preserve input line if present when the mapping has no line info.
line: frame.line,
parameters: frame.parameters,
method_synthesized: first.is_synthesized(),
});
}

let mapped =
iterate_without_lines(cache, &mut frame, &mut members, self.outer_source_file);
self.inner = Some((cache, frame, members));
return mapped;
}

// With a concrete line number, skip base entries if there are line mappings.
let mapped = iterate_with_lines(
cache,
&mut frame,
&mut members,
self.outer_source_file,
self.has_line_info,
);
self.inner = Some((cache, frame, members));
mapped
} else {
let mapped =
iterate_without_lines(cache, &mut frame, &mut members, self.outer_source_file);
self.inner = Some((cache, frame, members));
mapped
};

// If we returned early for the unambiguous line==0 case above, `self.inner` remains `None`
// which ensures the iterator terminates.
out
}
}

Expand All @@ -849,8 +1000,33 @@ fn iterate_with_lines<'a>(
frame: &mut StackFrame<'a>,
members: &mut std::slice::Iter<'_, raw::Member>,
outer_source_file: Option<&str>,
has_line_info: bool,
) -> Option<StackFrame<'a>> {
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 {
continue;
}
// If the mapping entry has no line range, preserve the input line number (if any).
if member.endline == 0 {
let class = cache
.read_string(member.original_class_offset)
.unwrap_or(frame.class);

let method = cache.read_string(member.original_name_offset).ok()?;

// Synthesize from class name (input filename is not reliable)
let file = synthesize_source_file(class, outer_source_file).map(Cow::Owned);

return Some(StackFrame {
class,
method,
file,
line: frame.line,
parameters: frame.parameters,
method_synthesized: member.is_synthesized(),
});
}
// 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)
Expand Down Expand Up @@ -902,6 +1078,39 @@ fn iterate_with_lines<'a>(
None
}

fn iterate_without_lines_preferring_base<'a>(
cache: &ProguardCache<'a>,
frame: &mut StackFrame<'a>,
members: &mut std::slice::Iter<'_, raw::Member>,
outer_source_file: Option<&str>,
) -> Option<StackFrame<'a>> {
for member in members {
if member.endline != 0 {
continue;
}

let class = cache
.read_string(member.original_class_offset)
.unwrap_or(frame.class);

let method = cache.read_string(member.original_name_offset).ok()?;

// Synthesize from class name (input filename is not reliable)
let file = synthesize_source_file(class, outer_source_file).map(Cow::Owned);

return Some(StackFrame {
class,
method,
file,
// Preserve input line if present when the mapping has no line info.
line: frame.line,
parameters: frame.parameters,
method_synthesized: member.is_synthesized(),
});
}
None
}

fn iterate_without_lines<'a>(
cache: &ProguardCache<'a>,
frame: &mut StackFrame<'a>,
Expand All @@ -923,7 +1132,8 @@ fn iterate_without_lines<'a>(
class,
method,
file,
line: 0,
// Preserve input line if present when the mapping has no line info.
line: frame.line,
parameters: frame.parameters,
method_synthesized: member.is_synthesized(),
})
Expand Down
5 changes: 0 additions & 5 deletions src/cache/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,12 +343,7 @@ impl<'data> ProguardCache<'data> {
.entry(obfuscated_method.as_str())
.or_default();

let has_line_info = members.all.iter().any(|m| m.endline > 0);
for member in members.all.iter() {
// Skip members without line information if there are members with line information
if has_line_info && member.startline == 0 && member.endline == 0 {
continue;
}
method_mappings.push(Self::resolve_mapping(
&mut string_table,
&parsed,
Expand Down
Loading