use crate::error::FitError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileHeader {
pub header_size: u8,
pub protocol_version: u8,
pub profile_version: u16,
pub data_size: u32,
pub header_crc: Option<u16>,
}
impl FileHeader {
pub const SIGNATURE: [u8; 4] = *b".FIT";
pub const MIN_SIZE: u8 = 12;
pub const MAX_SIZE: u8 = 14;
pub fn parse(bytes: &[u8]) -> Result<Self, FitError> {
if bytes.len() < Self::MIN_SIZE as usize {
return Err(FitError::TooShort {
expected: Self::MIN_SIZE as usize,
actual: bytes.len(),
});
}
let header_size = bytes[0];
if header_size != 12 && header_size != 14 {
return Err(FitError::InvalidHeaderSize(header_size));
}
if bytes.len() < header_size as usize {
return Err(FitError::TooShort {
expected: header_size as usize,
actual: bytes.len(),
});
}
let protocol_version = bytes[1];
let profile_version = u16::from_le_bytes([bytes[2], bytes[3]]);
let data_size = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
let signature: [u8; 4] = [bytes[8], bytes[9], bytes[10], bytes[11]];
if signature != Self::SIGNATURE {
return Err(FitError::InvalidSignature(signature));
}
let header_crc = if header_size == 14 {
Some(u16::from_le_bytes([bytes[12], bytes[13]]))
} else {
None
};
Ok(Self {
header_size,
protocol_version,
profile_version,
data_size,
header_crc,
})
}
#[inline]
pub fn total_file_size(&self) -> usize {
self.header_size as usize + self.data_size as usize + 2
}
#[inline]
pub fn file_crc_offset(&self) -> usize {
self.header_size as usize + self.data_size as usize
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_14_byte_header(data_size: u32, header_crc: u16) -> [u8; 14] {
let ds = data_size.to_le_bytes();
let hc = header_crc.to_le_bytes();
[
14, 0x20, 0xD0, 0x52, ds[0], ds[1], ds[2], ds[3], 0x2E, 0x46, 0x49, 0x54, hc[0], hc[1],
]
}
#[test]
fn parses_14_byte_header() {
let bytes = make_14_byte_header(94070, 0xABCD);
let h = FileHeader::parse(&bytes).unwrap();
assert_eq!(h.header_size, 14);
assert_eq!(h.protocol_version, 0x20);
assert_eq!(h.profile_version, 21200);
assert_eq!(h.data_size, 94070);
assert_eq!(h.header_crc, Some(0xABCD));
assert_eq!(h.total_file_size(), 14 + 94070 + 2);
}
#[test]
fn parses_12_byte_header() {
let bytes: [u8; 12] = [12, 0x10, 0x10, 0x00, 0x40, 0, 0, 0, 0x2E, 0x46, 0x49, 0x54];
let h = FileHeader::parse(&bytes).unwrap();
assert_eq!(h.header_size, 12);
assert_eq!(h.protocol_version, 0x10);
assert_eq!(h.data_size, 0x40);
assert_eq!(h.header_crc, None);
}
#[test]
fn rejects_invalid_header_size() {
let mut bytes = make_14_byte_header(0, 0);
bytes[0] = 13; assert_eq!(
FileHeader::parse(&bytes),
Err(FitError::InvalidHeaderSize(13))
);
}
#[test]
fn rejects_invalid_signature() {
let mut bytes = make_14_byte_header(0, 0);
bytes[8] = b'X'; let err = FileHeader::parse(&bytes).unwrap_err();
assert!(matches!(err, FitError::InvalidSignature(sig) if sig[0] == b'X'));
}
#[test]
fn rejects_too_short_for_minimum() {
let bytes = [14u8; 11];
assert_eq!(
FileHeader::parse(&bytes),
Err(FitError::TooShort {
expected: 12,
actual: 11,
}),
);
}
#[test]
fn rejects_too_short_for_declared_14_byte() {
let bytes: [u8; 12] = [14, 0x20, 0, 0, 0, 0, 0, 0, 0x2E, 0x46, 0x49, 0x54];
assert_eq!(
FileHeader::parse(&bytes),
Err(FitError::TooShort {
expected: 14,
actual: 12,
}),
);
}
}