1 //! A Parser for Java Stacktraces.
2 
3 use std::fmt::{Display, Formatter, Result as FmtResult};
4 
5 /// A full Java StackTrace as printed by [`Throwable.printStackTrace()`].
6 ///
7 /// [`Throwable.printStackTrace()`]: https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/Throwable.html#printStackTrace()
8 #[derive(Clone, Debug, PartialEq)]
9 pub struct StackTrace<'s> {
10     pub(crate) exception: Option<Throwable<'s>>,
11     pub(crate) frames: Vec<StackFrame<'s>>,
12     pub(crate) cause: Option<Box<StackTrace<'s>>>,
13 }
14 
15 impl<'s> StackTrace<'s> {
16     /// Create a new StackTrace.
new(exception: Option<Throwable<'s>>, frames: Vec<StackFrame<'s>>) -> Self17     pub fn new(exception: Option<Throwable<'s>>, frames: Vec<StackFrame<'s>>) -> Self {
18         Self {
19             exception,
20             frames,
21             cause: None,
22         }
23     }
24 
25     /// Create a new StackTrace with cause information.
with_cause( exception: Option<Throwable<'s>>, frames: Vec<StackFrame<'s>>, cause: StackTrace<'s>, ) -> Self26     pub fn with_cause(
27         exception: Option<Throwable<'s>>,
28         frames: Vec<StackFrame<'s>>,
29         cause: StackTrace<'s>,
30     ) -> Self {
31         Self {
32             exception,
33             frames,
34             cause: Some(Box::new(cause)),
35         }
36     }
37 
38     /// Parses a StackTrace from a full Java StackTrace.
39     ///
40     /// # Examples
41     ///
42     /// ```rust
43     /// use proguard::{StackFrame, StackTrace, Throwable};
44     ///
45     /// let stacktrace = "\
46     /// some.CustomException: Crashed!
47     ///     at some.Klass.method(Klass.java:1234)
48     /// Caused by: some.InnerException
49     ///     at some.Klass2.method2(Klass2.java:5678)
50     /// ";
51     /// let parsed = StackTrace::try_parse(stacktrace.as_bytes());
52     /// assert_eq!(
53     ///     parsed,
54     ///     Some(StackTrace::with_cause(
55     ///         Some(Throwable::with_message("some.CustomException", "Crashed!")),
56     ///         vec![StackFrame::with_file(
57     ///             "some.Klass",
58     ///             "method",
59     ///             1234,
60     ///             "Klass.java",
61     ///         )],
62     ///         StackTrace::new(
63     ///             Some(Throwable::new("some.InnerException")),
64     ///             vec![StackFrame::with_file(
65     ///                 "some.Klass2",
66     ///                 "method2",
67     ///                 5678,
68     ///                 "Klass2.java",
69     ///             )]
70     ///         )
71     ///     ))
72     /// );
73     /// ```
try_parse(stacktrace: &'s [u8]) -> Option<Self>74     pub fn try_parse(stacktrace: &'s [u8]) -> Option<Self> {
75         let stacktrace = std::str::from_utf8(stacktrace).ok()?;
76         parse_stacktrace(stacktrace)
77     }
78 
79     /// The exception at the top of the StackTrace, if present.
exception(&self) -> Option<&Throwable<'_>>80     pub fn exception(&self) -> Option<&Throwable<'_>> {
81         self.exception.as_ref()
82     }
83 
84     /// All StackFrames following the exception.
frames(&self) -> &[StackFrame<'_>]85     pub fn frames(&self) -> &[StackFrame<'_>] {
86         &self.frames
87     }
88 
89     /// An optional cause describing the inner exception.
cause(&self) -> Option<&StackTrace<'_>>90     pub fn cause(&self) -> Option<&StackTrace<'_>> {
91         self.cause.as_deref()
92     }
93 }
94 
95 impl<'s> Display for StackTrace<'s> {
fmt(&self, f: &mut Formatter<'_>) -> FmtResult96     fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
97         if let Some(exception) = &self.exception {
98             writeln!(f, "{}", exception)?;
99         }
100 
101         for frame in &self.frames {
102             writeln!(f, "    {}", frame)?;
103         }
104 
105         if let Some(cause) = &self.cause {
106             write!(f, "Caused by: {}", cause)?;
107         }
108 
109         Ok(())
110     }
111 }
112 
parse_stacktrace(content: &str) -> Option<StackTrace<'_>>113 fn parse_stacktrace(content: &str) -> Option<StackTrace<'_>> {
114     let mut lines = content.lines().peekable();
115 
116     let exception = lines.peek().and_then(|line| parse_throwable(line));
117     if exception.is_some() {
118         lines.next();
119     }
120 
121     let mut stacktrace = StackTrace {
122         exception,
123         frames: vec![],
124         cause: None,
125     };
126     let mut current = &mut stacktrace;
127 
128     for line in &mut lines {
129         if let Some(frame) = parse_frame(line) {
130             current.frames.push(frame);
131         } else if let Some(line) = line.strip_prefix("Caused by: ") {
132             current.cause = Some(Box::new(StackTrace {
133                 exception: parse_throwable(line),
134                 frames: vec![],
135                 cause: None,
136             }));
137             // We just set the `cause` so it's safe to unwrap here
138             current = current.cause.as_deref_mut().unwrap();
139         }
140     }
141 
142     if stacktrace.exception.is_some() || !stacktrace.frames.is_empty() {
143         Some(stacktrace)
144     } else {
145         None
146     }
147 }
148 
149 /// A Java StackFrame.
150 ///
151 /// Basically a Rust version of the Java [`StackTraceElement`].
152 ///
153 /// [`StackTraceElement`]: https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/StackTraceElement.html
154 #[derive(Clone, Debug, PartialEq)]
155 pub struct StackFrame<'s> {
156     pub(crate) class: &'s str,
157     pub(crate) method: &'s str,
158     pub(crate) line: usize,
159     pub(crate) file: Option<&'s str>,
160 }
161 
162 impl<'s> StackFrame<'s> {
163     /// Create a new StackFrame.
new(class: &'s str, method: &'s str, line: usize) -> Self164     pub fn new(class: &'s str, method: &'s str, line: usize) -> Self {
165         Self {
166             class,
167             method,
168             line,
169             file: None,
170         }
171     }
172 
173     /// Create a new StackFrame with file information.
with_file(class: &'s str, method: &'s str, line: usize, file: &'s str) -> Self174     pub fn with_file(class: &'s str, method: &'s str, line: usize, file: &'s str) -> Self {
175         Self {
176             class,
177             method,
178             line,
179             file: Some(file),
180         }
181     }
182 
183     /// Parses a StackFrame from a line of a Java StackTrace.
184     ///
185     /// # Examples
186     ///
187     /// ```
188     /// use proguard::StackFrame;
189     ///
190     /// let parsed = StackFrame::try_parse(b"    at some.Klass.method(Klass.java:1234)");
191     /// assert_eq!(
192     ///     parsed,
193     ///     Some(StackFrame::with_file(
194     ///         "some.Klass",
195     ///         "method",
196     ///         1234,
197     ///         "Klass.java"
198     ///     ))
199     /// );
200     /// ```
try_parse(line: &'s [u8]) -> Option<Self>201     pub fn try_parse(line: &'s [u8]) -> Option<Self> {
202         let line = std::str::from_utf8(line).ok()?;
203         parse_frame(line)
204     }
205 
206     /// The class of the StackFrame.
class(&self) -> &str207     pub fn class(&self) -> &str {
208         self.class
209     }
210 
211     /// The method of the StackFrame.
method(&self) -> &str212     pub fn method(&self) -> &str {
213         self.method
214     }
215 
216     /// The fully qualified method name, including the class.
full_method(&self) -> String217     pub fn full_method(&self) -> String {
218         format!("{}.{}", self.class, self.method)
219     }
220 
221     /// The file of the StackFrame.
file(&self) -> Option<&str>222     pub fn file(&self) -> Option<&str> {
223         self.file
224     }
225 
226     /// The line of the StackFrame, 1-based.
line(&self) -> usize227     pub fn line(&self) -> usize {
228         self.line
229     }
230 }
231 
232 impl<'s> Display for StackFrame<'s> {
fmt(&self, f: &mut Formatter<'_>) -> FmtResult233     fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
234         write!(
235             f,
236             "at {}.{}({}:{})",
237             self.class,
238             self.method,
239             self.file.unwrap_or("<unknown>"),
240             self.line
241         )
242     }
243 }
244 
245 /// Parses a single line from a Java StackTrace.
246 ///
247 /// Returns `None` if the line could not be parsed.
parse_frame(line: &str) -> Option<StackFrame>248 pub(crate) fn parse_frame(line: &str) -> Option<StackFrame> {
249     let line = line.trim();
250 
251     if !line.starts_with("at ") || !line.ends_with(')') {
252         return None;
253     }
254     let mut arg_split = line[3..line.len() - 1].splitn(2, '(');
255 
256     let mut method_split = arg_split.next()?.rsplitn(2, '.');
257     let method = method_split.next()?;
258     let class = method_split.next()?;
259 
260     let mut file_split = arg_split.next()?.splitn(2, ':');
261     let file = file_split.next()?;
262     let line = file_split.next()?.parse().ok()?;
263 
264     Some(StackFrame {
265         class,
266         method,
267         file: Some(file),
268         line,
269     })
270 }
271 
272 /// A Java Throwable.
273 ///
274 /// This is a Rust version of the first line from a [`Throwable.printStackTrace()`] output in Java.
275 ///
276 /// [`Throwable.printStackTrace()`]: https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/Throwable.html#printStackTrace()
277 #[derive(Clone, Debug, PartialEq)]
278 pub struct Throwable<'s> {
279     pub(crate) class: &'s str,
280     pub(crate) message: Option<&'s str>,
281 }
282 
283 impl<'s> Throwable<'s> {
284     /// Create a new Throwable.
new(class: &'s str) -> Self285     pub fn new(class: &'s str) -> Self {
286         Self {
287             class,
288             message: None,
289         }
290     }
291 
292     /// Create a new Throwable with message.
with_message(class: &'s str, message: &'s str) -> Self293     pub fn with_message(class: &'s str, message: &'s str) -> Self {
294         Self {
295             class,
296             message: Some(message),
297         }
298     }
299 
300     /// Parses a Throwable from the a line of a full Java StackTrace.
301     ///
302     /// # Example
303     /// ```rust
304     /// use proguard::Throwable;
305     ///
306     /// let parsed = Throwable::try_parse(b"some.CustomException: Crash!");
307     /// assert_eq!(
308     ///     parsed,
309     ///     Some(Throwable::with_message("some.CustomException", "Crash!")),
310     /// )
311     /// ```
try_parse(line: &'s [u8]) -> Option<Self>312     pub fn try_parse(line: &'s [u8]) -> Option<Self> {
313         std::str::from_utf8(line).ok().and_then(parse_throwable)
314     }
315 
316     /// The class of this Throwable.
class(&self) -> &str317     pub fn class(&self) -> &str {
318         self.class
319     }
320 
321     /// The optional message of this Throwable.
message(&self) -> Option<&str>322     pub fn message(&self) -> Option<&str> {
323         self.message
324     }
325 }
326 
327 impl<'s> Display for Throwable<'s> {
fmt(&self, f: &mut Formatter<'_>) -> FmtResult328     fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
329         write!(f, "{}", self.class)?;
330 
331         if let Some(message) = self.message {
332             write!(f, ": {}", message)?;
333         }
334 
335         Ok(())
336     }
337 }
338 
339 /// Parse the first line of a Java StackTrace which is usually the string version of a
340 /// [`Throwable`].
341 ///
342 /// Returns `None` if the line could not be parsed.
343 ///
344 /// [`Throwable`]: https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/Throwable.html
parse_throwable(line: &str) -> Option<Throwable<'_>>345 pub(crate) fn parse_throwable(line: &str) -> Option<Throwable<'_>> {
346     let line = line.trim();
347 
348     let mut class_split = line.splitn(2, ": ");
349     let class = class_split.next()?;
350     let message = class_split.next();
351 
352     if class.contains(' ') {
353         None
354     } else {
355         Some(Throwable { class, message })
356     }
357 }
358 
359 #[cfg(test)]
360 mod tests {
361     use super::*;
362 
363     #[test]
print_stack_trace()364     fn print_stack_trace() {
365         let trace = StackTrace {
366             exception: Some(Throwable {
367                 class: "com.example.MainFragment",
368                 message: Some("Crash"),
369             }),
370             frames: vec![StackFrame {
371                 class: "com.example.Util",
372                 method: "show",
373                 line: 5,
374                 file: Some("Util.java"),
375             }],
376             cause: Some(Box::new(StackTrace {
377                 exception: Some(Throwable {
378                     class: "com.example.Other",
379                     message: Some("Invalid data"),
380                 }),
381                 frames: vec![StackFrame {
382                     class: "com.example.Parser",
383                     method: "parse",
384                     line: 115,
385                     file: None,
386                 }],
387                 cause: None,
388             })),
389         };
390         let expect = "\
391 com.example.MainFragment: Crash
392     at com.example.Util.show(Util.java:5)
393 Caused by: com.example.Other: Invalid data
394     at com.example.Parser.parse(<unknown>:115)\n";
395 
396         assert_eq!(expect, trace.to_string());
397     }
398 
399     #[test]
stack_frame()400     fn stack_frame() {
401         let line = "at com.example.MainFragment.onClick(SourceFile:1)";
402         let stack_frame = parse_frame(line);
403         let expect = Some(StackFrame {
404             class: "com.example.MainFragment",
405             method: "onClick",
406             line: 1,
407             file: Some("SourceFile"),
408         });
409 
410         assert_eq!(expect, stack_frame);
411 
412         let line = "    at com.example.MainFragment.onClick(SourceFile:1)";
413         let stack_frame = parse_frame(line);
414 
415         assert_eq!(expect, stack_frame);
416 
417         let line = "\tat com.example.MainFragment.onClick(SourceFile:1)";
418         let stack_frame = parse_frame(line);
419 
420         assert_eq!(expect, stack_frame);
421     }
422 
423     #[test]
print_stack_frame()424     fn print_stack_frame() {
425         let frame = StackFrame {
426             class: "com.example.MainFragment",
427             method: "onClick",
428             line: 1,
429             file: None,
430         };
431 
432         assert_eq!(
433             "at com.example.MainFragment.onClick(<unknown>:1)",
434             frame.to_string()
435         );
436 
437         let frame = StackFrame {
438             class: "com.example.MainFragment",
439             method: "onClick",
440             line: 1,
441             file: Some("SourceFile"),
442         };
443 
444         assert_eq!(
445             "at com.example.MainFragment.onClick(SourceFile:1)",
446             frame.to_string()
447         );
448     }
449 
450     #[test]
throwable()451     fn throwable() {
452         let line = "com.example.MainFragment: Crash!";
453         let throwable = parse_throwable(line);
454         let expect = Some(Throwable {
455             class: "com.example.MainFragment",
456             message: Some("Crash!"),
457         });
458 
459         assert_eq!(expect, throwable);
460     }
461 
462     #[test]
print_throwable()463     fn print_throwable() {
464         let throwable = Throwable {
465             class: "com.example.MainFragment",
466             message: None,
467         };
468 
469         assert_eq!("com.example.MainFragment", throwable.to_string());
470 
471         let throwable = Throwable {
472             class: "com.example.MainFragment",
473             message: Some("Crash"),
474         };
475 
476         assert_eq!("com.example.MainFragment: Crash", throwable.to_string());
477     }
478 }
479