diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 16308e9..05dad98 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -6,11 +6,13 @@ //! - the format version, //! - the number of class, member, and member-by-params entries, //! - the number of outline mapping pairs, +//! - the number of rewrite rule entries and components, //! - and the length of the string section; //! * A [list](ProguardCache::classes) of [`Class`](raw::Class) entries; //! * A [list](ProguardCache::members) of [`Member`](raw::Member) entries; -//! * Another [list](Proguard_cache::members_by_params) of `Member` entries, sorted by parameter strings; +//! * Another [list](ProguardCache::members_by_params) of `Member` entries, sorted by parameter strings; //! * A [list] of outline mapping pairs shared by all members; +//! * A [list] of rewrite rule entries and their components; //! * A [string section](ProguardCache::string_bytes) in which class names, method names, &c. are collected. //! Whenever a class or member entry references a string, it is by offset into this section. //! @@ -30,8 +32,8 @@ //! * a params string, //! * an `is_synthesized` flag, //! * an `is_outline` flag designating outline methods, -//! * an `outline_pairs_offset` and `outline_pairs_len` which slice into the global outline -//! pairs section. +//! * an `outline_pairs_offset` and `outline_pairs_len` which slice into the global outline pairs section. +//! * an `rewrite_rules_offset` and `rewrite_rules_len` which slice into the global rewrite rule entries section. //! //! It may also contain //! * an original class name, @@ -51,6 +53,21 @@ //! Each [`Member`](raw::Member) that carries outline callsite information references a sub-slice of this //! section via its `outline_pairs_offset` and `outline_pairs_len`. This keeps members fixed-size and //! enables zero-copy parsing while supporting variable-length metadata. +//! +//! ## Rewrite rules section +//! Rewrite rules are R8's mechanism for post-processing stack frames during deobfuscation. +//! They are stored in two flat arrays: +//! +//! * **Rewrite rule entries**: Each [`RewriteRuleEntry`](raw::RewriteRuleEntry) contains offsets and +//! lengths into the components array for its conditions and actions. +//! * **Rewrite rule components**: A flat array of [`RewriteComponent`](raw::RewriteComponent) entries, +//! each representing either a condition (e.g., `throws(Ljava/lang/NullPointerException;)`) or an +//! action (e.g., `removeInnerFrames(1)`). +//! +//! Each [`Member`](raw::Member) references its rewrite rules via `rewrite_rules_offset` and +//! `rewrite_rules_len`, which slice into the entries array. This two-level indirection keeps +//! members fixed-size while supporting variable numbers of rules with variable numbers of +//! conditions and actions. mod debug; mod raw; @@ -60,6 +77,7 @@ use std::fmt::Write; use thiserror::Error; +use crate::builder::{RewriteAction, RewriteCondition, RewriteRule}; use crate::mapper::{format_cause, format_frames, format_throwable}; use crate::{java, stacktrace, DeobfuscatedSignature, StackFrame, StackTrace, Throwable}; @@ -221,6 +239,146 @@ impl<'data> ProguardCache<'data> { Some((original_class, original_method)) } + fn decode_rewrite_rules(&self, member: &raw::Member) -> Vec> { + 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)); + } + } + _ => {} + } + } + } + + // Only add rules with at least one condition and one action, + // matching the validation in parse_rewrite_rule. + // This guards against corrupt cache data where read_string fails, + // which would otherwise result in empty conditions that match + // any exception due to vacuous truth in iter().all(). + if !conditions.is_empty() && !actions.is_empty() { + rules.push(RewriteRule { + conditions, + actions, + }); + } + } + + rules + } + + /// Finds member entries for a frame and collects rewrite rules without building frames. + /// Returns (member_slice, prepared_frame, rewrite_rules, had_mappings). + fn find_members_and_rules( + &'data self, + frame: &StackFrame<'data>, + ) -> Option<( + &'data [raw::Member], + StackFrame<'data>, + Vec>, + bool, + )> { + let class = self.get_class(frame.class)?; + let original_class = self + .read_string(class.original_name_offset) + .unwrap_or(frame.class); + + 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) + })? + }; + + // Collect rewrite rules and check had_mappings by iterating members + let mut rewrite_rules = Vec::new(); + let mut had_mappings = false; + + if prepared_frame.parameters.is_none() { + for member in mapping_entries { + // Check if this member would produce a frame (line matching) + if member.endline == 0 + || (prepared_frame.line >= member.startline as usize + && prepared_frame.line <= member.endline as usize) + { + had_mappings = true; + rewrite_rules.extend(self.decode_rewrite_rules(member)); + } + } + } else { + // With parameters, all members match + had_mappings = !mapping_entries.is_empty(); + for member in mapping_entries { + rewrite_rules.extend(self.decode_rewrite_rules(member)); + } + } + + Some((mapping_entries, prepared_frame, rewrite_rules, had_mappings)) + } + /// Remaps a single Stackframe. /// /// Returns zero or more [`StackFrame`]s, based on the information in @@ -284,6 +442,62 @@ impl<'data> ProguardCache<'data> { } } + /// Remaps a single stack frame through the complete processing pipeline. + /// + /// This method combines: + /// - Outline frame detection via [`is_outline_frame`](Self::is_outline_frame) + /// - Frame preparation via [`prepare_frame_for_mapping`](Self::prepare_frame_for_mapping) + /// - Lazy frame remapping with rewrite rules applied via skip_count + /// + /// # Arguments + /// * `frame` - The frame to remap + /// * `exception_descriptor` - Optional exception descriptor for rewrite rules (e.g., `Ljava/lang/NullPointerException;`) + /// * `apply_rewrite` - Whether to apply rewrite rules (typically true only for the first frame after an exception) + /// * `carried_outline_pos` - Mutable reference to track outline position across frames + /// + /// # Returns + /// - `None` if this is an outline frame (caller should skip, `carried_outline_pos` is updated internally) + /// - `Some(iterator)` with remapped frames. Use [`RemappedFrameIter::had_mappings`] after collecting + /// to detect if rewrite rules cleared all frames (skip if `had_mappings() && collected.is_empty()`) + pub fn remap_frame_with_context<'r>( + &'r self, + frame: &StackFrame<'data>, + exception_descriptor: Option<&str>, + apply_rewrite: bool, + carried_outline_pos: &mut Option, + ) -> Option> + where + 'r: 'data, + { + if self.is_outline_frame(frame.class, frame.method) { + *carried_outline_pos = Some(frame.line); + return None; + } + + let effective = self.prepare_frame_for_mapping(frame, carried_outline_pos); + + let Some((members, prepared_frame, rewrite_rules, had_mappings)) = + self.find_members_and_rules(&effective) + else { + return Some(RemappedFrameIter::empty()); + }; + + // Compute skip_count from rewrite rules + let skip_count = if apply_rewrite { + compute_skip_count(&rewrite_rules, exception_descriptor) + } else { + 0 + }; + + Some(RemappedFrameIter::new( + self, + prepared_frame, + members.iter(), + skip_count, + had_mappings, + )) + } + /// Finds the range of elements of `members` for which `f(m) == Ordering::Equal`. /// /// This works by first binary searching for any element fitting the criteria @@ -450,57 +664,67 @@ impl<'data> ProguardCache<'data> { /// strings as input and output. pub fn remap_stacktrace(&self, input: &str) -> Result { let mut stacktrace = String::new(); - let mut lines = input.lines(); - let mut carried_outline_pos: Option = None; - - if let Some(line) = lines.next() { - match stacktrace::parse_throwable(line) { - None => match stacktrace::parse_frame(line) { - None => writeln!(&mut stacktrace, "{line}")?, - Some(frame) => { - if self.is_outline_frame(frame.class, frame.method) { - carried_outline_pos = Some(frame.line); - } else { - let effective_frame = - self.prepare_frame_for_mapping(&frame, &mut carried_outline_pos); - - format_frames( - &mut stacktrace, - line, - self.remap_frame(&effective_frame), - )?; - } - } - }, - Some(throwable) => { - format_throwable(&mut stacktrace, line, self.remap_throwable(&throwable))? - } + let mut current_exception_descriptor: Option = 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; } - } - for line in lines { - match stacktrace::parse_frame(line) { - None => match line - .strip_prefix("Caused by: ") - .and_then(stacktrace::parse_throwable) - { - None => writeln!(&mut stacktrace, "{line}")?, - Some(cause) => { - format_cause(&mut stacktrace, line, self.remap_throwable(&cause))? - } - }, - Some(frame) => { - if self.is_outline_frame(frame.class, frame.method) { - carried_outline_pos = Some(frame.line); - 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 { + // Outline frame, skip (preserve next_frame_can_rewrite for the next real frame) + continue; + }; + + next_frame_can_rewrite = false; + current_exception_descriptor = None; + + let had_mappings = iter.had_mappings(); + let frames: Vec<_> = iter.collect(); - let effective_frame = - self.prepare_frame_for_mapping(&frame, &mut carried_outline_pos); - format_frames(&mut stacktrace, line, self.remap_frame(&effective_frame))?; + if had_mappings && frames.is_empty() { + // Rewrite rules cleared all frames, skip + 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) } @@ -511,21 +735,41 @@ impl<'data> ProguardCache<'data> { .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 = None; - let mut frames: Vec> = Vec::with_capacity(trace.frames.len()); + 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() { - if self.is_outline_frame(f.class, f.method) { - carried_outline_pos = Some(f.line); + let Some(iter) = self.remap_frame_with_context( + f, + exception_descriptor.as_deref(), + next_frame_can_rewrite, + &mut carried_outline_pos, + ) else { + // Outline frame, skip (preserve next_frame_can_rewrite for the next real frame) + continue; + }; + next_frame_can_rewrite = false; + + let had_mappings = iter.had_mappings(); + let mut remapped: Vec<_> = iter.collect(); + + if had_mappings && remapped.is_empty() { + // Rewrite rules cleared all frames, skip continue; } - let effective = self.prepare_frame_for_mapping(f, &mut carried_outline_pos); - let mut iter = self.remap_frame(&effective).peekable(); - if iter.peek().is_some() { - frames.extend(iter); - } else { + if remapped.is_empty() { frames.push(f.clone()); + } else { + frames.append(&mut remapped); } } @@ -558,28 +802,56 @@ pub struct RemappedFrameIter<'r, 'data> { StackFrame<'data>, std::slice::Iter<'data, raw::Member>, )>, + /// Number of frames to skip from rewrite rules. + skip_count: usize, + /// Whether there were mapping entries (for should_skip determination). + had_mappings: bool, } -impl<'data> RemappedFrameIter<'_, 'data> { +impl<'r, 'data> RemappedFrameIter<'r, 'data> { fn empty() -> Self { - Self { inner: None } + Self { + inner: None, + skip_count: 0, + had_mappings: false, + } } fn members( - cache: &'data ProguardCache<'data>, + cache: &'r ProguardCache<'data>, frame: StackFrame<'data>, members: std::slice::Iter<'data, raw::Member>, ) -> Self { Self { inner: Some((cache, frame, members)), + skip_count: 0, + had_mappings: false, } } -} -impl<'data> Iterator for RemappedFrameIter<'_, 'data> { - type Item = StackFrame<'data>; + fn new( + cache: &'r ProguardCache<'data>, + frame: StackFrame<'data>, + members: std::slice::Iter<'data, raw::Member>, + skip_count: usize, + had_mappings: bool, + ) -> Self { + Self { + inner: Some((cache, frame, members)), + skip_count, + had_mappings, + } + } - fn next(&mut self) -> Option { + /// Returns whether there were mapping entries before rewrite rules were applied. + /// + /// After collecting frames, if `had_mappings()` is true but the result is empty, + /// it means rewrite rules cleared all frames and the caller should skip this frame. + pub fn had_mappings(&self) -> bool { + self.had_mappings + } + + fn next_inner(&mut self) -> Option> { let (cache, frame, members) = self.inner.as_mut()?; if frame.parameters.is_none() { iterate_with_lines(cache, frame, members) @@ -589,6 +861,19 @@ impl<'data> Iterator for RemappedFrameIter<'_, 'data> { } } +impl<'data> Iterator for RemappedFrameIter<'_, 'data> { + type Item = StackFrame<'data>; + + fn next(&mut self) -> Option { + // Lazily skip rewrite-removed frames + 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>, @@ -678,6 +963,42 @@ fn extract_class_name(full_path: &str) -> Option<&str> { after_last_period.split('$').next() } +/// Converts a Java class name to its JVM descriptor format. +/// +/// For example, `java.lang.NullPointerException` becomes `Ljava/lang/NullPointerException;`. +pub fn class_name_to_descriptor(class: &str) -> String { + let mut descriptor = String::with_capacity(class.len() + 2); + descriptor.push('L'); + descriptor.push_str(&class.replace('.', "/")); + descriptor.push(';'); + descriptor +} + +/// Computes the number of frames to skip based on rewrite rules. +/// Returns the total skip count from all matching RemoveInnerFrames actions. +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 crate::{ProguardMapping, StackFrame, StackTrace, Throwable}; @@ -801,4 +1122,148 @@ Caused by: com.example.MainFragment$EngineFailureException: Engines overheating 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(SourceFile: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(SourceFile: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(SourceFile:7) +"; + assert_eq!(cache.remap_stacktrace(input_ise).unwrap(), expected_ise); + } + + #[test] + fn rewrite_frame_removes_all_frames_skips_line() { + // When rewrite rules remove ALL frames, the line should be skipped entirely + // (not fall back to original obfuscated frame) + 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) +"; + + // The first frame (a.call) should be completely removed by rewrite rules, + // not replaced with the original "at a.call(SourceFile:4)" + let expected = "\ +java.lang.NullPointerException: Boom + at some.Other.method(SourceFile:30) +"; + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn rewrite_frame_removes_all_frames_skips_line_typed() { + // When rewrite rules remove ALL frames, the frame should be skipped entirely + // (not fall back to original obfuscated frame) + 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: 4, + file: Some("SourceFile"), + parameters: None, + method_synthesized: false, + }, + StackFrame { + class: "b", + method: "run", + line: 5, + file: Some("SourceFile"), + parameters: None, + method_synthesized: false, + }, + ], + cause: None, + }; + + let remapped = cache.remap_stacktrace_typed(&trace); + + // The first frame should be completely removed by rewrite rules, + // leaving only the second frame + 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, 30); + } } diff --git a/src/cache/raw.rs b/src/cache/raw.rs index cc402a3..a7197ff 100644 --- a/src/cache/raw.rs +++ b/src/cache/raw.rs @@ -19,7 +19,7 @@ pub(crate) const PRGCACHE_MAGIC: u32 = u32::from_le_bytes(PRGCACHE_MAGIC_BYTES); pub(crate) const PRGCACHE_MAGIC_FLIPPED: u32 = PRGCACHE_MAGIC.swap_bytes(); /// The current version of the ProguardCache format. -pub const PRGCACHE_VERSION: u32 = 3; +pub const PRGCACHE_VERSION: u32 = 4; /// The header of a proguard cache file. #[derive(Debug, Clone, PartialEq, Eq)] @@ -37,6 +37,10 @@ pub(crate) struct Header { pub(crate) num_members_by_params: u32, /// The total number of outline mapping pairs across all members. pub(crate) num_outline_pairs: u32, + /// The total number of rewrite rule entries across all members. + pub(crate) num_rewrite_rule_entries: u32, + /// The total number of rewrite rule components across all members. + pub(crate) num_rewrite_rule_components: u32, /// The number of string bytes in this cache. pub(crate) string_bytes: u32, } @@ -120,6 +124,10 @@ pub(crate) struct Member { pub(crate) outline_pairs_offset: u32, /// Number of outline pairs for this member. pub(crate) outline_pairs_len: u32, + /// Offset into the rewrite rule entries section for this member. + pub(crate) rewrite_rules_offset: u32, + /// Number of rewrite rule entries for this member. + pub(crate) rewrite_rules_len: u32, /// Whether this member was synthesized by the compiler. /// /// `0` means `false`, all other values mean `true`. @@ -157,6 +165,32 @@ pub(crate) struct OutlinePair { unsafe impl Pod for OutlinePair {} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub(crate) struct RewriteRuleEntry { + pub(crate) conditions_offset: u32, + pub(crate) conditions_len: u32, + pub(crate) actions_offset: u32, + pub(crate) actions_len: u32, +} + +unsafe impl Pod for RewriteRuleEntry {} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub(crate) struct RewriteComponent { + pub(crate) kind: u32, + pub(crate) value: u32, +} + +unsafe impl Pod for RewriteComponent {} + +pub(crate) const REWRITE_CONDITION_THROWS: u32 = 0; +pub(crate) const REWRITE_CONDITION_UNKNOWN: u32 = u32::MAX; + +pub(crate) const REWRITE_ACTION_REMOVE_INNER_FRAMES: u32 = 0; +pub(crate) const REWRITE_ACTION_UNKNOWN: u32 = u32::MAX; + /// The serialized `ProguardCache` binary format. #[derive(Clone, PartialEq, Eq)] pub struct ProguardCache<'data> { @@ -178,6 +212,10 @@ pub struct ProguardCache<'data> { pub(crate) members_by_params: &'data [Member], /// A flat list of outline mapping pairs. pub(crate) outline_pairs: &'data [OutlinePair], + /// A flat list of rewrite rule entries. + pub(crate) rewrite_rule_entries: &'data [RewriteRuleEntry], + /// A flat list of rewrite rule components. + pub(crate) rewrite_rule_components: &'data [RewriteComponent], /// The collection of all strings in the cache file. pub(crate) string_bytes: &'data [u8], } @@ -226,6 +264,16 @@ impl<'data> ProguardCache<'data> { OutlinePair::slice_from_prefix(rest, header.num_outline_pairs as usize) .ok_or(CacheErrorKind::InvalidMembers)?; + let (_, rest) = watto::align_to(rest, 8).ok_or(CacheErrorKind::InvalidMembers)?; + let (rewrite_rule_entries, rest) = + RewriteRuleEntry::slice_from_prefix(rest, header.num_rewrite_rule_entries as usize) + .ok_or(CacheErrorKind::InvalidMembers)?; + + let (_, rest) = watto::align_to(rest, 8).ok_or(CacheErrorKind::InvalidMembers)?; + let (rewrite_rule_components, rest) = + RewriteComponent::slice_from_prefix(rest, header.num_rewrite_rule_components as usize) + .ok_or(CacheErrorKind::InvalidMembers)?; + let (_, string_bytes) = watto::align_to(rest, 8).ok_or(CacheErrorKind::UnexpectedStringBytes { expected: header.string_bytes as usize, @@ -246,6 +294,8 @@ impl<'data> ProguardCache<'data> { members, members_by_params, outline_pairs, + rewrite_rule_entries, + rewrite_rule_components, string_bytes, }) } @@ -341,6 +391,8 @@ impl<'data> ProguardCache<'data> { let mut members: Vec = Vec::with_capacity(num_members as usize); let mut members_by_params: Vec = Vec::with_capacity(num_members_by_params as usize); let mut outline_pairs: Vec = Vec::new(); + let mut rewrite_rule_entries: Vec = Vec::new(); + let mut rewrite_rule_components: Vec = Vec::new(); for mut c in classes.into_values() { // Set offsets relative to current vector sizes @@ -348,33 +400,75 @@ impl<'data> ProguardCache<'data> { c.class.members_by_params_offset = members_by_params.len() as u32; // Serialize members without params - for (_method, ms) in c.members.into_iter() { - for mut mp in ms.into_iter() { + for (_method, ms) in c.members { + for mut mp in ms { let start = outline_pairs.len() as u32; if !mp.outline_pairs.is_empty() { mp.member.outline_pairs_offset = start; mp.member.outline_pairs_len = mp.outline_pairs.len() as u32; - outline_pairs.extend(mp.outline_pairs.into_iter()); + outline_pairs.extend(mp.outline_pairs); } else { mp.member.outline_pairs_offset = start; mp.member.outline_pairs_len = 0; } + + let rule_start = rewrite_rule_entries.len() as u32; + let mut rule_count = 0; + for rule in mp.rewrite_rules { + let cond_start = rewrite_rule_components.len() as u32; + rewrite_rule_components.extend(rule.conditions); + let cond_len = rewrite_rule_components.len() as u32 - cond_start; + let action_start = rewrite_rule_components.len() as u32; + rewrite_rule_components.extend(rule.actions); + let action_len = rewrite_rule_components.len() as u32 - action_start; + rewrite_rule_entries.push(RewriteRuleEntry { + conditions_offset: cond_start, + conditions_len: cond_len, + actions_offset: action_start, + actions_len: action_len, + }); + rule_count += 1; + } + mp.member.rewrite_rules_offset = rule_start; + mp.member.rewrite_rules_len = rule_count; + members.push(mp.member); } } // Serialize members by params - for (_key, ms) in c.members_by_params.into_iter() { - for mut mp in ms.into_iter() { + for (_key, ms) in c.members_by_params { + for mut mp in ms { let start = outline_pairs.len() as u32; if !mp.outline_pairs.is_empty() { mp.member.outline_pairs_offset = start; mp.member.outline_pairs_len = mp.outline_pairs.len() as u32; - outline_pairs.extend(mp.outline_pairs.into_iter()); + outline_pairs.extend(mp.outline_pairs); } else { mp.member.outline_pairs_offset = start; mp.member.outline_pairs_len = 0; } + + let rule_start = rewrite_rule_entries.len() as u32; + let mut rule_count = 0; + for rule in mp.rewrite_rules { + let cond_start = rewrite_rule_components.len() as u32; + rewrite_rule_components.extend(rule.conditions); + let cond_len = rewrite_rule_components.len() as u32 - cond_start; + let action_start = rewrite_rule_components.len() as u32; + rewrite_rule_components.extend(rule.actions); + let action_len = rewrite_rule_components.len() as u32 - action_start; + rewrite_rule_entries.push(RewriteRuleEntry { + conditions_offset: cond_start, + conditions_len: cond_len, + actions_offset: action_start, + actions_len: action_len, + }); + rule_count += 1; + } + mp.member.rewrite_rules_offset = rule_start; + mp.member.rewrite_rules_len = rule_count; + members_by_params.push(mp.member); } } @@ -383,6 +477,8 @@ impl<'data> ProguardCache<'data> { } let num_outline_pairs = outline_pairs.len() as u32; + let num_rewrite_rule_entries = rewrite_rule_entries.len() as u32; + let num_rewrite_rule_components = rewrite_rule_components.len() as u32; let header = Header { magic: PRGCACHE_MAGIC, @@ -391,6 +487,8 @@ impl<'data> ProguardCache<'data> { num_members, num_members_by_params, num_outline_pairs, + num_rewrite_rule_entries, + num_rewrite_rule_components, string_bytes: string_bytes.len() as u32, }; @@ -415,6 +513,12 @@ impl<'data> ProguardCache<'data> { writer.write_all(outline_pairs.as_bytes())?; writer.align_to(8)?; + writer.write_all(rewrite_rule_entries.as_bytes())?; + writer.align_to(8)?; + + writer.write_all(rewrite_rule_components.as_bytes())?; + writer.align_to(8)?; + // Write strings writer.write_all(&string_bytes)?; @@ -465,7 +569,57 @@ impl<'data> ProguardCache<'data> { }) .unwrap_or_default(); - let member = Member { + let rewrite_rules = member + .rewrite_rules + .iter() + .map(|rule| { + let mut conditions = Vec::new(); + for condition in &rule.conditions { + match condition { + builder::RewriteCondition::Throws(descriptor) => { + let offset = string_table.insert(descriptor) as u32; + conditions.push(RewriteComponent { + kind: REWRITE_CONDITION_THROWS, + value: offset, + }); + } + builder::RewriteCondition::Unknown(value) => { + let offset = string_table.insert(value) as u32; + conditions.push(RewriteComponent { + kind: REWRITE_CONDITION_UNKNOWN, + value: offset, + }); + } + } + } + + let mut actions = Vec::new(); + for action in &rule.actions { + match action { + builder::RewriteAction::RemoveInnerFrames(count) => { + actions.push(RewriteComponent { + kind: REWRITE_ACTION_REMOVE_INNER_FRAMES, + value: *count as u32, + }); + } + builder::RewriteAction::Unknown(value) => { + let offset = string_table.insert(value) as u32; + actions.push(RewriteComponent { + kind: REWRITE_ACTION_UNKNOWN, + value: offset, + }); + } + } + } + + RewriteRuleInProgress { + conditions, + actions, + } + }) + .collect(); + + let member: Member = Member { startline: member.startline as u32, endline: member.endline as u32, original_class_offset, @@ -479,12 +633,15 @@ impl<'data> ProguardCache<'data> { is_outline, outline_pairs_offset: 0, outline_pairs_len: 0, + rewrite_rules_offset: 0, + rewrite_rules_len: 0, _reserved: [0; 2], }; MemberInProgress { member, outline_pairs, + rewrite_rules, } } @@ -524,6 +681,22 @@ impl<'data> ProguardCache<'data> { let end = start.saturating_add(len); assert!(end <= self.outline_pairs.len()); + let rule_start = member.rewrite_rules_offset as usize; + let rule_len = member.rewrite_rules_len as usize; + let rule_end = rule_start.saturating_add(rule_len); + assert!(rule_end <= self.rewrite_rule_entries.len()); + for entry in &self.rewrite_rule_entries[rule_start..rule_end] { + let cond_start = entry.conditions_offset as usize; + let cond_len = entry.conditions_len as usize; + let cond_end = cond_start.saturating_add(cond_len); + assert!(cond_end <= self.rewrite_rule_components.len()); + + let action_start = entry.actions_offset as usize; + let action_len = entry.actions_len as usize; + let action_end = action_start.saturating_add(action_len); + assert!(action_end <= self.rewrite_rule_components.len()); + } + if member.params_offset != u32::MAX { assert!(self.read_string(member.params_offset).is_ok()); } @@ -559,4 +732,11 @@ struct ClassInProgress<'data> { struct MemberInProgress { member: Member, outline_pairs: Vec, + rewrite_rules: Vec, +} + +#[derive(Debug, Clone, Default)] +struct RewriteRuleInProgress { + conditions: Vec, + actions: Vec, } diff --git a/src/lib.rs b/src/lib.rs index c561fe7..adb4c57 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,9 @@ mod mapper; mod mapping; mod stacktrace; -pub use cache::{CacheError, CacheErrorKind, ProguardCache, PRGCACHE_VERSION}; +pub use cache::{ + class_name_to_descriptor, CacheError, CacheErrorKind, ProguardCache, PRGCACHE_VERSION, +}; pub use mapper::{DeobfuscatedSignature, ProguardMapper, RemappedFrameIter}; pub use mapping::{ LineMapping, MappingSummary, ParseError, ParseErrorKind, ProguardMapping, ProguardRecord, diff --git a/tests/r8.rs b/tests/r8.rs index 45963f5..1a1778f 100644 --- a/tests/r8.rs +++ b/tests/r8.rs @@ -265,34 +265,6 @@ fn test_outline_header_parsing_cache() { assert_eq!(class, Some("some.Class")); } -#[test] -fn test_outline_frame_retracing_cache() { - let mapping = ProguardMapping::new(MAPPING_OUTLINE); - - let mut buf = Vec::new(); - ProguardCache::write(&mapping, &mut buf).unwrap(); - let cache = ProguardCache::parse(&buf).unwrap(); - cache.test(); - - // Test retracing a frame from the outline class - let mut mapped = cache.remap_frame(&StackFrame::new("a", "a", 1)); - - assert_eq!( - mapped.next().unwrap(), - StackFrame::new("outline.Class", "outline", 1) - ); - assert_eq!(mapped.next(), None); - - // Test retracing a frame from the class with outlineCallsite - let mut mapped = cache.remap_frame(&StackFrame::new("b", "s", 27)); - - assert_eq!( - mapped.next().unwrap(), - StackFrame::new("some.Class", "outlineCaller", 0) - ); - assert_eq!(mapped.next(), None); -} - #[test] fn test_outline_header_parsing() { let mapping = ProguardMapping::new(MAPPING_OUTLINE); @@ -362,6 +334,42 @@ Caused by: java.lang.IllegalStateException: Secondary issue assert_eq!(actual, expected); } +#[test] +fn rewrite_frame_complex_stacktrace_cache() { + let mut cache_bytes = Vec::new(); + ProguardCache::write( + &ProguardMapping::new(MAPPING_REWRITE_COMPLEX.as_bytes()), + &mut cache_bytes, + ) + .unwrap(); + let cache = ProguardCache::parse(&cache_bytes).unwrap(); + cache.test(); + + let input = "\ +java.lang.NullPointerException: Primary issue + at a.start(SourceFile:10) + at b.dispatch(SourceFile:5) + at c.draw(SourceFile:20) +Caused by: java.lang.IllegalStateException: Secondary issue + at b.dispatch(SourceFile:5) + at c.draw(SourceFile:20) +"; + + let expected = "\ +java.lang.NullPointerException: Primary issue + at com.example.flow.Initializer.start(SourceFile:42) + at com.example.flow.StreamRouter$Inline.internalDispatch(:30) + at com.example.flow.StreamRouter.dispatch(SourceFile:12) + at com.example.flow.UiBridge.render(SourceFile:200) +Caused by: java.lang.IllegalStateException: Secondary issue + at com.example.flow.StreamRouter.dispatch(SourceFile:12) + at com.example.flow.UiBridge.render(SourceFile:200) +"; + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual, expected); +} + #[test] fn rewrite_frame_complex_stacktrace_typed() { let mapper = ProguardMapper::from(MAPPING_REWRITE_COMPLEX);