proguard/
mapper.rs

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
11/// A deobfuscated method signature.
12pub 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    /// Returns the java return type of the method signature
26    pub fn return_type(&self) -> &str {
27        self.return_type.as_str()
28    }
29
30    /// Returns the list of paramater types of the method signature
31    pub fn parameters_types(&self) -> impl Iterator<Item = &str> {
32        self.parameters.iter().map(|s| s.as_ref())
33    }
34
35    /// formats types (param_type list, return_type) into a human-readable signature
36    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    // This trait requires `fmt` with this exact signature.
49    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    // method_params -> Vec[MemberMapping]
70    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/// An Iterator over remapped StackFrames.
87#[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    // If the class is an inner class, we need to extract the outer class name
118    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        // skip any members which do not match our frames line
127        if member.endline > 0 && (frame.line < member.startline || frame.line > member.endline) {
128            continue;
129        }
130
131        // parents of inlined frames don’t have an `endline`, and
132        // the top inlined frame need to be correctly offset.
133        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            // when an inlined function is from a foreign class, we
149            // don’t know the file it is defined in.
150            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/// A Proguard Remapper.
195///
196/// This can remap class names, stack frames one at a time, or the complete
197/// raw stacktrace.
198#[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    /// Create a new ProguardMapper.
219    pub fn new(mapping: ProguardMapping<'s>) -> Self {
220        Self::create_proguard_mapper(mapping, false)
221    }
222
223    /// Create a new ProguardMapper with the extra mappings_by_params.
224    /// This is useful when we want to deobfuscate frames with missing
225    /// line information
226    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        // Initialize class mappings with obfuscated -> original name data. The mappings will be filled in afterwards.
240        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        // Only fill in `original_class` if it is _not_ the current class
298        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    /// Remaps an obfuscated Class.
323    ///
324    /// This works on the fully-qualified name of the class, with its complete
325    /// module prefix.
326    ///
327    /// # Examples
328    ///
329    /// ```
330    /// let mapping = r#"android.arch.core.executor.ArchTaskExecutor -> a.a.a.a.c:"#;
331    /// let mapper = proguard::ProguardMapper::from(mapping);
332    ///
333    /// let mapped = mapper.remap_class("a.a.a.a.c");
334    /// assert_eq!(mapped, Some("android.arch.core.executor.ArchTaskExecutor"));
335    /// ```
336    pub fn remap_class(&'s self, class: &str) -> Option<&'s str> {
337        self.classes.get(class).map(|class| class.original)
338    }
339
340    /// returns a tuple where the first element is the list of the function
341    /// parameters and the second one is the return type
342    pub fn deobfuscate_signature(&'s self, signature: &str) -> Option<DeobfuscatedSignature> {
343        java::deobfuscate_bytecode_signature(signature, self).map(DeobfuscatedSignature::new)
344    }
345
346    /// Remaps an obfuscated Class Method.
347    ///
348    /// The `class` argument has to be the fully-qualified obfuscated name of the
349    /// class, with its complete module prefix.
350    ///
351    /// If the `method` can be resolved unambiguously, it will be returned
352    /// alongside the remapped `class`, otherwise `None` is being returned.
353    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        // We conservatively check that all the mappings point to the same method,
359        // as we don’t have line numbers to disambiguate.
360        // We could potentially skip inlined functions here, but lets rather be conservative.
361        let all_matching = members.all(|member| member.original == first.original);
362
363        all_matching.then_some((class.original, first.original))
364    }
365
366    /// Remaps a single Stackframe.
367    ///
368    /// Returns zero or more [`StackFrame`]s, based on the information in
369    /// the proguard mapping. This can return more than one frame in the case
370    /// of inlined functions. In that case, frames are sorted top to bottom.
371    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    /// Remaps a throwable which is the first line of a full stacktrace.
397    ///
398    /// # Example
399    ///
400    /// ```
401    /// use proguard::{ProguardMapper, Throwable};
402    ///
403    /// let mapping = "com.example.Mapper -> a.b:";
404    /// let mapper = ProguardMapper::from(mapping);
405    ///
406    /// let throwable = Throwable::try_parse(b"a.b: Crash").unwrap();
407    /// let mapped = mapper.remap_throwable(&throwable);
408    ///
409    /// assert_eq!(
410    ///     Some(Throwable::with_message("com.example.Mapper", "Crash")),
411    ///     mapped
412    /// );
413    /// ```
414    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    /// Remaps a complete Java StackTrace, similar to [`Self::remap_stacktrace_typed`] but instead works on
422    /// strings as input and output.
423    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    /// Remaps a complete Java StackTrace.
457    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}