185 lines
5.6 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|