Skip to content

Commit 24d5643

Browse files
authored
Re-write the CRC functionality in Rust (#123)
## What's been rewritten - The `crc24q` function for computing the CRC-24Q checksum on byte-aligned data (e.g. RTCM messages) ## What's been left out - The `crc24q_bits` function for computing the CRC-24Q checksum on unaligned data (e.g. GPS CNAV messages) ## What's been removed No functionality has been removed in this PR
1 parent 36c1dee commit 24d5643

File tree

2 files changed

+214
-6
lines changed

2 files changed

+214
-6
lines changed

swiftnav/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ strum = { version = "0.27", features = ["derive"] }
1717

1818
[dev-dependencies]
1919
float_eq = "1.0.1"
20+
proptest = "1.5"
2021

2122
# This tells docs.rs to include the katex header for math formatting
2223
# To do this locally

swiftnav/src/edc.rs

Lines changed: 213 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,69 @@
88
// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
99
// WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
1010
//! Error detection code
11+
//!
12+
//! The checksum used by the RTCM protocol is CRC-24Q. This module provides a
13+
//! function for calculating that checksum value over a set of data.
14+
//!
15+
//! # Examples
16+
//!
17+
//! To generate a CRC value in one shot you can simply give the [`compute_crc24q`]
18+
//! function all of the data as a slice of bytes and the initial value:
19+
//!
20+
//! ```
21+
//! # use swiftnav::edc::compute_crc24q;
22+
//! 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];
23+
//! let init = 0;
24+
//!
25+
//! let crc = compute_crc24q(&msg_data, init);
26+
//! assert_eq!(crc, 0x00360B98);
27+
//! ```
28+
//!
29+
//! If the data is split up you can use the CRC from a previous invokation as the initial
30+
//! value for a subsequent invokation:
31+
//!
32+
//! ```
33+
//! # use swiftnav::edc::compute_crc24q;
34+
//! let block1 = vec![0xD3, 0x00, 0x13, 0x3E, 0xD7, 0xD3, 0x02, 0x02, 0x98, 0x0E];
35+
//! let block2 = vec![0xDE, 0xEF, 0x34, 0xB4, 0xBD, 0x62, 0xAC, 0x09, 0x41, 0x98, 0x6F, 0x33];
36+
//! let init = 0;
37+
//!
38+
//! let intermediate = compute_crc24q(&block1, init);
39+
//! let crc = compute_crc24q(&block2, intermediate);
40+
//! assert_eq!(crc, 0x00360B98);
41+
//! ```
42+
43+
const CRC24Q_TABLE: [u32; 256] = [
44+
0x000000, 0x864CFB, 0x8AD50D, 0x0C99F6, 0x93E6E1, 0x15AA1A, 0x1933EC, 0x9F7F17, 0xA18139,
45+
0x27CDC2, 0x2B5434, 0xAD18CF, 0x3267D8, 0xB42B23, 0xB8B2D5, 0x3EFE2E, 0xC54E89, 0x430272,
46+
0x4F9B84, 0xC9D77F, 0x56A868, 0xD0E493, 0xDC7D65, 0x5A319E, 0x64CFB0, 0xE2834B, 0xEE1ABD,
47+
0x685646, 0xF72951, 0x7165AA, 0x7DFC5C, 0xFBB0A7, 0x0CD1E9, 0x8A9D12, 0x8604E4, 0x00481F,
48+
0x9F3708, 0x197BF3, 0x15E205, 0x93AEFE, 0xAD50D0, 0x2B1C2B, 0x2785DD, 0xA1C926, 0x3EB631,
49+
0xB8FACA, 0xB4633C, 0x322FC7, 0xC99F60, 0x4FD39B, 0x434A6D, 0xC50696, 0x5A7981, 0xDC357A,
50+
0xD0AC8C, 0x56E077, 0x681E59, 0xEE52A2, 0xE2CB54, 0x6487AF, 0xFBF8B8, 0x7DB443, 0x712DB5,
51+
0xF7614E, 0x19A3D2, 0x9FEF29, 0x9376DF, 0x153A24, 0x8A4533, 0x0C09C8, 0x00903E, 0x86DCC5,
52+
0xB822EB, 0x3E6E10, 0x32F7E6, 0xB4BB1D, 0x2BC40A, 0xAD88F1, 0xA11107, 0x275DFC, 0xDCED5B,
53+
0x5AA1A0, 0x563856, 0xD074AD, 0x4F0BBA, 0xC94741, 0xC5DEB7, 0x43924C, 0x7D6C62, 0xFB2099,
54+
0xF7B96F, 0x71F594, 0xEE8A83, 0x68C678, 0x645F8E, 0xE21375, 0x15723B, 0x933EC0, 0x9FA736,
55+
0x19EBCD, 0x8694DA, 0x00D821, 0x0C41D7, 0x8A0D2C, 0xB4F302, 0x32BFF9, 0x3E260F, 0xB86AF4,
56+
0x2715E3, 0xA15918, 0xADC0EE, 0x2B8C15, 0xD03CB2, 0x567049, 0x5AE9BF, 0xDCA544, 0x43DA53,
57+
0xC596A8, 0xC90F5E, 0x4F43A5, 0x71BD8B, 0xF7F170, 0xFB6886, 0x7D247D, 0xE25B6A, 0x641791,
58+
0x688E67, 0xEEC29C, 0x3347A4, 0xB50B5F, 0xB992A9, 0x3FDE52, 0xA0A145, 0x26EDBE, 0x2A7448,
59+
0xAC38B3, 0x92C69D, 0x148A66, 0x181390, 0x9E5F6B, 0x01207C, 0x876C87, 0x8BF571, 0x0DB98A,
60+
0xF6092D, 0x7045D6, 0x7CDC20, 0xFA90DB, 0x65EFCC, 0xE3A337, 0xEF3AC1, 0x69763A, 0x578814,
61+
0xD1C4EF, 0xDD5D19, 0x5B11E2, 0xC46EF5, 0x42220E, 0x4EBBF8, 0xC8F703, 0x3F964D, 0xB9DAB6,
62+
0xB54340, 0x330FBB, 0xAC70AC, 0x2A3C57, 0x26A5A1, 0xA0E95A, 0x9E1774, 0x185B8F, 0x14C279,
63+
0x928E82, 0x0DF195, 0x8BBD6E, 0x872498, 0x016863, 0xFAD8C4, 0x7C943F, 0x700DC9, 0xF64132,
64+
0x693E25, 0xEF72DE, 0xE3EB28, 0x65A7D3, 0x5B59FD, 0xDD1506, 0xD18CF0, 0x57C00B, 0xC8BF1C,
65+
0x4EF3E7, 0x426A11, 0xC426EA, 0x2AE476, 0xACA88D, 0xA0317B, 0x267D80, 0xB90297, 0x3F4E6C,
66+
0x33D79A, 0xB59B61, 0x8B654F, 0x0D29B4, 0x01B042, 0x87FCB9, 0x1883AE, 0x9ECF55, 0x9256A3,
67+
0x141A58, 0xEFAAFF, 0x69E604, 0x657FF2, 0xE33309, 0x7C4C1E, 0xFA00E5, 0xF69913, 0x70D5E8,
68+
0x4E2BC6, 0xC8673D, 0xC4FECB, 0x42B230, 0xDDCD27, 0x5B81DC, 0x57182A, 0xD154D1, 0x26359F,
69+
0xA07964, 0xACE092, 0x2AAC69, 0xB5D37E, 0x339F85, 0x3F0673, 0xB94A88, 0x87B4A6, 0x01F85D,
70+
0x0D61AB, 0x8B2D50, 0x145247, 0x921EBC, 0x9E874A, 0x18CBB1, 0xE37B16, 0x6537ED, 0x69AE1B,
71+
0xEFE2E0, 0x709DF7, 0xF6D10C, 0xFA48FA, 0x7C0401, 0x42FA2F, 0xC4B6D4, 0xC82F22, 0x4E63D9,
72+
0xD11CCE, 0x575035, 0x5BC9C3, 0xDD8538,
73+
];
1174

1275
/// Calculate Qualcomm 24-bit Cyclical Redundancy Check (CRC-24Q).
1376
///
@@ -20,24 +83,53 @@
2083
/// ]$$
2184
///
2285
/// Mask 0x1864CFB, not reversed, not XOR'd
86+
///
87+
/// # Notes
88+
///
89+
/// Only the lower 24 bits of the initial value are used!
90+
#[must_use]
2391
pub fn compute_crc24q(buf: &[u8], initial_value: u32) -> u32 {
24-
unsafe { swiftnav_sys::crc24q(buf.as_ptr(), buf.len() as u32, initial_value) }
92+
let mut crc = initial_value & 0xFFFFFF;
93+
for &byte in buf {
94+
let index = ((crc >> 16) ^ byte as u32) as usize & 0xFF;
95+
crc = ((crc << 8) & 0xFFFFFF) ^ CRC24Q_TABLE[index];
96+
}
97+
crc
2598
}
2699

27100
#[cfg(test)]
28101
mod tests {
102+
use super::*;
103+
use proptest::prelude::*;
104+
29105
const TEST_DATA: &[u8] = "123456789".as_bytes();
30106

107+
/// Helper function to append a CRC-24Q value as 3 bytes (big-endian) to a buffer
108+
fn append_crc24q(data: &mut Vec<u8>, crc: u32) {
109+
data.push((crc >> 16) as u8);
110+
data.push((crc >> 8) as u8);
111+
data.push(crc as u8);
112+
}
113+
114+
/// Helper function to flip a single bit in the data at the given bit position
115+
fn flip_bit(data: &mut [u8], bit_position: usize) {
116+
if !data.is_empty() {
117+
let byte_index = (bit_position / 8) % data.len();
118+
let bit_index = bit_position % 8;
119+
data[byte_index] ^= 1 << bit_index;
120+
}
121+
}
122+
31123
#[test]
32-
fn crc24q() {
33-
let crc = super::compute_crc24q(&TEST_DATA[0..0], 0);
124+
fn test_crc24q() {
125+
let crc = compute_crc24q(&TEST_DATA[0..0], 0);
34126
assert!(
35127
crc == 0,
36128
"CRC of empty buffer with starting value 0 should be 0, not {}",
37129
crc
38130
);
39131

40-
let crc = super::compute_crc24q(&TEST_DATA[0..0], 22);
132+
let crc = compute_crc24q(&TEST_DATA[0..0], 22);
41133
assert!(
42134
crc == 22,
43135
"CRC of empty buffer with starting value 22 should be 22, not {}",
@@ -46,11 +138,126 @@ mod tests {
46138

47139
/* Test value taken from python crcmod package tests, see:
48140
* http://crcmod.sourceforge.net/crcmod.predefined.html */
49-
let crc = super::compute_crc24q(TEST_DATA, 0xB704CE);
141+
let crc = compute_crc24q(TEST_DATA, 0xB704CE);
50142
assert!(
51143
crc == 0x21CF02,
52-
"CRC of \"123456789\" with init value 0xB704CE should be {}, not 0x%06X",
144+
"CRC of \"123456789\" with init value 0xB704CE should be 0x21CF02, not {}",
53145
crc
54146
);
55147
}
148+
149+
// Property-based tests using proptest
150+
proptest! {
151+
#![proptest_config(ProptestConfig::with_cases(1000))]
152+
153+
/// Property: Appending the CRC to data and recalculating should yield zero.
154+
/// This is the fundamental property used for error detection in protocols.
155+
#[test]
156+
fn prop_crc_append_yields_zero(data in prop::collection::vec(any::<u8>(), 0..1000)) {
157+
let crc = compute_crc24q(&data, 0);
158+
let mut data_with_crc = data.clone();
159+
append_crc24q(&mut data_with_crc, crc);
160+
161+
let verification_crc = compute_crc24q(&data_with_crc, 0);
162+
prop_assert_eq!(verification_crc, 0,
163+
"CRC of data with appended CRC should be 0, got 0x{:06X} for data length {}",
164+
verification_crc, data.len());
165+
}
166+
167+
/// Property: CRC calculation is deterministic - same input always produces same output.
168+
#[test]
169+
fn prop_crc_is_deterministic(data in prop::collection::vec(any::<u8>(), 0..1000), init in any::<u32>()) {
170+
let crc1 = compute_crc24q(&data, init);
171+
let crc2 = compute_crc24q(&data, init);
172+
prop_assert_eq!(crc1, crc2, "CRC calculation should be deterministic");
173+
}
174+
175+
/// Property: CRC result always stays within 24-bit bounds (0x000000 to 0xFFFFFF).
176+
#[test]
177+
fn prop_crc_stays_within_24_bits(data in prop::collection::vec(any::<u8>(), 0..1000), init in any::<u32>()) {
178+
let crc = compute_crc24q(&data, init);
179+
prop_assert!(crc <= 0xFFFFFF, "CRC result 0x{:08X} exceeds 24-bit maximum", crc);
180+
}
181+
182+
/// Property: Incremental CRC calculation equals full calculation.
183+
/// CRC(data1 + data2) should equal CRC(data2, initial=CRC(data1))
184+
#[test]
185+
fn prop_crc_incremental_calculation(
186+
data1 in prop::collection::vec(any::<u8>(), 0..500),
187+
data2 in prop::collection::vec(any::<u8>(), 0..500),
188+
init in any::<u32>()
189+
) {
190+
// Calculate CRC on combined data
191+
let mut combined_data = data1.clone();
192+
combined_data.extend_from_slice(&data2);
193+
let full_crc = compute_crc24q(&combined_data, init);
194+
195+
// Calculate CRC incrementally
196+
let intermediate_crc = compute_crc24q(&data1, init);
197+
let incremental_crc = compute_crc24q(&data2, intermediate_crc);
198+
199+
prop_assert_eq!(full_crc, incremental_crc,
200+
"Incremental CRC calculation should match full calculation");
201+
}
202+
203+
/// Property: Initial values are properly masked to 24 bits.
204+
/// init and (init & 0xFFFFFF) should produce the same result.
205+
#[test]
206+
fn prop_crc_initial_value_masked(data in prop::collection::vec(any::<u8>(), 0..100), init in any::<u32>()) {
207+
let crc1 = compute_crc24q(&data, init);
208+
let crc2 = compute_crc24q(&data, init & 0xFFFFFF);
209+
prop_assert_eq!(crc1, crc2,
210+
"CRC with init 0x{:08X} should equal CRC with masked init 0x{:06X}",
211+
init, init & 0xFFFFFF);
212+
}
213+
214+
/// Property: Single bit errors are detected (CRC changes).
215+
/// Flipping any single bit in non-empty data should change the CRC.
216+
#[test]
217+
fn prop_crc_detects_single_bit_errors(
218+
mut data in prop::collection::vec(any::<u8>(), 1..100),
219+
bit_position in any::<usize>(),
220+
init in any::<u32>()
221+
) {
222+
let original_crc = compute_crc24q(&data, init);
223+
flip_bit(&mut data, bit_position);
224+
let modified_crc = compute_crc24q(&data, init);
225+
226+
prop_assert_ne!(original_crc, modified_crc,
227+
"CRC should change when a bit is flipped (original: 0x{:06X}, modified: 0x{:06X})",
228+
original_crc, modified_crc);
229+
}
230+
231+
/// Property: CRC calculation is associative when split into arbitrary chunks.
232+
#[test]
233+
fn prop_crc_associative_chunks(
234+
data in prop::collection::vec(any::<u8>(), 1..200),
235+
chunk_sizes in prop::collection::vec(1usize..50, 1..10),
236+
init in any::<u32>()
237+
) {
238+
// Calculate CRC on full data
239+
let full_crc = compute_crc24q(&data, init);
240+
241+
// Calculate CRC in chunks
242+
let mut current_crc = init;
243+
let mut pos = 0;
244+
245+
for &chunk_size in &chunk_sizes {
246+
if pos >= data.len() {
247+
break;
248+
}
249+
let end = std::cmp::min(pos + chunk_size, data.len());
250+
current_crc = compute_crc24q(&data[pos..end], current_crc);
251+
pos = end;
252+
}
253+
254+
// Process any remaining data
255+
if pos < data.len() {
256+
current_crc = compute_crc24q(&data[pos..], current_crc);
257+
}
258+
259+
prop_assert_eq!(full_crc, current_crc,
260+
"CRC calculated in chunks should match full calculation");
261+
}
262+
}
56263
}

0 commit comments

Comments
 (0)