Files
kaptan/firmware/libs/kaptan-log/src/crash.rs
T

185 lines
5.6 KiB
Rust

//! Retained crash-record codec primitives.
//!
//! This module defines and validates a fixed record only. It does not install a
//! panic handler, write retention memory, access flash, or transmit MAVLink.
use crate::KaptanError;
const MAGIC: u32 = u32::from_le_bytes(*b"KLOG");
const VERSION: u16 = 1;
const COMMITTED: u32 = u32::MAX;
/// Fixed endianness-independent retained record size.
pub const CRASH_RECORD_BYTES: usize = 32;
/// Stored reason for a previous abnormal or orderly runtime end.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u8)]
pub enum CrashKind {
/// A panic was captured by a future target-specific handler.
Panic = 1,
/// A hard fault was captured by a future target-specific handler.
HardFault = 2,
/// A requested shutdown sequence was captured.
Shutdown = 3,
/// A platform reset cause was captured.
Reset = 4,
}
impl CrashKind {
const fn as_u8(self) -> u8 {
self as u8
}
const fn from_u8(value: u8) -> Option<Self> {
match value {
1 => Some(Self::Panic),
2 => Some(Self::HardFault),
3 => Some(Self::Shutdown),
4 => Some(Self::Reset),
_ => None,
}
}
}
/// Validation failure for a retained record.
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
pub enum CrashRecordError {
/// Magic/version/commit marker did not identify a completed record.
InvalidHeader,
/// Record payload did not match its checksum.
BadChecksum,
}
/// Fixed, commit-last retained record value.
///
/// A board integration writes every field except `commit`, writes `checksum`,
/// then writes `commit` last. This pure type only validates that protocol.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CrashRecord {
kind: CrashKind,
boot_id: u64,
detail: u64,
}
impl CrashRecord {
/// Creates one completed record for tests or a future retention backend.
pub fn new(kind: CrashKind, boot_id: u64, detail: u64) -> Self {
Self {
kind,
boot_id,
detail,
}
}
/// Encodes an endian-independent record. A target writes the final four
/// commit-marker bytes last; this function itself performs no I/O.
pub fn encode(self) -> [u8; CRASH_RECORD_BYTES] {
let mut bytes = [0_u8; CRASH_RECORD_BYTES];
bytes[0..4].copy_from_slice(&MAGIC.to_le_bytes());
bytes[4..6].copy_from_slice(&VERSION.to_le_bytes());
bytes[6] = self.kind.as_u8();
bytes[7] = 0;
bytes[8..16].copy_from_slice(&self.boot_id.to_le_bytes());
bytes[16..24].copy_from_slice(&self.detail.to_le_bytes());
let record_checksum = checksum(&bytes[..24]);
bytes[24..28].copy_from_slice(&record_checksum.to_le_bytes());
bytes[28..32].copy_from_slice(&COMMITTED.to_le_bytes());
bytes
}
/// Decodes and validates an explicitly little-endian retained record.
/// Arbitrary retained bytes are never reinterpreted as Rust enums.
pub fn decode(bytes: [u8; CRASH_RECORD_BYTES]) -> Result<Self, CrashRecordError> {
if u32::from_le_bytes(bytes[0..4].try_into().unwrap()) != MAGIC
|| u16::from_le_bytes(bytes[4..6].try_into().unwrap()) != VERSION
|| bytes[7] != 0
|| u32::from_le_bytes(bytes[28..32].try_into().unwrap()) != COMMITTED
{
return Err(CrashRecordError::InvalidHeader);
}
if u32::from_le_bytes(bytes[24..28].try_into().unwrap()) != checksum(&bytes[..24]) {
return Err(CrashRecordError::BadChecksum);
}
let Some(kind) = CrashKind::from_u8(bytes[6]) else {
return Err(CrashRecordError::InvalidHeader);
};
Ok(Self {
kind,
boot_id: u64::from_le_bytes(bytes[8..16].try_into().unwrap()),
detail: u64::from_le_bytes(bytes[16..24].try_into().unwrap()),
})
}
/// Captured end reason.
pub const fn kind(self) -> CrashKind {
self.kind
}
/// Board-local boot identity.
pub const fn boot_id(self) -> u64 {
self.boot_id
}
/// Target-defined compact panic/fault/reset detail.
pub const fn detail(self) -> u64 {
self.detail
}
}
const fn checksum(bytes: &[u8]) -> u32 {
let mut state = 0xffff_ffff_u32;
let mut index = 0;
while index < bytes.len() {
state = crc32c_byte(state, bytes[index]);
index += 1;
}
!state
}
const fn crc32c_byte(mut state: u32, byte: u8) -> u32 {
state ^= byte as u32;
let mut index = 0;
while index != 8 {
state = if state & 1 != 0 {
(state >> 1) ^ 0x82f6_3b78
} else {
state >> 1
};
index += 1;
}
state
}
#[cfg(test)]
mod tests {
use super::{CrashKind, CrashRecord, CrashRecordError};
use crate::ErrorReport;
#[test]
fn retained_record_rejects_torn_or_corrupt_payloads() {
let record = CrashRecord::new(CrashKind::Panic, 7, 11);
let bytes = record.encode();
assert_eq!(CrashRecord::decode(bytes), Ok(record));
let mut corrupt = bytes;
corrupt[24] ^= 1;
assert_eq!(
CrashRecord::decode(corrupt),
Err(CrashRecordError::BadChecksum)
);
let mut torn = bytes;
torn[28..32].copy_from_slice(&0_u32.to_le_bytes());
assert_eq!(
CrashRecord::decode(torn),
Err(CrashRecordError::InvalidHeader)
);
}
#[test]
fn validation_error_has_a_static_derived_message() {
assert_eq!(
CrashRecordError::BadChecksum.static_error().path(),
"kaptan_log::crash::CrashRecordError::BadChecksum"
);
}
}