linux-kernel-panic-parser 0.1.0

Lossless, architecture-neutral parsing of Linux kernel panic logs
Documentation
use jiff::SignedDuration;
use linux_kernel_panic_parser::{LineKind, PanicLog, parse};
use std::{borrow::Cow, sync::Arc};

fn round_trip(input: &str) {
    let log = parse(input);
    assert_eq!(log.to_string(), input);
    assert_eq!(parse(log.to_string()), log);
    assert_eq!(
        log.lines()
            .iter()
            .map(|l| l.to_string())
            .collect::<String>(),
        input
    );
}

#[test]
// These explicit types are the behavior this test exercises.
#[allow(
    clippy::unnecessary_to_owned,
    clippy::needless_borrows_for_generic_args
)]
fn string_inputs_and_from_str() {
    let text = "Kernel panic - not syncing: test\n";
    let expected = parse(text);
    assert_eq!(parse(text.to_string()), expected);
    assert_eq!(parse(&text.to_string()), expected);
    assert_eq!(parse(Box::<str>::from(text)), expected);
    assert_eq!(parse(Cow::Borrowed(text)), expected);
    assert_eq!(parse(Cow::Owned::<str>(text.to_string())), expected);
    assert_eq!(parse(Arc::<str>::from(text)), expected);
    assert_eq!(text.parse::<PanicLog>(), Ok(expected.clone()));
    assert_eq!(PanicLog::parse(text), expected);
}

#[test]
fn exact_uptime_and_prefixes() {
    for (text, seconds, nanos) in [
        ("[    1.000000001] message", 1, 1),
        ("<0>[123.25] message", 123, 250_000_000),
        ("[0] message", 0, 0),
        (
            "[9223372036854775807.999999999] message",
            i64::MAX,
            999_999_999,
        ),
    ] {
        let log = parse(text);
        assert_eq!(
            log.lines()[0].uptime(),
            Some(SignedDuration::new(seconds, nanos))
        );
        assert_eq!(log.lines()[0].message(), "message");
        round_trip(text);
    }
    let log = parse("<4>ordinary line");
    assert_eq!(log.lines()[0].message(), "ordinary line");
}

#[test]
fn malformed_timestamps_are_retained() {
    for text in [
        "[",
        "[1.] foo",
        "[1.1234567890] foo",
        "[-1.0] foo",
        "[999999999999999999999999] foo",
        "<x>[1.0] foo",
        "[1.0 foo",
    ] {
        let log = parse(text);
        assert_eq!(log.lines()[0].uptime(), None, "{text}");
        assert_eq!(log.lines()[0].message(), text);
        round_trip(text);
    }
}

#[test]
fn panic_cpu_and_multiple_events() {
    let text = "[ 12.5] CPU: 3 PID: 123 Comm: task with spaces Tainted: G 6.12\n[ 12.6] Kernel panic - not syncing: fatal exception\nOct 3 host kernel: Kernel panic - not syncing: second panic\n";
    let log = parse(text);
    assert!(log.has_panic());
    assert_eq!(
        log.panic_messages().collect::<Vec<_>>(),
        ["fatal exception", "second panic"]
    );
    assert_eq!(
        log.lines()[0].kind(),
        &LineKind::Cpu {
            cpu: 3,
            uid: None,
            pid: 123,
            details: "task with spaces Tainted: G 6.12".into()
        }
    );
    assert!(!parse("BUG: unrelated diagnostic").has_panic());
    assert_eq!(
        parse("Kernel panic - not syncing:")
            .panic_messages()
            .collect::<Vec<_>>(),
        [""]
    );
    round_trip(text);
}

