Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions swiftnav/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
219 changes: 213 additions & 6 deletions swiftnav/src/edc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
///
Expand All @@ -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<u8>, 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 {}",
Expand All @@ -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::<u8>(), 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::<u8>(), 0..1000), init in any::<u32>()) {
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::<u8>(), 0..1000), init in any::<u32>()) {
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::<u8>(), 0..500),
data2 in prop::collection::vec(any::<u8>(), 0..500),
init in any::<u32>()
) {
// 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::<u8>(), 0..100), init in any::<u32>()) {
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::<u8>(), 1..100),
bit_position in any::<usize>(),
init in any::<u32>()
) {
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::<u8>(), 1..200),
chunk_sizes in prop::collection::vec(1usize..50, 1..10),
init in any::<u32>()
) {
// 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");
}
}
}
Loading