1use std::collections::HashMap;
2use std::fmt;
3use std::fmt::{Error as FmtError, Write};
4use std::iter::FusedIterator;
5
6use crate::builder::{Member, MethodReceiver, ParsedProguardMapping};
7use crate::java;
8use crate::mapping::ProguardMapping;
9use crate::stacktrace::{self, StackFrame, StackTrace, Throwable};
10
11pub struct DeobfuscatedSignature {
13 parameters: Vec<String>,
14 return_type: String,
15}
16
17impl DeobfuscatedSignature {
18 pub(crate) fn new(signature: (Vec<String>, String)) -> DeobfuscatedSignature {
19 DeobfuscatedSignature {
20 parameters: signature.0,
21 return_type: signature.1,
22 }
23 }
24
25 pub fn return_type(&self) -> &str {
27 self.return_type.as_str()
28 }
29
30 pub fn parameters_types(&self) -> impl Iterator<Item = &str> {
32 self.parameters.iter().map(|s| s.as_ref())
33 }
34
35 pub fn format_signature(&self) -> String {
37 let mut signature = format!("({})", self.parameters.join(", "));
38 if !self.return_type().is_empty() && self.return_type() != "void" {
39 signature.push_str(": ");
40 signature.push_str(self.return_type());
41 }
42
43 signature
44 }
45}
46
47impl fmt::Display for DeobfuscatedSignature {
48 fn fmt(&self, f: &mut std::fmt::Formatter) -> fmt::Result {
50 write!(f, "{}", self.format_signature())
51 }
52}
53
54#[derive(Clone, Debug, PartialEq, Eq, Hash)]
55struct MemberMapping<'s> {
56 startline: usize,
57 endline: usize,
58 original_class: Option<&'s str>,
59 original_file: Option<&'s str>,
60 original: &'s str,
61 original_startline: usize,
62 original_endline: Option<usize>,
63 is_synthesized: bool,
64}
65
66#[derive(Clone, Debug, Default)]
67struct ClassMembers<'s> {
68 all_mappings: Vec<MemberMapping<'s>>,
69 mappings_by_params: HashMap<&'s str, Vec<MemberMapping<'s>>>,
71}
72
73#[derive(Clone, Debug, Default)]
74struct ClassMapping<'s> {
75 original: &'s str,
76 members: HashMap<&'s str, ClassMembers<'s>>,
77 #[expect(
78 unused,
79 reason = "It is currently unknown what effect a synthesized class has."
80 )]
81 is_synthesized: bool,
82}
83
84type MemberIter<'m> = std::slice::Iter<'m, MemberMapping<'m>>;
85
86#[derive(Clone, Debug, Default)]
88pub struct RemappedFrameIter<'m> {
89 inner: Option<(StackFrame<'m>, MemberIter<'m>)>,
90}
91
92impl<'m> RemappedFrameIter<'m> {
93 fn empty() -> Self {
94 Self { inner: None }
95 }
96 fn members(frame: StackFrame<'m>, members: MemberIter<'m>) -> Self {
97 Self {
98 inner: Some((frame, members)),
99 }
100 }
101}
102
103impl<'m> Iterator for RemappedFrameIter<'m> {
104 type Item = StackFrame<'m>;
105 fn next(&mut self) -> Option<Self::Item> {
106 let (frame, ref mut members) = self.inner.as_mut()?;
107 if frame.parameters.is_none() {
108 iterate_with_lines(frame, members)
109 } else {
110 iterate_without_lines(frame, members)
111 }
112 }
113}
114
115fn extract_class_name(full_path: &str) -> Option<&str> {
116 let after_last_period = full_path.split('.').next_back()?;
117 after_last_period.split('$').next()
119}
120
121fn iterate_with_lines<'a>(
122 frame: &mut StackFrame<'a>,
123 members: &mut core::slice::Iter<'_, MemberMapping<'a>>,
124) -> Option<StackFrame<'a>> {
125 for member in members {
126 if member.endline > 0 && (frame.line < member.startline || frame.line > member.endline) {
128 continue;
129 }
130
131 let line = if member.original_endline.is_none()
134 || member.original_endline == Some(member.original_startline)
135 {
136 member.original_startline
137 } else {
138 member.original_startline + frame.line - member.startline
139 };
140
141 let file = if let Some(file_name) = member.original_file {
142 if file_name == "R8$$SyntheticClass" {
143 extract_class_name(member.original_class.unwrap_or(frame.class))
144 } else {
145 member.original_file
146 }
147 } else if member.original_class.is_some() {
148 None
151 } else {
152 frame.file
153 };
154
155 let class = match member.original_class {
156 Some(class) => class,
157 _ => frame.class,
158 };
159
160 return Some(StackFrame {
161 class,
162 method: member.original,
163 file,
164 line,
165 parameters: frame.parameters,
166 method_synthesized: member.is_synthesized,
167 });
168 }
169 None
170}
171
172fn iterate_without_lines<'a>(
173 frame: &mut StackFrame<'a>,
174 members: &mut core::slice::Iter<'_, MemberMapping<'a>>,
175) -> Option<StackFrame<'a>> {
176 let member = members.next()?;
177
178 let class = match member.original_class {
179 Some(class) => class,
180 _ => frame.class,
181 };
182 Some(StackFrame {
183 class,
184 method: member.original,
185 file: None,
186 line: 0,
187 parameters: frame.parameters,
188 method_synthesized: member.is_synthesized,
189 })
190}
191
192impl FusedIterator for RemappedFrameIter<'_> {}
193
194#[derive(Clone, Debug)]
199pub struct ProguardMapper<'s> {
200 classes: HashMap<&'s str, ClassMapping<'s>>,
201}
202
203impl<'s> From<&'s str> for ProguardMapper<'s> {
204 fn from(s: &'s str) -> Self {
205 let mapping = ProguardMapping::new(s.as_ref());
206 Self::new(mapping)
207 }
208}
209
210impl<'s> From<(&'s str, bool)> for ProguardMapper<'s> {
211 fn from(t: (&'s str, bool)) -> Self {
212 let mapping = ProguardMapping::new(t.0.as_ref());
213 Self::new_with_param_mapping(mapping, t.1)
214 }
215}
216
217impl<'s> ProguardMapper<'s> {
218 pub fn new(mapping: ProguardMapping<'s>) -> Self {
220 Self::create_proguard_mapper(mapping, false)
221 }
222
223 pub fn new_with_param_mapping(
227 mapping: ProguardMapping<'s>,
228 initialize_param_mapping: bool,
229 ) -> Self {
230 Self::create_proguard_mapper(mapping, initialize_param_mapping)
231 }
232
233 fn create_proguard_mapper(
234 mapping: ProguardMapping<'s>,
235 initialize_param_mapping: bool,
236 ) -> Self {
237 let parsed = ParsedProguardMapping::parse(mapping, initialize_param_mapping);
238
239 let mut class_mappings: HashMap<&str, ClassMapping<'s>> = parsed
241 .class_names
242 .iter()
243 .map(|(obfuscated, original)| {
244 let is_synthesized = parsed
245 .class_infos
246 .get(original)
247 .map(|ci| ci.is_synthesized)
248 .unwrap_or_default();
249 (
250 obfuscated.as_str(),
251 ClassMapping {
252 original: original.as_str(),
253 is_synthesized,
254 ..Default::default()
255 },
256 )
257 })
258 .collect();
259
260 for ((obfuscated_class, obfuscated_method), members) in &parsed.members {
261 let class_mapping = class_mappings.entry(obfuscated_class.as_str()).or_default();
262
263 let method_mappings = class_mapping
264 .members
265 .entry(obfuscated_method.as_str())
266 .or_default();
267
268 for member in members.all.iter().copied() {
269 method_mappings
270 .all_mappings
271 .push(Self::resolve_mapping(&parsed, member));
272 }
273
274 for (args, param_members) in members.by_params.iter() {
275 let param_mappings = method_mappings.mappings_by_params.entry(args).or_default();
276
277 for member in param_members {
278 param_mappings.push(Self::resolve_mapping(&parsed, *member));
279 }
280 }
281 }
282
283 Self {
284 classes: class_mappings,
285 }
286 }
287
288 fn resolve_mapping(
289 parsed: &ParsedProguardMapping<'s>,
290 member: Member<'s>,
291 ) -> MemberMapping<'s> {
292 let original_file = parsed
293 .class_infos
294 .get(&member.method.receiver.name())
295 .and_then(|class| class.source_file);
296
297 let original_class = match member.method.receiver {
299 MethodReceiver::ThisClass(_) => None,
300 MethodReceiver::OtherClass(original_class_name) => Some(original_class_name.as_str()),
301 };
302
303 let method_info = parsed
304 .method_infos
305 .get(&member.method)
306 .copied()
307 .unwrap_or_default();
308 let is_synthesized = method_info.is_synthesized;
309
310 MemberMapping {
311 startline: member.startline,
312 endline: member.endline,
313 original_class,
314 original_file,
315 original: member.method.name.as_str(),
316 original_startline: member.original_startline,
317 original_endline: member.original_endline,
318 is_synthesized,
319 }
320 }
321
322 pub fn remap_class(&'s self, class: &str) -> Option<&'s str> {
337 self.classes.get(class).map(|class| class.original)
338 }
339
340 pub fn deobfuscate_signature(&'s self, signature: &str) -> Option<DeobfuscatedSignature> {
343 java::deobfuscate_bytecode_signature(signature, self).map(DeobfuscatedSignature::new)
344 }
345
346 pub fn remap_method(&'s self, class: &str, method: &str) -> Option<(&'s str, &'s str)> {
354 let class = self.classes.get(class)?;
355 let mut members = class.members.get(method)?.all_mappings.iter();
356 let first = members.next()?;
357
358 let all_matching = members.all(|member| member.original == first.original);
362
363 all_matching.then_some((class.original, first.original))
364 }
365
366 pub fn remap_frame(&'s self, frame: &StackFrame<'s>) -> RemappedFrameIter<'s> {
372 let Some(class) = self.classes.get(frame.class) else {
373 return RemappedFrameIter::empty();
374 };
375
376 let Some(members) = class.members.get(frame.method) else {
377 return RemappedFrameIter::empty();
378 };
379
380 let mut frame = frame.clone();
381 frame.class = class.original;
382
383 let mappings = if let Some(parameters) = frame.parameters {
384 if let Some(typed_members) = members.mappings_by_params.get(parameters) {
385 typed_members.iter()
386 } else {
387 return RemappedFrameIter::empty();
388 }
389 } else {
390 members.all_mappings.iter()
391 };
392
393 RemappedFrameIter::members(frame, mappings)
394 }
395
396 pub fn remap_throwable<'a>(&'a self, throwable: &Throwable<'a>) -> Option<Throwable<'a>> {
415 self.remap_class(throwable.class).map(|class| Throwable {
416 class,
417 message: throwable.message,
418 })
419 }
420
421 pub fn remap_stacktrace(&self, input: &str) -> Result<String, std::fmt::Error> {
424 let mut stacktrace = String::new();
425 let mut lines = input.lines();
426
427 if let Some(line) = lines.next() {
428 match stacktrace::parse_throwable(line) {
429 None => match stacktrace::parse_frame(line) {
430 None => writeln!(&mut stacktrace, "{line}")?,
431 Some(frame) => format_frames(&mut stacktrace, line, self.remap_frame(&frame))?,
432 },
433 Some(throwable) => {
434 format_throwable(&mut stacktrace, line, self.remap_throwable(&throwable))?
435 }
436 }
437 }
438
439 for line in lines {
440 match stacktrace::parse_frame(line) {
441 None => match line
442 .strip_prefix("Caused by: ")
443 .and_then(stacktrace::parse_throwable)
444 {
445 None => writeln!(&mut stacktrace, "{line}")?,
446 Some(cause) => {
447 format_cause(&mut stacktrace, line, self.remap_throwable(&cause))?
448 }
449 },
450 Some(frame) => format_frames(&mut stacktrace, line, self.remap_frame(&frame))?,
451 }
452 }
453 Ok(stacktrace)
454 }
455
456 pub fn remap_stacktrace_typed<'a>(&'a self, trace: &StackTrace<'a>) -> StackTrace<'a> {
458 let exception = trace
459 .exception
460 .as_ref()
461 .and_then(|t| self.remap_throwable(t));
462
463 let frames =
464 trace
465 .frames
466 .iter()
467 .fold(Vec::with_capacity(trace.frames.len()), |mut frames, f| {
468 let mut peek_frames = self.remap_frame(f).peekable();
469 if peek_frames.peek().is_some() {
470 frames.extend(peek_frames);
471 } else {
472 frames.push(f.clone());
473 }
474
475 frames
476 });
477
478 let cause = trace
479 .cause
480 .as_ref()
481 .map(|c| Box::new(self.remap_stacktrace_typed(c)));
482
483 StackTrace {
484 exception,
485 frames,
486 cause,
487 }
488 }
489}
490
491pub(crate) fn format_throwable(
492 stacktrace: &mut impl Write,
493 line: &str,
494 throwable: Option<Throwable<'_>>,
495) -> Result<(), FmtError> {
496 if let Some(throwable) = throwable {
497 writeln!(stacktrace, "{throwable}")
498 } else {
499 writeln!(stacktrace, "{line}")
500 }
501}
502
503pub(crate) fn format_frames<'s>(
504 stacktrace: &mut impl Write,
505 line: &str,
506 remapped: impl Iterator<Item = StackFrame<'s>>,
507) -> Result<(), FmtError> {
508 let mut remapped = remapped.peekable();
509
510 if remapped.peek().is_none() {
511 return writeln!(stacktrace, "{line}");
512 }
513 for line in remapped {
514 writeln!(stacktrace, " {line}")?;
515 }
516
517 Ok(())
518}
519
520pub(crate) fn format_cause(
521 stacktrace: &mut impl Write,
522 line: &str,
523 cause: Option<Throwable<'_>>,
524) -> Result<(), FmtError> {
525 if let Some(cause) = cause {
526 writeln!(stacktrace, "Caused by: {cause}")
527 } else {
528 writeln!(stacktrace, "{line}")
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535
536 #[test]
537 fn stacktrace() {
538 let mapping = "\
539com.example.MainFragment$EngineFailureException -> com.example.MainFragment$d:
540com.example.MainFragment$RocketException -> com.example.MainFragment$e:
541com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g:
542 1:1:void com.example.MainFragment$Rocket.startEngines():90:90 -> onClick
543 1:1:void com.example.MainFragment$Rocket.fly():83 -> onClick
544 1:1:void onClick(android.view.View):65 -> onClick
545 2:2:void com.example.MainFragment$Rocket.fly():85:85 -> onClick
546 2:2:void onClick(android.view.View):65 -> onClick
547 ";
548 let stacktrace = StackTrace {
549 exception: Some(Throwable {
550 class: "com.example.MainFragment$e",
551 message: Some("Crash!"),
552 }),
553 frames: vec![
554 StackFrame {
555 class: "com.example.MainFragment$g",
556 method: "onClick",
557 line: 2,
558 file: Some("SourceFile"),
559 parameters: None,
560 method_synthesized: false,
561 },
562 StackFrame {
563 class: "android.view.View",
564 method: "performClick",
565 line: 7393,
566 file: Some("View.java"),
567 parameters: None,
568 method_synthesized: false,
569 },
570 ],
571 cause: Some(Box::new(StackTrace {
572 exception: Some(Throwable {
573 class: "com.example.MainFragment$d",
574 message: Some("Engines overheating"),
575 }),
576 frames: vec![StackFrame {
577 class: "com.example.MainFragment$g",
578 method: "onClick",
579 line: 1,
580 file: Some("SourceFile"),
581 parameters: None,
582 method_synthesized: false,
583 }],
584 cause: None,
585 })),
586 };
587 let expect = "\
588com.example.MainFragment$RocketException: Crash!
589 at com.example.MainFragment$Rocket.fly(<unknown>:85)
590 at com.example.MainFragment$onActivityCreated$4.onClick(SourceFile:65)
591 at android.view.View.performClick(View.java:7393)
592Caused by: com.example.MainFragment$EngineFailureException: Engines overheating
593 at com.example.MainFragment$Rocket.startEngines(<unknown>:90)
594 at com.example.MainFragment$Rocket.fly(<unknown>:83)
595 at com.example.MainFragment$onActivityCreated$4.onClick(SourceFile:65)\n";
596
597 let mapper = ProguardMapper::from(mapping);
598
599 assert_eq!(
600 mapper.remap_stacktrace_typed(&stacktrace).to_string(),
601 expect
602 );
603 }
604
605 #[test]
606 fn stacktrace_str() {
607 let mapping = "\
608com.example.MainFragment$EngineFailureException -> com.example.MainFragment$d:
609com.example.MainFragment$RocketException -> com.example.MainFragment$e:
610com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g:
611 1:1:void com.example.MainFragment$Rocket.startEngines():90:90 -> onClick
612 1:1:void com.example.MainFragment$Rocket.fly():83 -> onClick
613 1:1:void onClick(android.view.View):65 -> onClick
614 2:2:void com.example.MainFragment$Rocket.fly():85:85 -> onClick
615 2:2:void onClick(android.view.View):65 -> onClick
616 ";
617 let stacktrace = "\
618com.example.MainFragment$e: Crash!
619 at com.example.MainFragment$g.onClick(SourceFile:2)
620 at android.view.View.performClick(View.java:7393)
621Caused by: com.example.MainFragment$d: Engines overheating
622 at com.example.MainFragment$g.onClick(SourceFile:1)
623 ... 13 more";
624 let expect = "\
625com.example.MainFragment$RocketException: Crash!
626 at com.example.MainFragment$Rocket.fly(<unknown>:85)
627 at com.example.MainFragment$onActivityCreated$4.onClick(SourceFile:65)
628 at android.view.View.performClick(View.java:7393)
629Caused by: com.example.MainFragment$EngineFailureException: Engines overheating
630 at com.example.MainFragment$Rocket.startEngines(<unknown>:90)
631 at com.example.MainFragment$Rocket.fly(<unknown>:83)
632 at com.example.MainFragment$onActivityCreated$4.onClick(SourceFile:65)
633 ... 13 more\n";
634
635 let mapper = ProguardMapper::from(mapping);
636
637 assert_eq!(mapper.remap_stacktrace(stacktrace).unwrap(), expect);
638 }
639}