diff --git a/swiftnav/Cargo.toml b/swiftnav/Cargo.toml index ee393c6..3c5c424 100644 --- a/swiftnav/Cargo.toml +++ b/swiftnav/Cargo.toml @@ -17,6 +17,7 @@ strum = { version = "0.27", features = ["derive"] } [dev-dependencies] float_eq = "1.0.1" +proptest = "1.5" # This tells docs.rs to include the katex header for math formatting # To do this locally diff --git a/swiftnav/src/edc.rs b/swiftnav/src/edc.rs index fdd271a..40aec01 100644 --- a/swiftnav/src/edc.rs +++ b/swiftnav/src/edc.rs @@ -8,6 +8,69 @@ // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED // WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. //! Error detection code +//! +//! The checksum used by the RTCM protocol is CRC-24Q. This module provides a +//! function for calculating that checksum value over a set of data. +//! +//! # Examples +//! +//! To generate a CRC value in one shot you can simply give the [`compute_crc24q`] +//! function all of the data as a slice of bytes and the initial value: +//! +//! ``` +//! # use swiftnav::edc::compute_crc24q; +//! let msg_data = vec![0xD3, 0x00, 0x13, 0x3E, 0xD7, 0xD3, 0x02, 0x02, 0x98, 0x0E, 0xDE, 0xEF, 0x34, 0xB4, 0xBD, 0x62, 0xAC, 0x09, 0x41, 0x98, 0x6F, 0x33]; +//! let init = 0; +//! +//! let crc = compute_crc24q(&msg_data, init); +//! assert_eq!(crc, 0x00360B98); +//! ``` +//! +//! If the data is split up you can use the CRC from a previous invokation as the initial +//! value for a subsequent invokation: +//! +//! ``` +//! # use swiftnav::edc::compute_crc24q; +//! let block1 = vec![0xD3, 0x00, 0x13, 0x3E, 0xD7, 0xD3, 0x02, 0x02, 0x98, 0x0E]; +//! let block2 = vec![0xDE, 0xEF, 0x34, 0xB4, 0xBD, 0x62, 0xAC, 0x09, 0x41, 0x98, 0x6F, 0x33]; +//! let init = 0; +//! +//! let intermediate = compute_crc24q(&block1, init); +//! let crc = compute_crc24q(&block2, intermediate); +//! assert_eq!(crc, 0x00360B98); +//! ``` + +const CRC24Q_TABLE: [u32; 256] = [ + 0x000000, 0x864CFB, 0x8AD50D, 0x0C99F6, 0x93E6E1, 0x15AA1A, 0x1933EC, 0x9F7F17, 0xA18139, + 0x27CDC2, 0x2B5434, 0xAD18CF, 0x3267D8, 0xB42B23, 0xB8B2D5, 0x3EFE2E, 0xC54E89, 0x430272, + 0x4F9B84, 0xC9D77F, 0x56A868, 0xD0E493, 0xDC7D65, 0x5A319E, 0x64CFB0, 0xE2834B, 0xEE1ABD, + 0x685646, 0xF72951, 0x7165AA, 0x7DFC5C, 0xFBB0A7, 0x0CD1E9, 0x8A9D12, 0x8604E4, 0x00481F, + 0x9F3708, 0x197BF3, 0x15E205, 0x93AEFE, 0xAD50D0, 0x2B1C2B, 0x2785DD, 0xA1C926, 0x3EB631, + 0xB8FACA, 0xB4633C, 0x322FC7, 0xC99F60, 0x4FD39B, 0x434A6D, 0xC50696, 0x5A7981, 0xDC357A, + 0xD0AC8C, 0x56E077, 0x681E59, 0xEE52A2, 0xE2CB54, 0x6487AF, 0xFBF8B8, 0x7DB443, 0x712DB5, + 0xF7614E, 0x19A3D2, 0x9FEF29, 0x9376DF, 0x153A24, 0x8A4533, 0x0C09C8, 0x00903E, 0x86DCC5, + 0xB822EB, 0x3E6E10, 0x32F7E6, 0xB4BB1D, 0x2BC40A, 0xAD88F1, 0xA11107, 0x275DFC, 0xDCED5B, + 0x5AA1A0, 0x563856, 0xD074AD, 0x4F0BBA, 0xC94741, 0xC5DEB7, 0x43924C, 0x7D6C62, 0xFB2099, + 0xF7B96F, 0x71F594, 0xEE8A83, 0x68C678, 0x645F8E, 0xE21375, 0x15723B, 0x933EC0, 0x9FA736, + 0x19EBCD, 0x8694DA, 0x00D821, 0x0C41D7, 0x8A0D2C, 0xB4F302, 0x32BFF9, 0x3E260F, 0xB86AF4, + 0x2715E3, 0xA15918, 0xADC0EE, 0x2B8C15, 0xD03CB2, 0x567049, 0x5AE9BF, 0xDCA544, 0x43DA53, + 0xC596A8, 0xC90F5E, 0x4F43A5, 0x71BD8B, 0xF7F170, 0xFB6886, 0x7D247D, 0xE25B6A, 0x641791, + 0x688E67, 0xEEC29C, 0x3347A4, 0xB50B5F, 0xB992A9, 0x3FDE52, 0xA0A145, 0x26EDBE, 0x2A7448, + 0xAC38B3, 0x92C69D, 0x148A66, 0x181390, 0x9E5F6B, 0x01207C, 0x876C87, 0x8BF571, 0x0DB98A, + 0xF6092D, 0x7045D6, 0x7CDC20, 0xFA90DB, 0x65EFCC, 0xE3A337, 0xEF3AC1, 0x69763A, 0x578814, + 0xD1C4EF, 0xDD5D19, 0x5B11E2, 0xC46EF5, 0x42220E, 0x4EBBF8, 0xC8F703, 0x3F964D, 0xB9DAB6, + 0xB54340, 0x330FBB, 0xAC70AC, 0x2A3C57, 0x26A5A1, 0xA0E95A, 0x9E1774, 0x185B8F, 0x14C279, + 0x928E82, 0x0DF195, 0x8BBD6E, 0x872498, 0x016863, 0xFAD8C4, 0x7C943F, 0x700DC9, 0xF64132, + 0x693E25, 0xEF72DE, 0xE3EB28, 0x65A7D3, 0x5B59FD, 0xDD1506, 0xD18CF0, 0x57C00B, 0xC8BF1C, + 0x4EF3E7, 0x426A11, 0xC426EA, 0x2AE476, 0xACA88D, 0xA0317B, 0x267D80, 0xB90297, 0x3F4E6C, + 0x33D79A, 0xB59B61, 0x8B654F, 0x0D29B4, 0x01B042, 0x87FCB9, 0x1883AE, 0x9ECF55, 0x9256A3, + 0x141A58, 0xEFAAFF, 0x69E604, 0x657FF2, 0xE33309, 0x7C4C1E, 0xFA00E5, 0xF69913, 0x70D5E8, + 0x4E2BC6, 0xC8673D, 0xC4FECB, 0x42B230, 0xDDCD27, 0x5B81DC, 0x57182A, 0xD154D1, 0x26359F, + 0xA07964, 0xACE092, 0x2AAC69, 0xB5D37E, 0x339F85, 0x3F0673, 0xB94A88, 0x87B4A6, 0x01F85D, + 0x0D61AB, 0x8B2D50, 0x145247, 0x921EBC, 0x9E874A, 0x18CBB1, 0xE37B16, 0x6537ED, 0x69AE1B, + 0xEFE2E0, 0x709DF7, 0xF6D10C, 0xFA48FA, 0x7C0401, 0x42FA2F, 0xC4B6D4, 0xC82F22, 0x4E63D9, + 0xD11CCE, 0x575035, 0x5BC9C3, 0xDD8538, +]; /// Calculate Qualcomm 24-bit Cyclical Redundancy Check (CRC-24Q). /// @@ -20,24 +83,53 @@ /// ]$$ /// /// Mask 0x1864CFB, not reversed, not XOR'd +/// +/// # Notes +/// +/// Only the lower 24 bits of the initial value are used! +#[must_use] pub fn compute_crc24q(buf: &[u8], initial_value: u32) -> u32 { - unsafe { swiftnav_sys::crc24q(buf.as_ptr(), buf.len() as u32, initial_value) } + let mut crc = initial_value & 0xFFFFFF; + for &byte in buf { + let index = ((crc >> 16) ^ byte as u32) as usize & 0xFF; + crc = ((crc << 8) & 0xFFFFFF) ^ CRC24Q_TABLE[index]; + } + crc } #[cfg(test)] mod tests { + use super::*; + use proptest::prelude::*; + const TEST_DATA: &[u8] = "123456789".as_bytes(); + /// Helper function to append a CRC-24Q value as 3 bytes (big-endian) to a buffer + fn append_crc24q(data: &mut Vec, crc: u32) { + data.push((crc >> 16) as u8); + data.push((crc >> 8) as u8); + data.push(crc as u8); + } + + /// Helper function to flip a single bit in the data at the given bit position + fn flip_bit(data: &mut [u8], bit_position: usize) { + if !data.is_empty() { + let byte_index = (bit_position / 8) % data.len(); + let bit_index = bit_position % 8; + data[byte_index] ^= 1 << bit_index; + } + } + #[test] - fn crc24q() { - let crc = super::compute_crc24q(&TEST_DATA[0..0], 0); + fn test_crc24q() { + let crc = compute_crc24q(&TEST_DATA[0..0], 0); assert!( crc == 0, "CRC of empty buffer with starting value 0 should be 0, not {}", crc ); - let crc = super::compute_crc24q(&TEST_DATA[0..0], 22); + let crc = compute_crc24q(&TEST_DATA[0..0], 22); assert!( crc == 22, "CRC of empty buffer with starting value 22 should be 22, not {}", @@ -46,11 +138,126 @@ mod tests { /* Test value taken from python crcmod package tests, see: * http://crcmod.sourceforge.net/crcmod.predefined.html */ - let crc = super::compute_crc24q(TEST_DATA, 0xB704CE); + let crc = compute_crc24q(TEST_DATA, 0xB704CE); assert!( crc == 0x21CF02, - "CRC of \"123456789\" with init value 0xB704CE should be {}, not 0x%06X", + "CRC of \"123456789\" with init value 0xB704CE should be 0x21CF02, not {}", crc ); } + + // Property-based tests using proptest + proptest! { + #![proptest_config(ProptestConfig::with_cases(1000))] + + /// Property: Appending the CRC to data and recalculating should yield zero. + /// This is the fundamental property used for error detection in protocols. + #[test] + fn prop_crc_append_yields_zero(data in prop::collection::vec(any::(), 0..1000)) { + let crc = compute_crc24q(&data, 0); + let mut data_with_crc = data.clone(); + append_crc24q(&mut data_with_crc, crc); + + let verification_crc = compute_crc24q(&data_with_crc, 0); + prop_assert_eq!(verification_crc, 0, + "CRC of data with appended CRC should be 0, got 0x{:06X} for data length {}", + verification_crc, data.len()); + } + + /// Property: CRC calculation is deterministic - same input always produces same output. + #[test] + fn prop_crc_is_deterministic(data in prop::collection::vec(any::(), 0..1000), init in any::()) { + let crc1 = compute_crc24q(&data, init); + let crc2 = compute_crc24q(&data, init); + prop_assert_eq!(crc1, crc2, "CRC calculation should be deterministic"); + } + + /// Property: CRC result always stays within 24-bit bounds (0x000000 to 0xFFFFFF). + #[test] + fn prop_crc_stays_within_24_bits(data in prop::collection::vec(any::(), 0..1000), init in any::()) { + let crc = compute_crc24q(&data, init); + prop_assert!(crc <= 0xFFFFFF, "CRC result 0x{:08X} exceeds 24-bit maximum", crc); + } + + /// Property: Incremental CRC calculation equals full calculation. + /// CRC(data1 + data2) should equal CRC(data2, initial=CRC(data1)) + #[test] + fn prop_crc_incremental_calculation( + data1 in prop::collection::vec(any::(), 0..500), + data2 in prop::collection::vec(any::(), 0..500), + init in any::() + ) { + // Calculate CRC on combined data + let mut combined_data = data1.clone(); + combined_data.extend_from_slice(&data2); + let full_crc = compute_crc24q(&combined_data, init); + + // Calculate CRC incrementally + let intermediate_crc = compute_crc24q(&data1, init); + let incremental_crc = compute_crc24q(&data2, intermediate_crc); + + prop_assert_eq!(full_crc, incremental_crc, + "Incremental CRC calculation should match full calculation"); + } + + /// Property: Initial values are properly masked to 24 bits. + /// init and (init & 0xFFFFFF) should produce the same result. + #[test] + fn prop_crc_initial_value_masked(data in prop::collection::vec(any::(), 0..100), init in any::()) { + let crc1 = compute_crc24q(&data, init); + let crc2 = compute_crc24q(&data, init & 0xFFFFFF); + prop_assert_eq!(crc1, crc2, + "CRC with init 0x{:08X} should equal CRC with masked init 0x{:06X}", + init, init & 0xFFFFFF); + } + + /// Property: Single bit errors are detected (CRC changes). + /// Flipping any single bit in non-empty data should change the CRC. + #[test] + fn prop_crc_detects_single_bit_errors( + mut data in prop::collection::vec(any::(), 1..100), + bit_position in any::(), + init in any::() + ) { + let original_crc = compute_crc24q(&data, init); + flip_bit(&mut data, bit_position); + let modified_crc = compute_crc24q(&data, init); + + prop_assert_ne!(original_crc, modified_crc, + "CRC should change when a bit is flipped (original: 0x{:06X}, modified: 0x{:06X})", + original_crc, modified_crc); + } + + /// Property: CRC calculation is associative when split into arbitrary chunks. + #[test] + fn prop_crc_associative_chunks( + data in prop::collection::vec(any::(), 1..200), + chunk_sizes in prop::collection::vec(1usize..50, 1..10), + init in any::() + ) { + // Calculate CRC on full data + let full_crc = compute_crc24q(&data, init); + + // Calculate CRC in chunks + let mut current_crc = init; + let mut pos = 0; + + for &chunk_size in &chunk_sizes { + if pos >= data.len() { + break; + } + let end = std::cmp::min(pos + chunk_size, data.len()); + current_crc = compute_crc24q(&data[pos..end], current_crc); + pos = end; + } + + // Process any remaining data + if pos < data.len() { + current_crc = compute_crc24q(&data[pos..], current_crc); + } + + prop_assert_eq!(full_crc, current_crc, + "CRC calculated in chunks should match full calculation"); + } + } }