mod debug;
mod raw;
use std::borrow::Cow;
use std::cmp::Ordering;
use std::fmt::Write;
use thiserror::Error;
use crate::builder::{RewriteAction, RewriteCondition, RewriteRule};
use crate::mapper::{format_cause, format_frames, format_throwable};
use crate::utils::{class_name_to_descriptor, extract_class_name, synthesize_source_file};
use crate::{java, stacktrace, DeobfuscatedSignature, StackFrame, StackTrace, Throwable};
const MAX_SPAN_EXPANSION: u32 = 65_535;
pub use raw::{ProguardCache, PRGCACHE_VERSION};
type MemberLookupResult<'data> = (
&'data [raw::Member],
StackFrame<'data>,
Vec<RewriteRule<'data>>,
bool,
bool,
Option<&'data str>,
);
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CacheErrorKind {
#[error("endianness mismatch")]
WrongEndianness,
#[error("wrong format magic")]
WrongFormat,
#[error("unknown ProguardCache version")]
WrongVersion,
#[error("could not read header")]
InvalidHeader,
#[error("could not read classes")]
InvalidClasses,
#[error("could not read members")]
InvalidMembers,
#[error("expected {expected} string bytes, found {found}")]
UnexpectedStringBytes {
expected: usize,
found: usize,
},
}
#[derive(Debug, Error)]
#[error("{kind}")]
pub struct CacheError {
pub(crate) kind: CacheErrorKind,
#[source]
pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
}
impl CacheError {
pub fn kind(&self) -> CacheErrorKind {
self.kind
}
}
impl From<CacheErrorKind> for CacheError {
fn from(kind: CacheErrorKind) -> Self {
Self { kind, source: None }
}
}
impl<'data> ProguardCache<'data> {
fn remap_class_only(
&self,
frame: &StackFrame<'data>,
reference_file: Option<&'data str>,
) -> StackFrame<'data> {
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 get_class(&self, name: &str) -> Option<&raw::Class> {
let idx = self
.classes
.binary_search_by(|c| {
let Ok(obfuscated) = self.read_string(c.obfuscated_name_offset) else {
return Ordering::Greater;
};
obfuscated.cmp(name)
})
.ok()?;
self.classes.get(idx)
}
fn get_class_members(&self, class: &raw::Class) -> Option<&'data [raw::Member]> {
let raw::Class {
members_offset,
members_len,
..
} = class;
let start = *members_offset as usize;
let end = start.checked_add(*members_len as usize)?;
self.members.get(start..end)
}
fn get_class_members_by_params(&self, class: &raw::Class) -> Option<&'data [raw::Member]> {
let raw::Class {
members_by_params_offset,
members_by_params_len,
..
} = class;
let start = *members_by_params_offset as usize;
let end = start.checked_add(*members_by_params_len as usize)?;
self.members_by_params.get(start..end)
}
pub fn remap_class(&self, class: &str) -> Option<&'data str> {
let class = self.get_class(class)?;
self.read_string(class.original_name_offset).ok()
}
pub fn remap_method(&self, class: &str, method: &str) -> Option<(&'data str, &'data str)> {
let class = self.get_class(class)?;
let members = self.get_class_members(class)?;
let matching_members = Self::find_range_by_binary_search(members, |m| {
let Ok(obfuscated_name) = self.read_string(m.obfuscated_name_offset) else {
return Ordering::Greater;
};
obfuscated_name.cmp(method)
})?;
let mut iter = matching_members.iter();
let first = iter.next()?;
let all_matching =
iter.all(|member| member.original_name_offset == first.original_name_offset);
if !all_matching {
return None;
}
let original_class = self.read_string(class.original_name_offset).ok()?;
let original_method = self.read_string(first.original_name_offset).ok()?;
Some((original_class, original_method))
}
fn decode_rewrite_rules(&self, member: &raw::Member) -> Vec<RewriteRule<'data>> {
let mut rules = Vec::new();
let start = member.rewrite_rules_offset as usize;
let len = member.rewrite_rules_len as usize;
let Some(entries) = self
.rewrite_rule_entries
.get(start..start.saturating_add(len))
else {
return rules;
};
for entry in entries {
let mut conditions = Vec::new();
if let Some(condition_components) = self.rewrite_rule_components.get(
entry.conditions_offset as usize
..entry.conditions_offset.saturating_add(entry.conditions_len) as usize,
) {
for component in condition_components {
match component.kind {
raw::REWRITE_CONDITION_THROWS => {
if let Ok(descriptor) = self.read_string(component.value) {
conditions.push(RewriteCondition::Throws(descriptor));
}
}
raw::REWRITE_CONDITION_UNKNOWN => {
if let Ok(value) = self.read_string(component.value) {
conditions.push(RewriteCondition::Unknown(value));
}
}
_ => {}
}
}
}
let mut actions = Vec::new();
if let Some(action_components) = self.rewrite_rule_components.get(
entry.actions_offset as usize
..entry.actions_offset.saturating_add(entry.actions_len) as usize,
) {
for component in action_components {
match component.kind {
raw::REWRITE_ACTION_REMOVE_INNER_FRAMES => {
actions
.push(RewriteAction::RemoveInnerFrames(component.value as usize));
}
raw::REWRITE_ACTION_UNKNOWN => {
if let Ok(value) = self.read_string(component.value) {
actions.push(RewriteAction::Unknown(value));
}
}
_ => {}
}
}
}
if !conditions.is_empty() && !actions.is_empty() {
rules.push(RewriteRule {
conditions,
actions,
});
}
}
rules
}
fn find_members_and_rules(
&'data self,
frame: &StackFrame<'data>,
) -> Option<MemberLookupResult<'data>> {
let class = self.get_class(frame.class)?;
let original_class = self
.read_string(class.original_name_offset)
.unwrap_or(frame.class);
let outer_source_file = self.read_string(class.file_name_offset).ok();
let mut prepared_frame = frame.clone();
prepared_frame.class = original_class;
let method_name = prepared_frame.method;
let mapping_entries: &[raw::Member] = if let Some(parameters) = prepared_frame.parameters {
let members = self.get_class_members_by_params(class)?;
Self::find_range_by_binary_search(members, |m| {
let Ok(obfuscated_name) = self.read_string(m.obfuscated_name_offset) else {
return Ordering::Greater;
};
let params = if m.params_offset != u32::MAX {
self.read_string(m.params_offset).unwrap_or_default()
} else {
""
};
(obfuscated_name, params).cmp(&(method_name, parameters))
})?
} else {
let members = self.get_class_members(class)?;
Self::find_range_by_binary_search(members, |m| {
let Ok(obfuscated_name) = self.read_string(m.obfuscated_name_offset) else {
return Ordering::Greater;
};
obfuscated_name.cmp(method_name)
})?
};
let mut rewrite_rules = Vec::new();
let mut had_mappings = false;
if prepared_frame.parameters.is_none() {
for member in mapping_entries {
let pf_line = prepared_frame.line.unwrap_or(0);
let startline = member.startline().unwrap_or(0) as usize;
let endline = member.endline().unwrap_or(0) as usize;
if endline == 0 || (pf_line >= startline && pf_line <= endline) {
had_mappings = true;
rewrite_rules.extend(self.decode_rewrite_rules(member));
}
}
} else {
had_mappings = !mapping_entries.is_empty();
for member in mapping_entries {
rewrite_rules.extend(self.decode_rewrite_rules(member));
}
}
let has_line_info = mapping_entries.iter().any(|m| m.endline().unwrap_or(0) > 0);
Some((
mapping_entries,
prepared_frame,
rewrite_rules,
had_mappings,
has_line_info,
outer_source_file,
))
}
pub fn remap_frame<'r: 'data>(
&'r self,
frame: &StackFrame<'data>,
) -> RemappedFrameIter<'r, 'data> {
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();
};
RemappedFrameIter::members(
self,
prepared_frame,
members.iter(),
0,
had_mappings,
has_line_info,
outer_source_file,
)
}
pub fn remap_frame_with_context<'r>(
&'r self,
frame: &StackFrame<'data>,
exception_descriptor: Option<&str>,
apply_rewrite: bool,
carried_outline_pos: &mut Option<usize>,
) -> Option<RemappedFrameIter<'r, 'data>>
where
'r: 'data,
{
if self.is_outline_frame(frame.class, frame.method) {
*carried_outline_pos = Some(frame.line.unwrap_or(0));
return None;
}
let effective = self.prepare_frame_for_mapping(frame, carried_outline_pos);
let Some((
members,
prepared_frame,
rewrite_rules,
had_mappings,
has_line_info,
outer_source_file,
)) = self.find_members_and_rules(&effective)
else {
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();
return Some(RemappedFrameIter::single(self.remap_class_only(
&StackFrame {
class: original_class,
..effective
},
outer_source_file,
)));
}
return Some(RemappedFrameIter::empty());
};
let skip_count = if apply_rewrite {
compute_skip_count(&rewrite_rules, exception_descriptor)
} else {
0
};
Some(RemappedFrameIter::members(
self,
prepared_frame,
members.iter(),
skip_count,
had_mappings,
has_line_info,
outer_source_file,
))
}
fn find_range_by_binary_search<F>(members: &[raw::Member], f: F) -> Option<&[raw::Member]>
where
F: Fn(&raw::Member) -> std::cmp::Ordering,
{
let mid = members.binary_search_by(&f).ok()?;
let matches_not = |m: &raw::Member| f(m).is_ne();
let start = members[..mid]
.iter()
.rposition(matches_not)
.map_or(0, |idx| idx + 1);
let end = members[mid..]
.iter()
.position(matches_not)
.map_or(members.len(), |idx| idx + mid);
members.get(start..end)
}
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,
})
}
fn member_outline_pairs(&self, member: &raw::Member) -> &'data [raw::OutlinePair] {
let start = member.outline_pairs_offset as usize;
let end = start + member.outline_pairs_len as usize;
if start >= self.outline_pairs.len() || end > self.outline_pairs.len() {
&self.outline_pairs[0..0]
} else {
&self.outline_pairs[start..end]
}
}
fn map_outline_position(
&self,
class: &str,
method: &str,
callsite_line: usize,
pos: usize,
parameters: Option<&str>,
) -> Option<usize> {
let class = self.get_class(class)?;
let candidates: &[raw::Member] = if let Some(params) = parameters {
let members = self.get_class_members_by_params(class)?;
Self::find_range_by_binary_search(members, |m| {
let Ok(obfuscated_name) = self.read_string(m.obfuscated_name_offset) else {
return Ordering::Greater;
};
let p = self.read_string(m.params_offset).unwrap_or_default();
(obfuscated_name, p).cmp(&(method, params))
})?
} else {
let members = self.get_class_members(class)?;
Self::find_range_by_binary_search(members, |m| {
let Ok(obfuscated_name) = self.read_string(m.obfuscated_name_offset) else {
return Ordering::Greater;
};
obfuscated_name.cmp(method)
})?
};
candidates
.iter()
.filter(|m| {
m.endline().unwrap_or(0) == 0
|| (callsite_line >= m.startline().unwrap_or(0) as usize
&& callsite_line <= m.endline().unwrap_or(0) as usize)
})
.find_map(|m| {
self.member_outline_pairs(m)
.iter()
.find(|pair| pair.outline_pos as usize == pos)
.map(|pair| pair.callsite_line as usize)
})
}
pub fn is_outline_frame(&self, class: &str, method: &str) -> bool {
let Some(class) = self.get_class(class) else {
return false;
};
let Some(members) = self.get_class_members(class) else {
return false;
};
let Some(candidates) = Self::find_range_by_binary_search(members, |m| {
let Ok(obfuscated_name) = self.read_string(m.obfuscated_name_offset) else {
return Ordering::Greater;
};
obfuscated_name.cmp(method)
}) else {
return false;
};
candidates.first().is_some_and(|member| member.is_outline())
}
pub 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_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) {
let Some(iter) = self.remap_frame_with_context(
&frame,
current_exception_descriptor.as_deref(),
next_frame_can_rewrite,
&mut carried_outline_pos,
) else {
continue;
};
next_frame_can_rewrite = false;
current_exception_descriptor = None;
let had_mappings = iter.had_mappings();
let frames: Vec<_> = iter.collect();
if had_mappings && frames.is_empty() {
continue;
}
format_frames(&mut stacktrace, line, 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 = Vec::with_capacity(trace.frames.len());
let mut next_frame_can_rewrite = exception_descriptor.is_some();
for f in trace.frames.iter() {
let Some(iter) = self.remap_frame_with_context(
f,
exception_descriptor.as_deref(),
next_frame_can_rewrite,
&mut carried_outline_pos,
) else {
continue;
};
next_frame_can_rewrite = false;
let had_mappings = iter.had_mappings();
let mut remapped: Vec<_> = iter.collect();
if had_mappings && remapped.is_empty() {
continue;
}
if remapped.is_empty() {
frames.push(f.clone());
} else {
frames.append(&mut remapped);
}
}
let cause = trace
.cause
.as_ref()
.map(|c| Box::new(self.remap_stacktrace_typed(c)));
StackTrace {
exception,
frames,
cause,
}
}
pub fn deobfuscate_signature(&self, signature: &str) -> Option<DeobfuscatedSignature> {
java::deobfuscate_bytecode_signature_cache(signature, self).map(DeobfuscatedSignature::new)
}
}
#[derive(Clone, Debug)]
pub struct RemappedFrameIter<'r, 'data> {
inner: Option<(
&'r ProguardCache<'data>,
StackFrame<'data>,
std::slice::Iter<'data, raw::Member>,
)>,
fallback: Option<StackFrame<'data>>,
pending_frames: Vec<StackFrame<'data>>,
skip_count: usize,
had_mappings: bool,
has_line_info: bool,
matched_any: bool,
outer_source_file: Option<&'data str>,
}
impl<'r, 'data> RemappedFrameIter<'r, 'data> {
fn empty() -> Self {
Self {
inner: None,
fallback: None,
pending_frames: Vec::new(),
skip_count: 0,
had_mappings: false,
has_line_info: false,
matched_any: false,
outer_source_file: None,
}
}
fn members(
cache: &'r ProguardCache<'data>,
frame: StackFrame<'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,
pending_frames: Vec::new(),
skip_count,
had_mappings,
has_line_info,
matched_any: false,
outer_source_file,
}
}
fn single(frame: StackFrame<'data>) -> Self {
Self {
inner: None,
fallback: Some(frame),
pending_frames: Vec::new(),
skip_count: 0,
had_mappings: false,
has_line_info: false,
matched_any: false,
outer_source_file: None,
}
}
pub fn had_mappings(&self) -> bool {
self.had_mappings
}
fn next_inner(&mut self) -> Option<StackFrame<'data>> {
if !self.pending_frames.is_empty() {
return self.pending_frames.pop();
}
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 frame.line.unwrap_or(0) == 0 {
let mut frames = resolve_no_line_frames(
cache,
&frame,
members.as_slice(),
self.outer_source_file,
);
frames.reverse();
if let Some(first) = frames.pop() {
self.pending_frames = frames;
return Some(first);
}
return None;
}
let mapped = iterate_with_lines(
cache,
&mut frame,
&mut members,
self.outer_source_file,
self.has_line_info,
&mut self.pending_frames,
);
if mapped.is_some() {
self.matched_any = true;
self.inner = Some((cache, frame, members));
mapped
} else if !self.matched_any && self.has_line_info {
Some(cache.remap_class_only(&frame, self.outer_source_file))
} else {
None
}
} else {
let mapped =
iterate_without_lines(cache, &mut frame, &mut members, self.outer_source_file);
self.inner = Some((cache, frame, members));
mapped
};
out
}
}
impl<'data> Iterator for RemappedFrameIter<'_, 'data> {
type Item = StackFrame<'data>;
fn next(&mut self) -> Option<Self::Item> {
while self.skip_count > 0 {
self.skip_count -= 1;
self.next_inner()?;
}
self.next_inner()
}
}
fn iterate_with_lines<'a>(
cache: &ProguardCache<'a>,
frame: &mut StackFrame<'a>,
members: &mut std::slice::Iter<'_, raw::Member>,
outer_source_file: Option<&str>,
has_line_info: bool,
pending_frames: &mut Vec<StackFrame<'a>>,
) -> 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 {
if member.original_startline().is_none() {
return map_member_without_lines(
cache,
frame,
member,
outer_source_file,
frame.line,
);
}
if member.original_endline != u32::MAX
&& member.original_endline > member.original_startline().unwrap_or(0)
&& (member.original_endline - member.original_startline().unwrap_or(0))
<= MAX_SPAN_EXPANSION
{
let first_line = member.original_startline().unwrap_or(0) as usize;
let last_line = member.original_endline as usize;
let mut first_frame = None;
for line in first_line..=last_line {
if let Some(f) = map_member_without_lines(
cache,
frame,
member,
outer_source_file,
Some(line),
) {
if first_frame.is_none() {
first_frame = Some(f);
} else {
pending_frames.push(f);
}
}
}
pending_frames.reverse();
return first_frame;
}
let output_line = if member.original_startline().unwrap_or(0) > 0 {
Some(member.original_startline().unwrap_or(0) as usize)
} else {
None
};
return map_member_without_lines(cache, frame, member, outer_source_file, output_line);
}
if member.endline().unwrap_or(0) > 0
&& (frame_line < member.startline().unwrap_or(0) as usize
|| frame_line > member.endline().unwrap_or(0) as usize)
{
continue;
}
let line = if member.original_endline == u32::MAX
|| member.original_endline == member.original_startline().unwrap_or(0)
{
member.original_startline().unwrap_or(0) as usize
} else {
member.original_startline().unwrap_or(0) as usize + frame_line
- member.startline().unwrap_or(0) as usize
};
let class = cache
.read_string(member.original_class_offset)
.unwrap_or(frame.class);
let file: Option<Cow<'_, str>> = if member.original_file_offset != u32::MAX {
let Ok(file_name) = cache.read_string(member.original_file_offset) else {
continue;
};
if file_name == "R8$$SyntheticClass" {
extract_class_name(class).map(Cow::Borrowed)
} else {
Some(Cow::Borrowed(file_name))
}
} else {
synthesize_source_file(class, outer_source_file).map(Cow::Owned)
};
let Ok(method) = cache.read_string(member.original_name_offset) else {
continue;
};
return Some(StackFrame {
class,
method,
file,
line: Some(line),
parameters: frame.parameters,
method_synthesized: member.is_synthesized(),
});
}
None
}
fn map_member_without_lines<'a>(
cache: &ProguardCache<'a>,
frame: &StackFrame<'a>,
member: &raw::Member,
outer_source_file: Option<&str>,
output_line: Option<usize>,
) -> Option<StackFrame<'a>> {
let class = cache
.read_string(member.original_class_offset)
.unwrap_or(frame.class);
let method = cache.read_string(member.original_name_offset).ok()?;
let file = synthesize_source_file(class, outer_source_file).map(Cow::Owned);
Some(StackFrame {
class,
method,
file,
line: output_line,
parameters: frame.parameters,
method_synthesized: member.is_synthesized(),
})
}
fn compute_member_output_line(member: &raw::Member) -> Option<usize> {
member
.original_startline()
.filter(|&v| v > 0)
.map(|v| v as usize)
}
fn iterate_without_lines<'a>(
cache: &ProguardCache<'a>,
frame: &mut StackFrame<'a>,
members: &mut std::slice::Iter<'_, raw::Member>,
outer_source_file: Option<&str>,
) -> Option<StackFrame<'a>> {
let member = members.next()?;
let output_line = compute_member_output_line(member);
map_member_without_lines(cache, frame, member, outer_source_file, output_line)
}
fn resolve_no_line_frames<'a>(
cache: &ProguardCache<'a>,
frame: &StackFrame<'a>,
members: &[raw::Member],
outer_source_file: Option<&str>,
) -> Vec<StackFrame<'a>> {
let base_entries: Vec<&raw::Member> = members
.iter()
.filter(|m| m.endline().unwrap_or(0) == 0)
.collect();
if !base_entries.is_empty() {
return resolve_base_entries(cache, frame, &base_entries, outer_source_file);
}
let mut frames = Vec::new();
if let Some(first) = members.first() {
let first_start = first.startline();
let first_end = first.endline();
let first_group: Vec<_> = members
.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 = compute_member_output_line(member).or(Some(0));
if let Some(f) =
map_member_without_lines(cache, frame, member, outer_source_file, line)
{
frames.push(f);
}
}
} else {
let all_same = members.iter().all(|m| {
m.original_class_offset == first.original_class_offset
&& m.original_name_offset == first.original_name_offset
});
if all_same {
if let Some(f) =
map_member_without_lines(cache, frame, first, outer_source_file, Some(0))
{
frames.push(f);
}
} else {
for member in members {
if let Some(f) =
map_member_without_lines(cache, frame, member, outer_source_file, Some(0))
{
frames.push(f);
}
}
}
}
}
frames
}
fn resolve_base_entries<'a>(
cache: &ProguardCache<'a>,
frame: &StackFrame<'a>,
base_entries: &[&raw::Member],
outer_source_file: Option<&str>,
) -> Vec<StackFrame<'a>> {
let mut any_zero_zero_has_range = false;
let mut no_range_count = 0usize;
let mut first_no_range_offset: Option<u32> = 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 != u32::MAX
&& member.original_endline != member.original_startline().unwrap_or(0)
{
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_offset {
None => first_no_range_offset = Some(member.original_name_offset),
Some(first) if member.original_name_offset != first => {
all_no_range_same_name = false
}
_ => {}
}
}
}
let mut frames = Vec::new();
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 {
compute_member_output_line(member)
};
if let Some(f) = map_member_without_lines(cache, frame, member, outer_source_file, line)
{
frames.push(f);
}
} else if all_no_range_same_name {
if !no_range_emitted {
no_range_emitted = true;
let line = if no_range_count > 1 {
Some(0)
} else {
compute_member_output_line(member).or(Some(0))
};
if let Some(f) =
map_member_without_lines(cache, frame, member, outer_source_file, line)
{
frames.push(f);
}
}
} else if let Some(f) =
map_member_without_lines(cache, frame, member, outer_source_file, Some(0))
{
frames.push(f);
}
}
if !all_no_range_same_name && all_no_range_have_line_mapping {
frames.sort_by(|a, b| a.method.cmp(b.method));
}
frames
}
fn compute_skip_count(rewrite_rules: &[RewriteRule<'_>], thrown_descriptor: Option<&str>) -> usize {
let mut skip_count = 0;
for rule in 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 {
if let RewriteAction::RemoveInnerFrames(count) = action {
skip_count += count;
}
}
}
skip_count
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use crate::{ProguardMapping, StackFrame, StackTrace, Throwable};
use super::raw::ProguardCache;
#[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 mapping = ProguardMapping::new(mapping.as_bytes());
let mut cache = Vec::new();
ProguardCache::write(&mapping, &mut cache).unwrap();
let cache = ProguardCache::parse(&cache).unwrap();
cache.test();
assert_eq!(
cache.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 mapping = ProguardMapping::new(mapping.as_bytes());
let mut cache = Vec::new();
ProguardCache::write(&mapping, &mut cache).unwrap();
let cache = ProguardCache::parse(&cache).unwrap();
cache.test();
assert_eq!(cache.remap_stacktrace(stacktrace).unwrap(), expect);
}
#[test]
fn rewrite_frame_cache_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 mapping = ProguardMapping::new(mapping.as_bytes());
let mut buf = Vec::new();
ProguardCache::write(&mapping, &mut buf).unwrap();
let cache = ProguardCache::parse(&buf).unwrap();
let input = "\
java.lang.NullPointerException: Boom
at a.a(SourceFile:4)";
let expect = "\
java.lang.NullPointerException: Boom
at some.Class.caller(Class.java:7)
";
assert_eq!(cache.remap_stacktrace(input).unwrap(), expect);
}
#[test]
fn rewrite_frame_cache_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 mapping = ProguardMapping::new(mapping.as_bytes());
let mut buf = Vec::new();
ProguardCache::write(&mapping, &mut buf).unwrap();
let cache = ProguardCache::parse(&buf).unwrap();
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!(cache.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!(cache.remap_stacktrace(input_ise).unwrap(), expected_ise);
}
#[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 mapping = ProguardMapping::new(mapping.as_bytes());
let mut buf = Vec::new();
ProguardCache::write(&mapping, &mut buf).unwrap();
let cache = ProguardCache::parse(&buf).unwrap();
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 = cache.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 mapping = ProguardMapping::new(mapping.as_bytes());
let mut buf = Vec::new();
ProguardCache::write(&mapping, &mut buf).unwrap();
let cache = ProguardCache::parse(&buf).unwrap();
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 = cache.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));
}
}