Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6f00903
WIP
romtsn Nov 11, 2025
1dd7d25
Merge branch 'master' into rz/feat/rewrite-frames-support
romtsn Dec 1, 2025
c7dab35
Add more complex test
romtsn Dec 1, 2025
085678a
add test fixture file
romtsn Dec 1, 2025
79335e1
Support multiple rewrite rules (OR)
romtsn Dec 3, 2025
96bcffa
Do not destructure COllectedFrames
romtsn Dec 3, 2025
9870077
Improve COllectedFrames handling
romtsn Dec 4, 2025
292f1f5
Remove unnecessary import
romtsn Dec 4, 2025
217bd9a
feat(r8): Support annotation in ProguardCache
romtsn Dec 4, 2025
ad156c8
Simplify conditions
romtsn Dec 9, 2025
bb565b7
Skip line if rewrite rules cleared all frames
romtsn Dec 9, 2025
de58387
Merge branch 'master' into rz/feat/rewrite-frames-support
romtsn Dec 10, 2025
9fabece
resolve merge conflicts
romtsn Dec 10, 2025
5d83211
Merge branch 'rz/feat/rewrite-frames-support' into rz/feat/rewrite-fr…
romtsn Dec 10, 2025
d8979d6
use let-else
romtsn Dec 10, 2025
649dcef
use u32::MAX for unknown variants
romtsn Dec 10, 2025
872af98
Drop unnecessary into_iter
romtsn Dec 10, 2025
bec6bce
Skip line if rewrite rules cleared all frames
romtsn Dec 10, 2025
1892067
Do not add RewritRules with both empty conditions and actions
romtsn Dec 10, 2025
c204f0c
Return early in apply_rewrite_rules
romtsn Dec 10, 2025
34101f6
Add doc for rewrite rules
romtsn Dec 10, 2025
9ab3a72
Commonize 'remap_frame' and use it in remap_stacktrace(typed)
romtsn Dec 12, 2025
4b77917
feat(r8-tests): Add R8 inline tests
romtsn Dec 17, 2025
0823cb6
fix(r8): Synthesize file name for foreign members and classes without…
romtsn Dec 18, 2025
54fafc7
Merge branch 'master' into rz/feat/r8-tests-inline
romtsn Dec 19, 2025
1c4cd14
REsolve merge conflicts
romtsn Dec 19, 2025
faee5d4
reuse find_members_with_rules in remap_frame
romtsn Dec 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
351 changes: 273 additions & 78 deletions src/cache/mod.rs

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions src/cache/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,15 +313,16 @@ impl<'data> ProguardCache<'data> {
.map(|(obfuscated, original)| {
let obfuscated_name_offset = string_table.insert(obfuscated.as_str()) as u32;
let original_name_offset = string_table.insert(original.as_str()) as u32;
let is_synthesized = parsed
.class_infos
.get(original)
.map(|ci| ci.is_synthesized)
.unwrap_or_default();
let class_info = parsed.class_infos.get(original);
let is_synthesized = class_info.map(|ci| ci.is_synthesized).unwrap_or_default();
let file_name_offset = class_info
.and_then(|ci| ci.source_file)
.map_or(u32::MAX, |s| string_table.insert(s) as u32);
let class = ClassInProgress {
class: Class {
original_name_offset,
obfuscated_name_offset,
file_name_offset,
is_synthesized: is_synthesized as u8,
..Default::default()
},
Expand Down
5 changes: 3 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@
//! mapper
//! .remap_frame(&proguard::StackFrame::new("a.a.a.b.c", "a", 13))
//! .collect::<Vec<_>>(),
//! vec![proguard::StackFrame::new(
//! vec![proguard::StackFrame::with_file(
//! "android.arch.core.internal.SafeIterableMap",
//! "eldest",
//! 168
//! 168,
//! "SafeIterableMap.java"
//! )],
//! );
//! ```
Expand Down
116 changes: 78 additions & 38 deletions src/mapper.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt;
use std::fmt::{Error as FmtError, Write};
Expand Down Expand Up @@ -66,6 +67,9 @@ struct MemberMapping<'s> {
is_outline: bool,
outline_callsite_positions: Option<HashMap<usize, usize>>,
rewrite_rules: Vec<RewriteRule<'s>>,
/// The source file of the outer class, used for synthesizing file names when
/// the inlined method's class doesn't have its own sourceFile metadata.
outer_source_file: Option<&'s str>,
}

#[derive(Clone, Debug, Default)]
Expand Down Expand Up @@ -137,6 +141,31 @@ fn class_name_to_descriptor(class: &str) -> String {
descriptor
}

/// Synthesizes a full source file name from a class name and a reference source file.
/// For Kotlin top-level classes ending in "Kt", the suffix is stripped and ".kt" is used.
/// Otherwise, the extension is derived from the reference file, defaulting to ".java".
/// For example: ("com.example.MainKt", Some("Other.java")) -> "Main.kt" (Kt suffix takes precedence)
/// For example: ("com.example.Main", Some("Other.kt")) -> "Main.kt"
/// For example: ("com.example.MainKt", None) -> "Main.kt"
/// For inner classes: ("com.example.Main$Inner", None) -> "Main.java"
fn synthesize_source_file(class_name: &str, reference_file: Option<&str>) -> Option<String> {
let base = extract_class_name(class_name)?;

// For Kotlin top-level classes (ending in "Kt"), always use .kt extension and strip suffix
// This takes precedence over reference_file since Kt suffix is a strong Kotlin indicator
if base.ends_with("Kt") && base.len() > 2 {
let kotlin_base = &base[..base.len() - 2];
return Some(format!("{}.kt", kotlin_base));
}

// If we have a reference file, derive extension from it
if let Some(ext) = reference_file.and_then(|f| f.rfind('.').map(|pos| &f[pos..])) {
return Some(format!("{}{}", base, ext));
}

Some(format!("{}.java", base))
}

fn map_member_with_lines<'a>(
frame: &StackFrame<'a>,
member: &MemberMapping<'a>,
Expand All @@ -145,7 +174,7 @@ fn map_member_with_lines<'a>(
return None;
}

// parents of inlined frames dont have an `endline`, and
// parents of inlined frames don't have an `endline`, and
// the top inlined frame need to be correctly offset.
let line = if member.original_endline.is_none()
|| member.original_endline == Some(member.original_startline)
Expand All @@ -155,22 +184,20 @@ fn map_member_with_lines<'a>(
member.original_startline + frame.line - member.startline
};

let file = if let Some(file_name) = member.original_file {
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(member.original_class.unwrap_or(frame.class))
// Synthesize from class name for synthetic classes
extract_class_name(class).map(Cow::Borrowed)
} else {
member.original_file
Some(Cow::Borrowed(file_name))
}
} else if member.original_class.is_some() {
// when an inlined function is from a foreign class, we
// don’t know the file it is defined in.
None
} else {
frame.file
// Synthesize from class name (input filename is not reliable)
synthesize_source_file(class, member.outer_source_file).map(Cow::Owned)
};

