dstar_gateway_core/dprs/crc.rs
1//! DPRS CRC — CRC-CCITT over the DPRS payload bytes.
2//!
3//! Reference: `ircDDBGateway/Common/APRSCollector.cpp:371-394`
4//! (`CAPRSCollector::calcCRC`). The algorithm is:
5//!
6//! - initial value `0xFFFF`
7//! - reflected polynomial `0x8408`
8//! - final output `~accumulator & 0xFFFF` (bitwise NOT)
9//!
10//! This is identical to the D-STAR radio-header CRC-CCITT used in
11//! [`crate::header::crc_ccitt`], so this module simply delegates.
12
13use crate::header::crc_ccitt;
14
15/// Compute the DPRS CRC-CCITT over the given bytes.
16///
17/// Matches `CAPRSCollector::calcCRC` from ircDDBGateway: reflected
18/// polynomial `0x8408`, initial value `0xFFFF`, final `~accumulator`.
19/// Equivalent to [`crate::header::crc_ccitt`].
20///
21/// # Example
22/// ```
23/// # use dstar_gateway_core::dprs::compute_crc;
24/// // The empty-bytes CRC of CCITT with 0xFFFF init + final NOT is
25/// // the canonical 0x0000 (init ^ 0xFFFF).
26/// assert_eq!(compute_crc(b""), 0x0000);
27/// ```
28#[must_use]
29pub fn compute_crc(bytes: &[u8]) -> u16 {
30 crc_ccitt(bytes)
31}
32
33#[cfg(test)]
34mod tests {
35 use super::*;
36 use crate::header::crc_ccitt;
37
38 #[test]
39 fn empty_input_matches_header_crc() {
40 assert_eq!(compute_crc(b""), crc_ccitt(b""));
41 }
42
43 #[test]
44 fn short_input_matches_header_crc() {
45 assert_eq!(compute_crc(b"TEST"), crc_ccitt(b"TEST"));
46 }
47
48 #[test]
49 fn dprs_body_matches_header_crc() {
50 // A representative DPRS payload — everything after the
51 // `$$CRC<4hex>,` prefix up to but not including the final
52 // line terminator. The exact input doesn't matter; the point
53 // is that the CRC algorithm matches the header CRC.
54 let body = b"W1AW *>APDPRS,DSTAR*:!4041.27N/07229.28W>Test";
55 assert_eq!(compute_crc(body), crc_ccitt(body));
56 }
57
58 #[test]
59 fn empty_crc_is_zero() {
60 // CCITT with init 0xFFFF and final `~acc` → ~0xFFFF = 0x0000
61 // for the empty input (no bytes to process).
62 assert_eq!(compute_crc(b""), 0x0000);
63 }
64
65 #[test]
66 fn single_byte_crc_is_nonzero() {
67 // Sanity: a non-empty input must produce a non-zero CRC
68 // (otherwise the placeholder bug would still pass).
69 assert_ne!(compute_crc(b"A"), 0);
70 }
71
72 #[test]
73 fn known_dprs_body_regression() {
74 // Regression vector: a fixed DPRS body must have a stable
75 // CRC value. Computed by running the reference algorithm
76 // (init 0xFFFF, reflected poly 0x8408, final ~acc) and
77 // cross-checking with [`crc_ccitt`]. The body is the
78 // contents that would follow `$$CRC<4hex>,` in a live
79 // sentence — no line terminator, no prefix.
80 let body: &[u8] = b"W1AW *>APDPRS,DSTAR*:!4041.27N/07229.28W>Test";
81 let observed = compute_crc(body);
82 // Delegate sanity: this must equal `crc_ccitt(body)`.
83 assert_eq!(observed, crc_ccitt(body));
84 // Locked-in vector to prevent silent regression to a
85 // placeholder-zero implementation. This is the value the
86 // reference algorithm produces — any future edit that
87 // changes it needs a documented reason.
88 assert_eq!(observed, 0xB8D9);
89 }
90
91 #[test]
92 fn single_byte_a_has_known_crc() {
93 // Second locked-in vector: the CRC of the single byte `A`
94 // is `0xA3F5` under CRC-CCITT init 0xFFFF, reflected poly
95 // 0x8408, final `~acc`.
96 assert_eq!(compute_crc(b"A"), 0xA3F5);
97 }
98
99 #[test]
100 fn different_inputs_produce_different_crcs() {
101 // Two inputs differing by one byte must produce different
102 // CRCs — this would fail under the placeholder implementation
103 // that returned 0 regardless of input.
104 let a = compute_crc(b"HELLO");
105 let b = compute_crc(b"HELLP");
106 assert_ne!(a, b);
107 }
108}