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}