let class = member.original_class.unwrap_or(frame.class);

Some(StackFrame {
class,
method: member.original,
Expand All @@ -186,10 +213,12 @@ fn map_member_without_lines<'a>(
member: &MemberMapping<'a>,
) -> StackFrame<'a> {
let class = member.original_class.unwrap_or(frame.class);
// Synthesize from class name (input filename is not reliable)
let file = synthesize_source_file(class, member.outer_source_file).map(Cow::Owned);
StackFrame {
class,
method: member.original,
file: None,
file,
line: 0,
parameters: frame.parameters,
method_synthesized: member.is_synthesized,
Expand Down Expand Up @@ -320,6 +349,13 @@ impl<'s> ProguardMapper<'s> {
for ((obfuscated_class, obfuscated_method), members) in &parsed.members {
let class_mapping = class_mappings.entry(obfuscated_class.as_str()).or_default();

// Get the outer class's sourceFile for use in synthesizing file names
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())
Expand All @@ -331,16 +367,18 @@ impl<'s> ProguardMapper<'s> {
if has_line_info && member.startline == 0 && member.endline == 0 {
continue;
}
method_mappings
.all_mappings
.push(Self::resolve_mapping(&parsed, member));
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));
param_mappings.push(Self::resolve_mapping(&parsed, member, outer_source_file));
}
}
}
Expand All @@ -353,6 +391,7 @@ impl<'s> ProguardMapper<'s> {
fn resolve_mapping(
parsed: &ParsedProguardMapping<'s>,
member: &Member<'s>,
outer_source_file: Option<&'s str>,
) -> MemberMapping<'s> {
let original_file = parsed
.class_infos
Expand Down Expand Up @@ -387,6 +426,7 @@ impl<'s> ProguardMapper<'s> {
is_outline,
outline_callsite_positions,
rewrite_rules: member.rewrite_rules.clone(),
outer_source_file,
}
}

