//! The FIT CRC-16. //! //! The FIT specification defines a nibble-table CRC. It is bit-for-bit //! CRC-16/ARC (reflected polynomial `0xA001`, init `0x0000`, no final XOR), //! which gives us published test vectors to check against — see the tests. //! //! Two CRCs appear in every FIT file and both must be right or the file is //! silently rejected on upload: //! //! * the *header CRC* — bytes 0..12 of a 14-byte header, stored at bytes 12..14; //! * the *file CRC* — every byte from the start of the header through the end //! of the data records, appended as the last two bytes of the file. /// Nibble lookup table from the FIT SDK. const CRC_TABLE: [u16; 16] = [ 0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401, 0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400, ]; /// Running FIT CRC-16 state. /// /// Lets the encoder checksum bytes as they are produced rather than buffering /// the whole file twice. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct Crc16(u16); impl Crc16 { /// A fresh CRC with the FIT initial value (zero). pub const fn new() -> Self { Self(0) } /// Fold `data` into the running CRC. pub fn update(&mut self, data: &[u8]) { let mut crc = self.0; for &byte in data { // Low nibble, then high nibble. let mut tmp = CRC_TABLE[(crc & 0xF) as usize]; crc = (crc >> 4) & 0x0FFF; crc = crc ^ tmp ^ CRC_TABLE[(byte & 0xF) as usize]; tmp = CRC_TABLE[(crc & 0xF) as usize]; crc = (crc >> 4) & 0x0FFF; crc = crc ^ tmp ^ CRC_TABLE[((byte >> 4) & 0xF) as usize]; } self.0 = crc; } /// The current checksum. pub const fn value(self) -> u16 { self.0 } } /// One-shot FIT CRC-16 over `data`. pub fn crc16(data: &[u8]) -> u16 { let mut crc = Crc16::new(); crc.update(data); crc.value() } #[cfg(test)] mod tests { use super::*; /// The canonical CRC-16/ARC check value: `crc("123456789") == 0xBB3D`. /// If this fails, every FIT file we produce is rejected. #[test] fn known_vector_check_string() { assert_eq!(crc16(b"123456789"), 0xBB3D); } #[test] fn known_vector_empty_and_zero() { assert_eq!(crc16(b""), 0x0000); // CRC-16/ARC of a single zero byte is 0. assert_eq!(crc16(&[0x00]), 0x0000); // Published CRC-16/ARC vectors. assert_eq!(crc16(b"A"), 0x30C0); assert_eq!(crc16(&[0x00, 0x00, 0x00, 0x00]), 0x0000); } #[test] fn matches_reference_bitwise_implementation() { // Independent, deliberately naive reflected-CRC implementation. fn reference(data: &[u8]) -> u16 { let mut crc: u16 = 0; for &b in data { crc ^= b as u16; for _ in 0..8 { if crc & 1 != 0 { crc = (crc >> 1) ^ 0xA001; } else { crc >>= 1; } } } crc } // A deterministic pseudo-random corpus. let mut data = Vec::new(); let mut x: u32 = 0x1234_5678; for _ in 0..1000 { x = x.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); data.push((x >> 16) as u8); assert_eq!(crc16(&data), reference(&data), "mismatch at len {}", data.len()); } } #[test] fn incremental_equals_one_shot() { let data: Vec = (0u8..=255).cycle().take(777).collect(); let mut running = Crc16::new(); for chunk in data.chunks(13) { running.update(chunk); } assert_eq!(running.value(), crc16(&data)); } }