1#![forbid(unsafe_code)]
24#![warn(missing_docs)]
25
26use jiff::{SignedDuration, Timestamp};
27use std::{convert::Infallible, fmt, str::FromStr};
28use winnow::{
29 Parser,
30 ascii::{digit1, space0, space1},
31 combinator::{alt, opt, preceded},
32 error::ModalResult,
33 token::{take_till, take_while},
34};
35
36#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct PanicLog {
39 lines: Vec<LogLine>,
40}
41
42impl PanicLog {
43 pub fn parse(input: impl AsRef<str>) -> Self {
45 parse(input)
46 }
47
48 pub fn lines(&self) -> &[LogLine] {
50 &self.lines
51 }
52
53 pub fn panic_messages(&self) -> impl Iterator<Item = &str> {
55 self.lines.iter().filter_map(|line| match &line.kind {
56 LineKind::Panic { message } => Some(message.as_str()),
57 _ => None,
58 })
59 }
60
61 pub fn has_panic(&self) -> bool {
63 self.panic_messages().next().is_some()
64 }
65}
66
67impl fmt::Display for PanicLog {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 for line in &self.lines {
70 f.write_str(&line.raw)?;
71 }
72 Ok(())
73 }
74}
75
76impl FromStr for PanicLog {
77 type Err = Infallible;
78 fn from_str(s: &str) -> Result<Self, Self::Err> {
79 Ok(parse(s))
80 }
81}
82
83#[derive(Clone, Debug, Eq, PartialEq)]
85pub struct LogLine {
86 raw: String,
87 message: String,
88 uptime: Option<SignedDuration>,
89 timestamp: Option<Timestamp>,
90 kind: LineKind,
91}
92
93impl LogLine {
94 pub fn as_str(&self) -> &str {
96 &self.raw
97 }
98 pub fn message(&self) -> &str {
101 &self.message
102 }
103 pub fn uptime(&self) -> Option<SignedDuration> {
105 self.uptime
106 }
107 pub fn timestamp(&self) -> Option<Timestamp> {
109 self.timestamp
110 }
111 pub fn kind(&self) -> &LineKind {
113 &self.kind
114 }
115}
116
117impl fmt::Display for LogLine {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 f.write_str(&self.raw)
120 }
121}
122
123#[derive(Clone, Debug, Eq, PartialEq)]
125#[non_exhaustive]
126pub enum LineKind {
127 Panic {
129 message: String,
131 },
132 Cpu {
134 cpu: u64,
136 uid: Option<u64>,
138 pid: u64,
140 details: String,
142 },
143 Symbol {
145 label: String,
147 segment: Option<String>,
149 frame: StackFrame,
151 },
152 Diagnostic {
154 label: String,
156 details: String,
158 },
159 Modules {
161 modules: Vec<String>,
163 last_unloaded: Option<String>,
165 continuation: bool,
167 },
168 Reboot {
170 delay: SignedDuration,
172 },
173 CallTrace,
175 Frame(StackFrame),
177 Registers(Vec<Register>),
179 Unknown,
181}
182
183#[derive(Clone, Debug, Eq, PartialEq)]
185pub struct StackFrame {
186 pub address: Option<String>,
188 pub uncertain: bool,
190 pub symbol: String,
192 pub offset: String,
194 pub size: String,
196 pub suffix: String,
198}
199
200#[derive(Clone, Debug, Eq, PartialEq)]
202pub struct Register {
203 pub name: String,
205 pub value: String,
207}
208
209pub fn parse(input: impl AsRef<str>) -> PanicLog {
215 let mut input = input.as_ref();
216 let mut lines = Vec::new();
217 let mut in_modules = false;
218 while !input.is_empty() {
219 let remaining = input;
221 let raw = match raw_line.parse_next(&mut input) {
222 Ok(raw) => raw,
223 Err(_) => {
224 input = "";
227 remaining
228 }
229 };
230 let content = raw.trim_end_matches(['\r', '\n']);
231 let mut body = content;
232 if let Ok(rest) = priority.parse_next(&mut body) {
233 body = rest;
234 } else {
235 body = content;
236 }
237 let wall_clock = wall_clock_prefix.parse_next(&mut &*body).ok();
238 let wall_timestamp = wall_clock.map(|(instant, rest)| {
239 body = rest;
240 instant
241 });
242 let before_time = body;
243 let uptime = match timestamp.parse_next(&mut body) {
244 Ok(time) => Some(time),
245 Err(_) => {
246 body = before_time;
247 None
248 }
249 };
250 let mut kind = classify(body);
251 if in_modules
252 && matches!(kind, LineKind::Unknown)
253 && let Ok(value) = module_list(true).parse(body.trim())
254 {
255 kind = value;
256 }
257 in_modules = matches!(kind, LineKind::Modules { .. });
258 lines.push(LogLine {
259 raw: raw.to_owned(),
260 message: body.to_owned(),
261 uptime,
262 timestamp: wall_timestamp,
263 kind,
264 });
265 }
266 PanicLog { lines }
267}
268
269fn raw_line<'a>(input: &mut &'a str) -> ModalResult<&'a str> {
270 (take_till(0.., ['\r', '\n']), opt(alt(("\r\n", "\n", "\r"))))
271 .take()
272 .parse_next(input)
273}
274
275fn priority<'a>(input: &mut &'a str) -> ModalResult<&'a str> {
276 (
277 "<",
278 take_while(1.., |c: char| c.is_ascii_digit()),
279 ">",
280 space0,
281 )
282 .parse_next(input)?;
283 Ok(*input)
284}
285
286fn timestamp(input: &mut &str) -> ModalResult<SignedDuration> {
287 let (_, _, seconds, fraction, _, _) = (
288 "[",
289 space0,
290 digit1,
291 opt((".", take_while(1..=9, |c: char| c.is_ascii_digit()))),
292 "]",
293 space0,
294 )
295 .parse_next(input)?;
296 let seconds = seconds
297 .parse::<i64>()
298 .map_err(|_| winnow::error::ErrMode::Backtrack(winnow::error::ContextError::new()))?;
299 let nanos = fraction.map_or(0, |(_, digits)| {
300 digits
303 .bytes()
304 .fold(0_i32, |value, digit| value * 10 + i32::from(digit - b'0'))
305 * 10_i32.pow(9 - digits.len() as u32)
306 });
307 Ok(SignedDuration::new(seconds, nanos))
308}
309
310fn wall_clock_prefix<'a>(input: &mut &'a str) -> ModalResult<(Timestamp, &'a str)> {
311 let (_, seconds, _) = ("[", digit1, "]").parse_next(input)?;
312 let instant = seconds
313 .parse::<i64>()
314 .ok()
315 .and_then(|s| Timestamp::from_second(s).ok())
316 .ok_or_else(|| winnow::error::ErrMode::Backtrack(winnow::error::ContextError::new()))?;
317 timestamp.parse_next(&mut &**input)?;
320 Ok((instant, *input))
321}
322
323fn cpu(input: &mut &str) -> ModalResult<LineKind> {
324 let (_, cpu, _, uid, _, pid, _, _) = (
325 "CPU:",
326 preceded(space0, digit1),
327 space1,
328 opt(("UID:", preceded(space0, digit1), space1)),
329 "PID:",
330 preceded(space0, digit1),
331 space1,
332 "Comm:",
333 )
334 .parse_next(input)?;
335 let number = |s: &str| {
336 s.parse::<u64>()
337 .map_err(|_| winnow::error::ErrMode::Backtrack(winnow::error::ContextError::new()))
338 };
339 Ok(LineKind::Cpu {
340 cpu: number(cpu)?,
341 uid: uid.map(|(_, value, _)| number(value)).transpose()?,
342 pid: number(pid)?,
343 details: input.trim_start().to_owned(),
344 })
345}
346
347fn hex<'a>(input: &mut &'a str) -> ModalResult<&'a str> {
348 ("0x", take_while(1.., |c: char| c.is_ascii_hexdigit()))
349 .take()
350 .parse_next(input)
351}
352
353fn frame(input: &mut &str) -> ModalResult<StackFrame> {
354 space0.parse_next(input)?;
355 let address = opt((
356 "[<",
357 take_while(1.., |c: char| c.is_ascii_hexdigit()),
358 ">]",
359 space0,
360 ))
361 .parse_next(input)?;
362 let uncertain = opt(("?", space0)).parse_next(input)?.is_some();
363 let (symbol, _, offset, _, size) = (
364 take_while(1.., |c: char| !c.is_whitespace() && c != '+'),
365 "+",
366 hex,
367 "/",
368 hex,
369 )
370 .parse_next(input)?;
371 if input.chars().next().is_some_and(|c| !c.is_whitespace()) {
373 return Err(winnow::error::ErrMode::Backtrack(
374 winnow::error::ContextError::new(),
375 ));
376 }
377 Ok(StackFrame {
378 address: address.map(|(_, value, _, _)| value.to_owned()),
379 uncertain,
380 symbol: symbol.to_owned(),
381 offset: offset.to_owned(),
382 size: size.to_owned(),
383 suffix: input.trim_start().to_owned(),
384 })
385}
386
387fn register_value<'a>(input: &mut &'a str) -> ModalResult<&'a str> {
388 (
389 opt("0x"),
390 take_while(1.., |c: char| c.is_ascii_hexdigit()),
391 opt((
392 ":",
393 opt("0x"),
394 take_while(1.., |c: char| c.is_ascii_hexdigit()),
395 )),
396 opt(("(", take_while(1.., |c: char| c.is_ascii_hexdigit()), ")")),
397 )
398 .take()
399 .parse_next(input)
400}
401
402fn named_symbol(input: &mut &str) -> ModalResult<LineKind> {
403 let (label, _, _) = (
404 take_while(1.., |c: char| c.is_ascii_alphanumeric() || c == '_'),
405 ":",
406 space0,
407 )
408 .parse_next(input)?;
409 let segment = opt((take_while(1.., |c: char| c.is_ascii_hexdigit()), ":")).parse_next(input)?;
410 Ok(LineKind::Symbol {
411 label: label.to_owned(),
412 segment: segment.map(|(s, _)| s.to_owned()),
413 frame: frame.parse_next(input)?,
414 })
415}
416
417fn module_list(continuation: bool) -> impl FnMut(&mut &str) -> ModalResult<LineKind> {
418 move |input| {
419 let mut modules = Vec::new();
420 let mut last_unloaded = None;
421 while !input.is_empty() {
422 if input.starts_with("[last unloaded:") {
423 let (_, _, name, _, _) = (
424 "[last unloaded:",
425 space0,
426 take_while(1.., |c: char| {
427 c.is_ascii_alphanumeric() || "_-+.".contains(c)
428 }),
429 "]",
430 space0,
431 )
432 .parse_next(input)?;
433 last_unloaded = Some(name.to_owned());
434 break;
435 }
436 let (name, separator) = (
437 (
438 take_while(1.., |c: char| {
439 c.is_ascii_alphanumeric() || "_-+.".contains(c)
440 }),
441 opt(("(", take_while(1.., |c: char| c.is_ascii_alphabetic()), ")")),
442 )
443 .take(),
444 space0,
445 )
446 .parse_next(input)?;
447 if separator.is_empty() && !input.is_empty() {
448 return Err(winnow::error::ErrMode::Backtrack(
449 winnow::error::ContextError::new(),
450 ));
451 }
452 modules.push(name.to_owned());
453 }
454 if continuation && modules.is_empty() && last_unloaded.is_none() {
455 return Err(winnow::error::ErrMode::Backtrack(
456 winnow::error::ContextError::new(),
457 ));
458 }
459 Ok(LineKind::Modules {
460 modules,
461 last_unloaded,
462 continuation,
463 })
464 }
465}
466
467fn reboot(input: &mut &str) -> ModalResult<LineKind> {
468 let (_, seconds, _, _) =
469 ("Rebooting in ", digit1, " seconds", alt(("..", ".", ""))).parse_next(input)?;
470 let seconds = seconds
471 .parse::<i64>()
472 .map_err(|_| winnow::error::ErrMode::Backtrack(winnow::error::ContextError::new()))?;
473 Ok(LineKind::Reboot {
474 delay: SignedDuration::from_secs(seconds),
475 })
476}
477
478fn registers(input: &mut &str) -> ModalResult<Vec<Register>> {
479 let mut values = Vec::new();
480 while !input.is_empty() {
481 let (name, _, value, separator) = (
482 take_while(1.., |c: char| c.is_ascii_alphanumeric() || c == '_'),
483 alt((preceded(":", space0), space1)),
484 register_value,
485 space0,
486 )
487 .parse_next(input)?;
488 if separator.is_empty() && !input.is_empty() {
489 return Err(winnow::error::ErrMode::Backtrack(
490 winnow::error::ContextError::new(),
491 ));
492 }
493 values.push(Register {
494 name: name.to_owned(),
495 value: value.to_owned(),
496 });
497 }
498 if values.is_empty() {
499 return Err(winnow::error::ErrMode::Backtrack(
500 winnow::error::ContextError::new(),
501 ));
502 }
503 Ok(values)
504}
505
506fn classify(body: &str) -> LineKind {
507 let body = body.trim();
508 if let Some((_, message)) = body.split_once("Kernel panic - not syncing:") {
510 return LineKind::Panic {
511 message: message.trim_start().to_owned(),
512 };
513 }
514 if matches!(
515 body,
516 "Call Trace:" | "Call trace:" | "Backtrace:" | "Stack backtrace:"
517 ) {
518 return LineKind::CallTrace;
519 }
520 if let Ok(value) = cpu.parse_next(&mut &*body) {
521 return value;
522 }
523 if let Ok(value) = named_symbol.parse_next(&mut &*body) {
524 return value;
525 }
526 if let Some(modules) = body.strip_prefix("Modules linked in:")
527 && let Ok(value) = module_list(false).parse(modules.trim())
528 {
529 return value;
530 }
531 if let Ok(value) = reboot.parse(body) {
532 return value;
533 }
534 for label in [
535 "Oops",
536 "BUG",
537 "#PF",
538 "Tainted",
539 "Hardware name",
540 "Workqueue",
541 "Code",
542 "Kernel Offset",
543 ] {
544 if let Some(details) = body
545 .strip_prefix(label)
546 .and_then(|rest| rest.strip_prefix(':'))
547 {
548 return LineKind::Diagnostic {
549 label: label.to_owned(),
550 details: details.trim_start().to_owned(),
551 };
552 }
553 }
554 if let Ok(value) = frame.parse_next(&mut &*body) {
555 return LineKind::Frame(value);
556 }
557 if let Ok(value) = registers.parse(body) {
558 return LineKind::Registers(value);
559 }
560 LineKind::Unknown
561}