Expand Down Expand Up @@ -787,15 +827,15 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g:
class: "com.example.MainFragment$g",
method: "onClick",
line: 2,
file: Some("SourceFile"),
file: Some(Cow::Borrowed("SourceFile")),
parameters: None,
method_synthesized: false,
},
StackFrame {
class: "android.view.View",
method: "performClick",
line: 7393,
file: Some("View.java"),
file: Some(Cow::Borrowed("View.java")),
parameters: None,
method_synthesized: false,
},
Expand All @@ -809,7 +849,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g:
class: "com.example.MainFragment$g",
method: "onClick",
line: 1,
file: Some("SourceFile"),
file: Some(Cow::Borrowed("SourceFile")),
parameters: None,
method_synthesized: false,
}],
Expand All @@ -818,13 +858,13 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g:
};
let expect = "\
com.example.MainFragment$RocketException: Crash!
at com.example.MainFragment$Rocket.fly(<unknown>:85)
at com.example.MainFragment$onActivityCreated$4.onClick(SourceFile:65)
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(<unknown>:90)
at com.example.MainFragment$Rocket.fly(<unknown>:83)
at com.example.MainFragment$onActivityCreated$4.onClick(SourceFile:65)\n";
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);

Expand Down Expand Up @@ -855,13 +895,13 @@ Caused by: com.example.MainFragment$d: Engines overheating
... 13 more";
let expect = "\
com.example.MainFragment$RocketException: Crash!
at com.example.MainFragment$Rocket.fly(<unknown>:85)
at com.example.MainFragment$onActivityCreated$4.onClick(SourceFile:65)
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(<unknown>:90)
at com.example.MainFragment$Rocket.fly(<unknown>:83)
at com.example.MainFragment$onActivityCreated$4.onClick(SourceFile:65)
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);
Expand All @@ -882,7 +922,7 @@ java.lang.NullPointerException: Boom
at a.a(SourceFile:4)";
let expect = "\
java.lang.NullPointerException: Boom
at some.Class.caller(SourceFile:7)
at some.Class.caller(Class.java:7)
";

let mapper = ProguardMapper::from(mapping);
Expand All @@ -903,8 +943,8 @@ java.lang.IllegalStateException: Boom
at a.a(SourceFile:4)";
let expect = "\
java.lang.IllegalStateException: Boom
at other.Class.inlinee(<unknown>:23)
at some.Class.caller(SourceFile:7)
at other.Class.inlinee(Class.java:23)
at some.Class.caller(Class.java:7)
";

let mapper = ProguardMapper::from(mapping);
Expand All @@ -929,7 +969,7 @@ some.Class -> a:
class: "a",
method: "a",
line: 4,
file: Some("SourceFile"),
file: Some(Cow::Borrowed("SourceFile")),
parameters: None,
method_synthesized: false,
}],
Expand Down Expand Up @@ -961,7 +1001,7 @@ java.lang.NullPointerException: Boom
at a.call(SourceFile:4)";
let expected_npe = "\
java.lang.NullPointerException: Boom
at some.Class.outer(SourceFile:7)
at some.Class.outer(Class.java:7)
";
assert_eq!(mapper.remap_stacktrace(input_npe).unwrap(), expected_npe);

Expand All @@ -970,7 +1010,7 @@ java.lang.IllegalStateException: Boom
at a.call(SourceFile:4)";
let expected_ise = "\
java.lang.IllegalStateException: Boom
at some.Class.outer(SourceFile:7)
at some.Class.outer(Class.java:7)
";
assert_eq!(mapper.remap_stacktrace(input_ise).unwrap(), expected_ise);
}
Expand Down Expand Up @@ -1019,7 +1059,7 @@ java.lang.NullPointerException: Boom
// not replaced with the original "at a.call(SourceFile:4)"
let expected = "\
java.lang.NullPointerException: Boom
at some.Other.method(SourceFile:30)
at some.Other.method(Other.java:30)
";

let actual = mapper.remap_stacktrace(input).unwrap();
Expand Down Expand Up @@ -1050,15 +1090,15 @@ some.Other -> b:
class: "a",
method: "call",
line: 4,
file: Some("SourceFile"),
file: Some(Cow::Borrowed("SourceFile")),
parameters: None,
method_synthesized: false,
},
StackFrame {
class: "b",
method: "run",
line: 5,
file: Some("SourceFile"),
file: Some(Cow::Borrowed("SourceFile")),
parameters: None,
method_synthesized: false,
},
Expand Down
Loading
Loading