use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt;
use std::fmt::{Error as FmtError, Write};
use std::iter::FusedIterator;
const MAX_SPAN_EXPANSION: usize = 65_535;
use crate::builder::{
Member, MethodReceiver, ParsedProguardMapping, RewriteAction, RewriteCondition, RewriteRule,
};
use crate::java;
use crate::mapping::ProguardMapping;
use crate::stacktrace::{self, StackFrame, StackTrace, Throwable};
use crate::utils::{class_name_to_descriptor, extract_class_name, synthesize_source_file};
pub struct DeobfuscatedSignature {
parameters: Vec<String>,
return_type: String,
}
impl DeobfuscatedSignature {
pub(crate) fn new(signature: (Vec<String>, String)) -> DeobfuscatedSignature {
DeobfuscatedSignature {
parameters: signature.0,
return_type: signature.1,
}
}
pub fn return_type(&self) -> &str {
self.return_type.as_str()
}
pub fn parameters_types(&self) -> impl Iterator<Item = &str> {
self.parameters.iter().map(|s| s.as_ref())
}
pub fn format_signature(&self) -> String {
let mut signature = format!("({})", self.parameters.join(", "));
if !self.return_type().is_empty() && self.return_type() != "void" {
signature.push_str(": ");
signature.push_str(self.return_type());
}
signature
}
}
impl fmt::Display for DeobfuscatedSignature {
fn fmt(&self, f: &mut std::fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.format_signature())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct MemberMapping<'s> {
startline: Option<usize>,
endline: Option<usize>,
original_class: Option<&'s str>,
original_file: Option<&'s str>,
original: &'s str,
original_startline: Option<usize>,
original_endline: Option<usize>,
is_synthesized: bool,
is_outline: bool,
outline_callsite_positions: Option<HashMap<usize, usize>>,
rewrite_rules: Vec<RewriteRule<'s>>,
outer_source_file: Option<&'s str>,
}
#[derive(Clone, Debug, Default)]
struct ClassMembers<'s> {
all_mappings: Vec<MemberMapping<'s>>,
mappings_by_params: HashMap<&'s str, Vec<MemberMapping<'s>>>,
}
#[derive(Clone, Debug, Default)]
struct ClassMapping<'s> {
original: &'s str,
members: HashMap<&'s str, ClassMembers<'s>>,
#[expect(
unused,
reason = "Class-level synthesized is propagated to members in resolve_mapping; \
kept here for potential future class-level queries."
)]
is_synthesized: bool,
}
#[derive(Default)]
struct CollectedFrames<'s> {
frames: Vec<StackFrame<'s>>,
rewrite_rules: Vec<&'s RewriteRule<'s>>,
}
type MemberIter<'m> = std::slice::Iter<'m, MemberMapping<'m>>;
#[derive(Clone, Debug, Default)]
pub struct RemappedFrameIter<'m> {
inner: Option<(StackFrame<'m>, MemberIter<'m>)>,
has_line_info: bool,
}
impl<'m> RemappedFrameIter<'m> {
fn empty() -> Self {
Self {
inner: None,
has_line_info: false,
}
}
fn members(frame: StackFrame<'m>, members: MemberIter<'m>, has_line_info: bool) -> Self {
Self {
inner: Some((frame, members)),
has_line_info,
}
}
}
impl<'m> Iterator for RemappedFrameIter<'m> {
type Item = StackFrame<'m>;
fn next(&mut self) -> Option<Self::Item> {
let (frame, ref mut members) = self.inner.as_mut()?;
if frame.parameters.is_none() {
iterate_with_lines(frame, members, self.has_line_info)
} else {
iterate_without_lines(frame, members)
}
}
}
fn map_member_with_lines<'a>(
frame: &StackFrame<'a>,
member: &MemberMapping<'a>,
) -> Option<StackFrame<'a>> {
let frame_line = frame.line.unwrap_or(0);
if member.endline.unwrap_or(0) > 0
&& (frame_line < member.startline.unwrap_or(0) || frame_line > member.endline.unwrap_or(0))
{
return None;
}
let line = if member.original_endline.is_none()
|| member.original_endline == member.original_startline
{
member.original_startline.unwrap_or(0)
} else {
member.original_startline.unwrap_or(0) + frame_line - member.startline.unwrap_or(0)
};
let class = member.original_class.unwrap_or(frame.class);
let file: Option<Cow<'a, str>> = if let Some(file_name) = member.original_file {
if file_name == "R8$$SyntheticClass" {
extract_class_name(class).map(Cow::Borrowed)
} else {
Some(Cow::Borrowed(file_name))
}
} else {
synthesize_source_file(class, member.outer_source_file).map(Cow::Owned)
};
Some(StackFrame {
class,
method: member.original,
file,
line: Some(line),
parameters: frame.parameters,
method_synthesized: member.is_synthesized,
})
}
fn map_member_without_lines<'a>(
frame: &StackFrame<'a>,
member: &MemberMapping<'a>,
output_line: Option<usize>,
) -> StackFrame<'a> {
let class = member.original_class.unwrap_or(frame.class);
let file = synthesize_source_file(class, member.outer_source_file).map(Cow::Owned);
StackFrame {
class,
method: member.original,
file,
line: output_line,
parameters: frame.parameters,
method_synthesized: member.is_synthesized,
}
}
fn remap_class_only<'a>(frame: &StackFrame<'a>, reference_file: Option<&str>) -> StackFrame<'a> {
let file = synthesize_source_file(frame.class, reference_file).map(Cow::Owned);
StackFrame {
class: frame.class,
method: frame.method,
file,
line: Some(frame.line.unwrap_or(0)),
parameters: frame.parameters,
method_synthesized: false,
}
}
fn apply_rewrite_rules<'s>(collected: &mut CollectedFrames<'s>, thrown_descriptor: Option<&str>) {
if collected.frames.is_empty() {
return;
}
for rule in &collected.rewrite_rules {
let matches = rule.conditions.iter().all(|condition| match condition {
RewriteCondition::Throws(descriptor) => Some(*descriptor) == thrown_descriptor,
RewriteCondition::Unknown(_) => false,
});
if !matches {
continue;
}
for action in &rule.actions {
match action {
RewriteAction::RemoveInnerFrames(count) => {
if *count >= collected.frames.len() {
collected.frames.clear();
} else {
collected.frames.drain(0..*count);
}
}
RewriteAction::Unknown(_) => {}
}
}
if collected.frames.is_empty() {
break;
}
}
}
fn iterate_with_lines<'a>(
frame: &mut StackFrame<'a>,
members: &mut core::slice::Iter<'_, MemberMapping<'a>>,
has_line_info: bool,
) -> Option<StackFrame<'a>> {
let frame_line = frame.line.unwrap_or(0);
for member in members {
if has_line_info && frame_line > 0 && member.endline.unwrap_or(0) == 0 {
continue;
}
if member.endline.unwrap_or(0) == 0 {
let output_line = if member.original_startline.is_none() {
frame.line
} else if member.original_startline.unwrap_or(0) > 0 {
member.original_startline
} else {
None
};
return Some(map_member_without_lines(frame, member, output_line));
}
if let Some(mapped) = map_member_with_lines(frame, member) {
return Some(mapped);
}
}
None
}
fn iterate_without_lines<'a>(
frame: &mut StackFrame<'a>,
members: &mut core::slice::Iter<'_, MemberMapping<'a>>,
) -> Option<StackFrame<'a>> {
members.next().map(|member| {
let output_line = if member.original_startline.unwrap_or(0) > 0 {
member.original_startline
} else {
None
};
map_member_without_lines(frame, member, output_line)
})
}
impl FusedIterator for RemappedFrameIter<'_> {}
fn resolve_no_line_frames<'s>(
frame: &StackFrame<'s>,
mapping_entries: &'s [MemberMapping<'s>],
base_entries: &[&'s MemberMapping<'s>],
collected: &mut CollectedFrames<'s>,
) {
if !base_entries.is_empty() {
resolve_base_entries(frame, base_entries, collected);
return;
}
let Some(first) = mapping_entries.first() else {
return;
};
let first_start = first.startline;
let first_end = first.endline;
let first_group: Vec<_> = mapping_entries
.iter()
.take_while(|m| m.startline == first_start && m.endline == first_end)
.collect();
if first_group.len() > 1 {
for member in &first_group {
let line = member.original_startline.filter(|&v| v > 0).or(Some(0));
collected
.frames
.push(map_member_without_lines(frame, member, line));
collected.rewrite_rules.extend(member.rewrite_rules.iter());
}
} else {
let unambiguous = mapping_entries.iter().all(|m| m.original == first.original);
if unambiguous {
collected
.frames
.push(map_member_without_lines(frame, first, Some(0)));
collected.rewrite_rules.extend(first.rewrite_rules.iter());
} else {
for member in mapping_entries {
collected
.frames
.push(map_member_without_lines(frame, member, Some(0)));
collected.rewrite_rules.extend(member.rewrite_rules.iter());
}
}
}
}
fn resolve_base_entries<'s>(
frame: &StackFrame<'s>,
base_entries: &[&'s MemberMapping<'s>],
collected: &mut CollectedFrames<'s>,
) {
let mut any_zero_zero_has_range = false;
let mut no_range_count = 0usize;
let mut first_no_range_name: Option<&str> = None;
let mut all_no_range_same_name = true;
let mut all_no_range_have_line_mapping = true;
for member in base_entries {
if member.startline.is_some() {
if member.original_endline.is_some()
&& member.original_endline != member.original_startline
{
any_zero_zero_has_range = true;
}
} else {
no_range_count += 1;
if member.original_startline.is_none() {
all_no_range_have_line_mapping = false;
}
match first_no_range_name {
None => first_no_range_name = Some(member.original),
Some(first) if member.original != first => all_no_range_same_name = false,
_ => {}
}
}
}
let mut no_range_emitted = false;
for member in base_entries {
if member.startline.is_some() {
let line = if any_zero_zero_has_range {
Some(0)
} else if member.original_startline.unwrap_or(0) > 0 {
member.original_startline
} else {
None
};
collected
.frames
.push(map_member_without_lines(frame, member, line));
collected.rewrite_rules.extend(member.rewrite_rules.iter());
} else if all_no_range_same_name {
if !no_range_emitted {
no_range_emitted = true;
let line = if no_range_count == 1 {
member.original_startline.or(Some(0))
} else {
Some(0)
};
collected
.frames
.push(map_member_without_lines(frame, member, line));
collected.rewrite_rules.extend(member.rewrite_rules.iter());
}
} else {
collected
.frames
.push(map_member_without_lines(frame, member, Some(0)));
collected.rewrite_rules.extend(member.rewrite_rules.iter());
}
}
if !all_no_range_same_name && all_no_range_have_line_mapping {
collected.frames.sort_by_key(|f| f.method);
}
}
#[derive(Clone, Debug)]
pub struct ProguardMapper<'s> {
classes: HashMap<&'s str, ClassMapping<'s>>,
}
impl<'s> From<&'s str> for ProguardMapper<'s> {
fn from(s: &'s str) -> Self {
let mapping = ProguardMapping::new(s.as_ref());
Self::new(mapping)
}
}
impl<'s> From<(&'s str, bool)> for ProguardMapper<'s> {
fn from(t: (&'s str, bool)) -> Self {
let mapping = ProguardMapping::new(t.0.as_ref());
Self::new_with_param_mapping(mapping, t.1)
}
}
impl<'s> ProguardMapper<'s> {
pub fn new(mapping: ProguardMapping<'s>) -> Self {
Self::create_proguard_mapper(mapping, false)
}
pub fn new_with_param_mapping(
mapping: ProguardMapping<'s>,
initialize_param_mapping: bool,
) -> Self {
Self::create_proguard_mapper(mapping, initialize_param_mapping)
}
fn create_proguard_mapper(
mapping: ProguardMapping<'s>,
initialize_param_mapping: bool,
) -> Self {
let parsed = ParsedProguardMapping::parse(mapping, initialize_param_mapping);
let mut class_mappings: HashMap<&str, ClassMapping<'s>> = parsed
.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()
},
)
})
.collect();
for ((obfuscated_class, obfuscated_method), members) in &parsed.members {
let class_mapping = class_mappings.entry(obfuscated_class.as_str()).or_default();
let outer_source_file = parsed
.class_names
.get(obfuscated_class)
.and_then(|original| parsed.class_infos.get(original))
.and_then(|ci| ci.source_file);
let method_mappings = class_mapping
.members
.entry(obfuscated_method.as_str())
.or_default();
for member in members.all.iter() {
method_mappings.all_mappings.push(Self::resolve_mapping(
&parsed,
member,
outer_source_file,
));
}
for (args, param_members) in members.by_params.iter() {
let param_mappings = method_mappings.mappings_by_params.entry(args).or_default();
for member in param_members.iter() {
param_mappings.push(Self::resolve_mapping(&parsed, member, outer_source_file));
}
}
}
Self {
classes: class_mappings,
}
}
fn resolve_mapping(
parsed: &ParsedProguardMapping<'s>,
member: &Member<'s>,
outer_source_file: Option<&'s str>,
) -> MemberMapping<'s> {
let original_file = parsed
.class_infos
.get(&member.method.receiver.name())
.and_then(|class| class.source_file);
let original_class = match member.method.receiver {
MethodReceiver::ThisClass(_) => None,
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 class_synthesized = parsed
.class_infos
.get(&member.method.receiver.name())
.is_some_and(|ci| ci.is_synthesized);
let is_synthesized = method_info.is_synthesized || class_synthesized;
let is_outline = method_info.is_outline;
let outline_callsite_positions = member.outline_callsite_positions.clone();
MemberMapping {
startline: member.startline,
endline: member.endline,
original_class,
original_file,
original: member.method.name.as_str(),
original_startline: member.original_startline,
original_endline: member.original_endline,
is_synthesized,
is_outline,
outline_callsite_positions,
rewrite_rules: member.rewrite_rules.clone(),
outer_source_file,
}
}
fn map_outline_position(
&self,
class: &str,
method: &str,
callsite_line: usize,
pos: usize,
parameters: Option<&str>,
) -> Option<usize> {
let ms = self.classes.get(class)?.members.get(method)?;
let candidates: &[_] = if let Some(params) = parameters {
match ms.mappings_by_params.get(params) {
Some(v) => &v[..],
None => &[],
}
} else {
&ms.all_mappings[..]
};
candidates
.iter()
.filter(|m| {
m.endline.unwrap_or(0) == 0
|| (callsite_line >= m.startline.unwrap_or(0)
&& callsite_line <= m.endline.unwrap_or(0))
})
.find_map(|m| {
m.outline_callsite_positions
.as_ref()
.and_then(|mm| mm.get(&pos).copied())
})
}
fn is_outline_frame(&self, class: &str, method: &str) -> bool {
self.classes
.get(class)
.and_then(|c| c.members.get(method))
.and_then(|ms| ms.all_mappings.first())
.is_some_and(|m| m.is_outline)
}
fn prepare_frame_for_mapping<'a>(
&self,
frame: &StackFrame<'a>,
carried_outline_pos: &mut Option<usize>,
) -> StackFrame<'a> {
let mut effective = frame.clone();
if let Some(pos) = carried_outline_pos.take() {
if let Some(mapped) = self.map_outline_position(
effective.class,
effective.method,
effective.line.unwrap_or(0),
pos,
effective.parameters,
) {
effective.line = Some(mapped);
}
}
effective
}
pub fn remap_class(&'s self, class: &str) -> Option<&'s str> {
self.classes.get(class).map(|class| class.original)
}
fn collect_remapped_frames(&'s self, frame: &StackFrame<'s>) -> CollectedFrames<'s> {
let mut collected = CollectedFrames::default();
let Some(class) = self.classes.get(frame.class) else {
return collected;
};
let mut frame = frame.clone();
frame.class = class.original;
let Some(members) = class.members.get(frame.method) else {
collected
.frames
.push(remap_class_only(&frame, frame.file()));
return collected;
};
let mapping_entries: &[MemberMapping<'s>] = if let Some(parameters) = frame.parameters {
let Some(typed_members) = members.mappings_by_params.get(parameters) else {
return collected;
};
typed_members.as_slice()
} else {
members.all_mappings.as_slice()
};
if frame.parameters.is_none() {
let has_line_info = mapping_entries.iter().any(|m| m.endline.unwrap_or(0) > 0);
let frame_line = frame.line.unwrap_or(0);
let base_entries: Vec<&MemberMapping<'s>> = mapping_entries
.iter()
.filter(|m| m.endline.unwrap_or(0) == 0)
.collect();
if frame_line == 0 {
resolve_no_line_frames(&frame, mapping_entries, &base_entries, &mut collected);
return collected;
}
for member in mapping_entries {
if has_line_info && frame_line > 0 && member.endline.unwrap_or(0) == 0 {
continue;
}
if member.endline.unwrap_or(0) == 0 {
if member.original_startline.is_none() {
collected
.frames
.push(map_member_without_lines(&frame, member, frame.line));
collected.rewrite_rules.extend(member.rewrite_rules.iter());
continue;
}
if let Some(oe) = member.original_endline {
let os = member.original_startline.unwrap_or(0);
if oe > os && (oe - os) <= MAX_SPAN_EXPANSION {
for line in os..=oe {
collected.frames.push(map_member_without_lines(
&frame,
member,
Some(line),
));
}
collected.rewrite_rules.extend(member.rewrite_rules.iter());
continue;
}
}
let output_line = if member.original_startline.unwrap_or(0) > 0 {
member.original_startline
} else {
None
};
collected
.frames
.push(map_member_without_lines(&frame, member, output_line));
collected.rewrite_rules.extend(member.rewrite_rules.iter());
} else if let Some(mapped) = map_member_with_lines(&frame, member) {
collected.frames.push(mapped);
collected.rewrite_rules.extend(member.rewrite_rules.iter());
}
}
if collected.frames.is_empty() && has_line_info {
collected
.frames
.push(remap_class_only(&frame, frame.file()));
}
} else {
for member in mapping_entries {
let output_line = if member.original_startline.unwrap_or(0) > 0 {
member.original_startline
} else {
None
};
let mapped = map_member_without_lines(&frame, member, output_line);
collected.frames.push(mapped);
collected.rewrite_rules.extend(member.rewrite_rules.iter());
}
}
collected
}
pub fn deobfuscate_signature(&'s self, signature: &str) -> Option<DeobfuscatedSignature> {
java::deobfuscate_bytecode_signature(signature, self).map(DeobfuscatedSignature::new)
}
pub fn remap_method(&'s self, class: &str, method: &str) -> Option<(&'s str, &'s str)> {
let class = self.classes.get(class)?;
let mut members = class.members.get(method)?.all_mappings.iter();
let first = members.next()?;
let all_matching = members.all(|member| member.original == first.original);
all_matching.then_some((class.original, first.original))
}
pub fn remap_frame(&'s self, frame: &StackFrame<'s>) -> RemappedFrameIter<'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();
};
let mut frame = frame.clone();
frame.class = class.original;
let mappings = if let Some(parameters) = frame.parameters {
if let Some(typed_members) = members.mappings_by_params.get(parameters) {
typed_members.iter()
} else {
return RemappedFrameIter::empty();
}
} else {
members.all_mappings.iter()
};
let has_line_info = members
.all_mappings
.iter()
.any(|m| m.endline.unwrap_or(0) > 0);
RemappedFrameIter::members(frame, mappings, has_line_info)
}
pub fn remap_throwable<'a>(&'a self, throwable: &Throwable<'a>) -> Option<Throwable<'a>> {
self.remap_class(throwable.class).map(|class| Throwable {
class,
message: throwable.message,
})
}
pub fn remap_stacktrace(&self, input: &str) -> Result<String, std::fmt::Error> {
let mut stacktrace = String::new();
let mut carried_outline_pos: Option<usize> = None;
let mut current_exception_descriptor: Option<String> = None;
let mut next_frame_can_rewrite = false;
for line in input.lines() {
if let Some(throwable) = stacktrace::parse_throwable(line) {
let remapped_throwable = self.remap_throwable(&throwable);
let descriptor_class = remapped_throwable
.as_ref()
.map(|t| t.class)
.unwrap_or(throwable.class);
current_exception_descriptor = Some(class_name_to_descriptor(descriptor_class));
next_frame_can_rewrite = true;
format_throwable(&mut stacktrace, line, remapped_throwable)?;
continue;
}
if let Some(frame) = stacktrace::parse_frame(line) {
if self.is_outline_frame(frame.class, frame.method) {
carried_outline_pos = Some(frame.line.unwrap_or(0));
continue;
}
let effective_frame =
self.prepare_frame_for_mapping(&frame, &mut carried_outline_pos);
let mut collected = self.collect_remapped_frames(&effective_frame);
let had_frames = !collected.frames.is_empty();
if next_frame_can_rewrite {
apply_rewrite_rules(&mut collected, current_exception_descriptor.as_deref());
}
next_frame_can_rewrite = false;
current_exception_descriptor = None;
if had_frames && collected.frames.is_empty() {
continue;
}
format_frames(&mut stacktrace, line, collected.frames.into_iter())?;
continue;
}
if let Some(cause) = line
.strip_prefix("Caused by: ")
.and_then(stacktrace::parse_throwable)
{
let remapped_cause = self.remap_throwable(&cause);
let descriptor_class = remapped_cause
.as_ref()
.map(|t| t.class)
.unwrap_or(cause.class);
current_exception_descriptor = Some(class_name_to_descriptor(descriptor_class));
next_frame_can_rewrite = true;
format_cause(&mut stacktrace, line, remapped_cause)?;
continue;
}
current_exception_descriptor = None;
next_frame_can_rewrite = false;
writeln!(&mut stacktrace, "{line}")?;
}
Ok(stacktrace)
}
pub fn remap_stacktrace_typed<'a>(&'a self, trace: &StackTrace<'a>) -> StackTrace<'a> {
let exception = trace
.exception
.as_ref()
.and_then(|t| self.remap_throwable(t));
let exception_descriptor = trace.exception.as_ref().map(|original| {
let class = exception
.as_ref()
.map(|t| t.class)
.unwrap_or(original.class);
class_name_to_descriptor(class)
});
let mut carried_outline_pos: Option<usize> = None;
let mut frames_out = Vec::with_capacity(trace.frames.len());
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.unwrap_or(0));
continue;
}
let effective = self.prepare_frame_for_mapping(f, &mut carried_outline_pos);
let mut collected = self.collect_remapped_frames(&effective);
let had_frames = !collected.frames.is_empty();
if next_frame_can_rewrite {
apply_rewrite_rules(&mut collected, exception_descriptor.as_deref());
}
next_frame_can_rewrite = false;
if had_frames && collected.frames.is_empty() {
continue;
}
if collected.frames.is_empty() {
frames_out.push(f.clone());
} else {
frames_out.append(&mut collected.frames);
}
}
let cause = trace
.cause
.as_ref()
.map(|c| Box::new(self.remap_stacktrace_typed(c)));
StackTrace {
exception,
frames: frames_out,
cause,
}
}
}
pub(crate) fn format_throwable(
stacktrace: &mut impl Write,
line: &str,
throwable: Option<Throwable<'_>>,
) -> Result<(), FmtError> {
if let Some(throwable) = throwable {
writeln!(stacktrace, "{throwable}")
} else {
writeln!(stacktrace, "{line}")
}
}
pub(crate) fn format_frames<'s>(
stacktrace: &mut impl Write,
line: &str,
remapped: impl Iterator<Item = StackFrame<'s>>,
) -> Result<(), FmtError> {
let mut remapped = remapped.peekable();
if remapped.peek().is_none() {
return writeln!(stacktrace, "{line}");
}
for line in remapped {
writeln!(stacktrace, " {line}")?;
}
Ok(())
}
pub(crate) fn format_cause(
stacktrace: &mut impl Write,
line: &str,
cause: Option<Throwable<'_>>,
) -> Result<(), FmtError> {
if let Some(cause) = cause {
writeln!(stacktrace, "Caused by: {cause}")
} else {
writeln!(stacktrace, "{line}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stacktrace() {
let mapping = "\
com.example.MainFragment$EngineFailureException -> com.example.MainFragment$d:
com.example.MainFragment$RocketException -> com.example.MainFragment$e:
com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g:
1:1:void com.example.MainFragment$Rocket.startEngines():90:90 -> onClick
1:1:void com.example.MainFragment$Rocket.fly():83 -> onClick
1:1:void onClick(android.view.View):65 -> onClick
2:2:void com.example.MainFragment$Rocket.fly():85:85 -> onClick
2:2:void onClick(android.view.View):65 -> onClick
";
let stacktrace = StackTrace {
exception: Some(Throwable {
class: "com.example.MainFragment$e",
message: Some("Crash!"),
}),
frames: vec![
StackFrame {
class: "com.example.MainFragment$g",
method: "onClick",
line: Some(2),
file: Some(Cow::Borrowed("SourceFile")),
parameters: None,
method_synthesized: false,
},
StackFrame {
class: "android.view.View",
method: "performClick",
line: Some(7393),
file: Some(Cow::Borrowed("View.java")),
parameters: None,
method_synthesized: false,
},
],
cause: Some(Box::new(StackTrace {
exception: Some(Throwable {
class: "com.example.MainFragment$d",
message: Some("Engines overheating"),
}),
frames: vec![StackFrame {
class: "com.example.MainFragment$g",
method: "onClick",
line: Some(1),
file: Some(Cow::Borrowed("SourceFile")),
parameters: None,
method_synthesized: false,
}],
cause: None,
})),
};
let expect = "\
com.example.MainFragment$RocketException: Crash!
at com.example.MainFragment$Rocket.fly(MainFragment.java:85)
at com.example.MainFragment$onActivityCreated$4.onClick(MainFragment.java:65)
at android.view.View.performClick(View.java:7393)
Caused by: com.example.MainFragment$EngineFailureException: Engines overheating
at com.example.MainFragment$Rocket.startEngines(MainFragment.java:90)
at com.example.MainFragment$Rocket.fly(MainFragment.java:83)
at com.example.MainFragment$onActivityCreated$4.onClick(MainFragment.java:65)\n";
let mapper = ProguardMapper::from(mapping);
assert_eq!(
mapper.remap_stacktrace_typed(&stacktrace).to_string(),
expect
);
}
#[test]
fn stacktrace_str() {
let mapping = "\
com.example.MainFragment$EngineFailureException -> com.example.MainFragment$d:
com.example.MainFragment$RocketException -> com.example.MainFragment$e:
com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g:
1:1:void com.example.MainFragment$Rocket.startEngines():90:90 -> onClick
1:1:void com.example.MainFragment$Rocket.fly():83 -> onClick
1:1:void onClick(android.view.View):65 -> onClick
2:2:void com.example.MainFragment$Rocket.fly():85:85 -> onClick
2:2:void onClick(android.view.View):65 -> onClick
";
let stacktrace = "\
com.example.MainFragment$e: Crash!
at com.example.MainFragment$g.onClick(SourceFile:2)
at android.view.View.performClick(View.java:7393)
Caused by: com.example.MainFragment$d: Engines overheating
at com.example.MainFragment$g.onClick(SourceFile:1)
... 13 more";
let expect = "\
com.example.MainFragment$RocketException: Crash!
at com.example.MainFragment$Rocket.fly(MainFragment.java:85)
at com.example.MainFragment$onActivityCreated$4.onClick(MainFragment.java:65)
at android.view.View.performClick(View.java:7393)
Caused by: com.example.MainFragment$EngineFailureException: Engines overheating
at com.example.MainFragment$Rocket.startEngines(MainFragment.java:90)
at com.example.MainFragment$Rocket.fly(MainFragment.java:83)
at com.example.MainFragment$onActivityCreated$4.onClick(MainFragment.java:65)
... 13 more\n";
let mapper = ProguardMapper::from(mapping);
assert_eq!(mapper.remap_stacktrace(stacktrace).unwrap(), expect);
}
#[test]
fn rewrite_frame_remove_inner_frame() {
let mapping = "\
some.Class -> a:
4:4:void other.Class.inlinee():23:23 -> a
4:4:void caller(other.Class):7 -> a
# {\"id\":\"com.android.tools.r8.rewriteFrame\",\"conditions\":[\"throws(Ljava/lang/NullPointerException;)\"],\"actions\":[\"removeInnerFrames(1)\"]}
";
let stacktrace = "\
java.lang.NullPointerException: Boom
at a.a(SourceFile:4)";
let expect = "\
java.lang.NullPointerException: Boom
at some.Class.caller(Class.java:7)
";
let mapper = ProguardMapper::from(mapping);
assert_eq!(mapper.remap_stacktrace(stacktrace).unwrap(), expect);
}
#[test]
fn rewrite_frame_condition_mismatch() {
let mapping = "\
some.Class -> a:
4:4:void other.Class.inlinee():23:23 -> a
4:4:void caller(other.Class):7 -> a
# {\"id\":\"com.android.tools.r8.rewriteFrame\",\"conditions\":[\"throws(Ljava/lang/NullPointerException;)\"],\"actions\":[\"removeInnerFrames(1)\"]}
";
let stacktrace = "\
java.lang.IllegalStateException: Boom
at a.a(SourceFile:4)";
let expect = "\
java.lang.IllegalStateException: Boom
at other.Class.inlinee(Class.java:23)
at some.Class.caller(Class.java:7)
";
let mapper = ProguardMapper::from(mapping);
assert_eq!(mapper.remap_stacktrace(stacktrace).unwrap(), expect);
}
#[test]
fn rewrite_frame_typed_stacktrace() {
let mapping = "\
some.Class -> a:
4:4:void other.Class.inlinee():23:23 -> a
4:4:void caller(other.Class):7 -> a
# {\"id\":\"com.android.tools.r8.rewriteFrame\",\"conditions\":[\"throws(Ljava/lang/NullPointerException;)\"],\"actions\":[\"removeInnerFrames(1)\"]}
";
let trace = StackTrace {
exception: Some(Throwable {
class: "java.lang.NullPointerException",
message: Some("Boom"),
}),
frames: vec![StackFrame {
class: "a",
method: "a",
line: Some(4),
file: Some(Cow::Borrowed("SourceFile")),
parameters: None,
method_synthesized: false,
}],
cause: None,
};
let mapper = ProguardMapper::from(mapping);
let remapped = mapper.remap_stacktrace_typed(&trace);
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, Some(7));
}
#[test]
fn rewrite_frame_multiple_rules_or_semantics() {
let mapping = "\
some.Class -> a:
4:4:void other.Class.inlinee():23:23 -> call
4:4:void outer():7 -> call
# {\"id\":\"com.android.tools.r8.rewriteFrame\",\"conditions\":[\"throws(Ljava/lang/NullPointerException;)\"],\"actions\":[\"removeInnerFrames(1)\"]}
# {\"id\":\"com.android.tools.r8.rewriteFrame\",\"conditions\":[\"throws(Ljava/lang/IllegalStateException;)\"],\"actions\":[\"removeInnerFrames(1)\"]}
";
let mapper = ProguardMapper::from(mapping);
let input_npe = "\
java.lang.NullPointerException: Boom
at a.call(SourceFile:4)";
let expected_npe = "\
java.lang.NullPointerException: Boom
at some.Class.outer(Class.java:7)
";
assert_eq!(mapper.remap_stacktrace(input_npe).unwrap(), expected_npe);
let input_ise = "\
java.lang.IllegalStateException: Boom
at a.call(SourceFile:4)";
let expected_ise = "\
java.lang.IllegalStateException: Boom
at some.Class.outer(Class.java:7)
";
assert_eq!(mapper.remap_stacktrace(input_ise).unwrap(), expected_ise);
}
#[test]
fn remap_frame_without_mapping_remaps_class_best_effort() {
let mapping = "\
some.Class -> a:
1:1:void some.Class.existing():10:10 -> a
";
let mapper = ProguardMapper::from(mapping);
let input = "\
java.lang.RuntimeException: boom
at a.missing(SourceFile:42)
";
let expected = "\
java.lang.RuntimeException: boom
at some.Class.missing(Class.java:42)
";
assert_eq!(mapper.remap_stacktrace(input).unwrap(), expected);
}
#[test]
fn rewrite_frame_removes_all_frames_skips_line() {
let mapping = "\
some.Class -> a:
4:4:void inlined():10:10 -> call
4:4:void outer():20 -> call
# {\"id\":\"com.android.tools.r8.rewriteFrame\",\"conditions\":[\"throws(Ljava/lang/NullPointerException;)\"],\"actions\":[\"removeInnerFrames(2)\"]}
some.Other -> b:
5:5:void method():30 -> run
";
let mapper = ProguardMapper::from(mapping);
let input = "\
java.lang.NullPointerException: Boom
at a.call(SourceFile:4)
at b.run(SourceFile:5)
";
let expected = "\
java.lang.NullPointerException: Boom
at some.Other.method(Other.java:30)
";
let actual = mapper.remap_stacktrace(input).unwrap();
assert_eq!(actual, expected);
}
#[test]
fn rewrite_frame_removes_all_frames_skips_line_typed() {
let mapping = "\
some.Class -> a:
4:4:void inlined():10:10 -> call
4:4:void outer():20 -> call
# {\"id\":\"com.android.tools.r8.rewriteFrame\",\"conditions\":[\"throws(Ljava/lang/NullPointerException;)\"],\"actions\":[\"removeInnerFrames(2)\"]}
some.Other -> b:
5:5:void method():30 -> run
";
let mapper = ProguardMapper::from(mapping);
let trace = StackTrace {
exception: Some(Throwable {
class: "java.lang.NullPointerException",
message: Some("Boom"),
}),
frames: vec![
StackFrame {
class: "a",
method: "call",
line: Some(4),
file: Some(Cow::Borrowed("SourceFile")),
parameters: None,
method_synthesized: false,
},
StackFrame {
class: "b",
method: "run",
line: Some(5),
file: Some(Cow::Borrowed("SourceFile")),
parameters: None,
method_synthesized: false,
},
],
cause: None,
};
let remapped = mapper.remap_stacktrace_typed(&trace);
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, Some(30));
}
}