Adds backing state for Resistance and Erg control modes, which had no value to hold and so could never satisfy FR-4.3/FR-4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
121 lines
3.7 KiB
Rust
121 lines
3.7 KiB
Rust
//! 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<u8> = (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));
|
|
}
|
|
}
|