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