#[test]
fn architecture_neutral_traces_and_registers() {
    // Synthetic grammar fixtures, not captured real-world crash reports.
    for (architecture, registers, frame) in [
        (
            "x86_64",
            "RAX: 0000000000000000 RBX: ffff888000000001",
            " ? panic+0x12/0x300",
        ),
        (
            "i386",
            "EAX: 00000000 EBX: c0000001",
            " [<c0000001>] panic+0x12/0x300",
        ),
        (
            "arm64",
            "x0: 0000000000000000 x30: ffff800000000001",
            " panic+0x12/0x300 [module]",
        ),
        (
            "arm",
            "r0: 00000000 r10: c0000001",
            " [<c0000001>] panic+0x12/0x300",
        ),
        (
            "riscv",
            "a0: 0000000000000000 ra: ffffffff80000001",
            " [<ffffffff80000001>] panic+0x12/0x300",
        ),
        (
            "powerpc",
            "NIP: c000000000000001 LR: c000000000000002",
            " .panic+0x12/0x300",
        ),
        (
            "s390",
            "r0: 0000000000000000 r15: 0000000000000001",
            " [<0000000000000001>] panic+0x12/0x300",
        ),
        ("mips", "epc: 80000001 ra: 80000002", " panic+0x12/0x300"),
        (
            "future",
            "custom_reg: 0x123456789abcdef0123456789abcdef0",
            " [<123456789abcdef0123456789abcdef0>] panic+0x12/0x300",
        ),
    ] {
        let text =
            format!("Call trace:\n{registers}\n{frame}\nKernel panic - not syncing: fatal\n");
        let log = parse(&text);
        assert_eq!(log.lines()[0].kind(), &LineKind::CallTrace);
        assert!(
            matches!(log.lines()[1].kind(), LineKind::Registers(_)),
            "{architecture}"
        );
        let LineKind::Frame(parsed) = log.lines()[2].kind() else {
            panic!("{architecture}")
        };
        assert_eq!(parsed.offset, "0x12");
        assert_eq!(parsed.size, "0x300");
        round_trip(&text);
    }
    let log = parse(" [<ffffffff12345678>] ? function.isra.0+0x01/0xFF [module]");
    let LineKind::Frame(frame) = log.lines()[0].kind() else {
        panic!()
    };
    assert_eq!(frame.address.as_deref(), Some("ffffffff12345678"));
    assert!(frame.uncertain);
    assert_eq!(frame.symbol, "function.isra.0");
    assert_eq!(frame.suffix, "[module]");
}

#[test]
fn unknown_and_truncated_lines_survive() {
    for text in [
        "RAX: xyz",
        "RAX:",
        "panic+0x/0x1",
        "panic+0x1/0x10garbage",
        "CPU: 999999999999999999999 PID: 1 Comm: task",
        "[<ffff>]",
        "Unrecognized metadata: exotic board",
        "λ kernel diagnostic 🐧",
        "\0\t binary-looking text",
    ] {
        assert_eq!(parse(text).lines()[0].kind(), &LineKind::Unknown, "{text}");
        round_trip(text);
    }
}

#[test]
fn line_boundaries_and_whitespace() {
    assert!(parse("").lines().is_empty());
    assert_eq!(parse("\n").lines().len(), 1);
    assert_eq!(parse("a\r\nb\rc\nd").lines().len(), 4);
    for text in [
        "",
        "\n",
        "\r\n",
        "\r",
        "  \t\r\n\n",
        "a\r\nb\rc\nd",
        "Kernel panic - not syncing: reason  \r\n",
        "no final newline",
    ] {
        round_trip(text);
    }
}

#[test]
fn generated_utf8_round_trips() {
    let pieces = [
        "",
        "\n",
        "\r",
        "\r\n",
        " ",
        "\t",
        "\0",
        "λ🐧",
        "[1.25] ",
        "<0>",
        "Kernel panic - not syncing: test",
        "r0: ff",
        "panic+0x1/0x2",
    ];
    for a in pieces {
        for b in pieces {
            for c in pieces {
                round_trip(&format!("{a}{b}{c}"));
            }
        }
    }
}