//! 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 { 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 { 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" ); } }