Skip to main content

mfsk_core/msg/
packet_bytes.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//! `PacketBytesMessage` — variable-length byte-payload message codec.
3//!
4//! Worked example of a byte-oriented [`MessageCodec`]. Unlike the
5//! WSJT-style codecs ([`crate::msg::Wsjt77Message`],
6//! [`crate::msg::Wspr50Message`], [`crate::msg::Jt72Codec`]) which pack
7//! callsign / grid / report fields into a fixed-width payload, this
8//! codec carries an arbitrary byte slice of length 1..=10 in 91
9//! information bits — the K of [`crate::fec::Ldpc174_91`].
10//!
11//! No protocol in mfsk-core 0.3.0 uses this codec directly; it remains
12//! as a reference implementation demonstrating that the `MessageCodec`
13//! trait surface accommodates byte-oriented protocols, alongside the
14//! WSJT-77 callsign-packing flavour. Future binary-payload protocols
15//! (planned for 0.4.0+) can use it directly when the LDPC174_91 K=91
16//! payload size is a fit, or build analogous codecs for other LDPC
17//! sizes.
18//!
19//! ## Bit layout (91 bits)
20//!
21//! ```text
22//! bits  0 ..  4 : length code (4 bits) = (actual_length - 1)
23//! bits  4 .. 84 : 10 bytes × 8 = 80 bits, MSB-first per byte
24//! bits 84 .. 91 : CRC-7 over bits 0..84 (poly x^7 + x^3 + 1, 0x09)
25//! ```
26//!
27//! Length codes 0..=9 encode payload byte counts 1..=10. Codes 10..=15
28//! are reserved and cause [`MessageCodec::unpack`] to return `None`.
29//! The CRC-7 occupies the trailing 7 bits and is verified both on
30//! [`MessageCodec::unpack`] and as the in-FEC `verify_info` integrity
31//! check (BP rejects mid-iteration on CRC-7 fail). Polynomial
32//! `x^7 + x^3 + 1` (0x09) — the SD-card standard CRC-7. Hamming
33//! distance ≥ 3 over the 84-bit input; combined with LDPC's already-low
34//! post-FEC BER this drops false-decode rate by ~2 orders of magnitude
35//! versus the naive "always accept" verifier.
36//!
37//! [`MessageCodec::Unpacked = Vec<u8>`] — the codec's `unpack`
38//! returns the payload bytes only (length and CRC fields stripped).
39
40use crate::core::{DecodeContext, MessageCodec, MessageFields};
41
42/// Maximum payload length in bytes per frame.
43pub const MAX_PAYLOAD_BYTES: usize = 10;
44
45/// Number of head bits (length + payload) covered by the CRC.
46const HEAD_BITS: usize = 84;
47/// CRC-7 generator polynomial (`x^7 + x^3 + 1` = 0b1001001 = 0x09 in
48/// the standard SD-card form). Uses the leading `x^7` term implicitly;
49/// the value below is the 7-bit polynomial without the high bit.
50const CRC7_POLY: u8 = 0x09;
51
52/// CRC-7 over `bits` (one bit per byte, LSB), MSB-first bit order.
53///
54/// Returns the 7-bit CRC value (top 7 bits of the final shift register
55/// `<< 1`). Mirrors the canonical WSJT bit-buffer CRC pattern: shift in
56/// each input bit at the LSB of an 8-bit register, XOR the polynomial
57/// when the bit shifted out at position 7 is set.
58fn crc7(bits: &[u8]) -> u8 {
59    let mut crc: u8 = 0;
60    for &bit in bits {
61        let in_bit = bit & 1;
62        let top = (crc >> 6) & 1;
63        crc = ((crc << 1) | in_bit) & 0x7F;
64        if top ^ in_bit != 0 {
65            // Standard CRC-7 step: XOR the 7-bit poly when the bit
66            // about to overflow XOR'd with the incoming bit is 1.
67            crc ^= CRC7_POLY;
68        }
69    }
70    crc & 0x7F
71}
72
73/// Variable-length byte-payload codec. See module docs for the bit
74/// layout.
75#[derive(Copy, Clone, Debug, Default)]
76pub struct PacketBytesMessage;
77
78impl MessageCodec for PacketBytesMessage {
79    type Unpacked = Vec<u8>;
80
81    /// 91 information bits matching `Ldpc174_91`'s K. Of those, 4 bits
82    /// are length, 80 bits are up to 10 bytes of payload, and the
83    /// final 7 bits are a CRC-7 over the head 84 bits.
84    const PAYLOAD_BITS: u32 = 91;
85    /// CRC-7 trailing the payload — `x^7 + x^3 + 1` over bits 0..84.
86    /// The 7-bit CRC sits at info bits 84..91. See the private
87    /// `crc7` helper in this module / [`Self::verify_info`].
88    const CRC_BITS: u32 = 7;
89
90    fn pack(&self, fields: &MessageFields) -> Option<Vec<u8>> {
91        // The codec is byte-oriented: callers pass payload via the
92        // `free_text` field (interpreting the bytes as UTF-8 is up
93        // to the application — `Vec<u8>` is what comes back out).
94        let bytes = fields.free_text.as_ref()?.as_bytes();
95        if bytes.is_empty() || bytes.len() > MAX_PAYLOAD_BYTES {
96            return None;
97        }
98        let mut out = vec![0u8; PacketBytesMessage::PAYLOAD_BITS as usize];
99        // 4-bit length field (length - 1 in 0..=10, big-endian, MSB first).
100        let len_code = (bytes.len() - 1) as u8;
101        for i in 0..4 {
102            out[i] = (len_code >> (3 - i)) & 1;
103        }
104        // 80 bits of payload (10 bytes max), MSB first per byte. Bytes
105        // beyond `len` are zero-padded.
106        for byte_idx in 0..MAX_PAYLOAD_BYTES {
107            let b = if byte_idx < bytes.len() {
108                bytes[byte_idx]
109            } else {
110                0
111            };
112            for bit in 0..8 {
113                out[4 + byte_idx * 8 + bit] = (b >> (7 - bit)) & 1;
114            }
115        }
116        // CRC-7 over bits 0..84 in the trailing 7 bits.
117        let crc = crc7(&out[..HEAD_BITS]);
118        for i in 0..7 {
119            out[HEAD_BITS + i] = (crc >> (6 - i)) & 1;
120        }
121        Some(out)
122    }
123
124    fn unpack(&self, payload: &[u8], _ctx: &DecodeContext) -> Option<Self::Unpacked> {
125        if payload.len() != Self::PAYLOAD_BITS as usize {
126            return None;
127        }
128        // Verify CRC-7 first — rejects garbage that survived BP parity.
129        if !Self::verify_info(payload) {
130            return None;
131        }
132        // Length: 4 bits, big-endian, encodes (len - 1) in 0..=10.
133        let mut len_code: u8 = 0;
134        for i in 0..4 {
135            len_code = (len_code << 1) | (payload[i] & 1);
136        }
137        let len = len_code as usize + 1;
138        if len > MAX_PAYLOAD_BYTES {
139            return None;
140        }
141        // Payload bytes: 8 bits each, MSB first.
142        let mut out = Vec::with_capacity(len);
143        for byte_idx in 0..len {
144            let mut b: u8 = 0;
145            for bit in 0..8 {
146                b = (b << 1) | (payload[4 + byte_idx * 8 + bit] & 1);
147            }
148            out.push(b);
149        }
150        Some(out)
151    }
152
153    /// Verify the CRC-7 trailer. Called by the FEC layer (BP / OSD)
154    /// to reject parity-converged candidates whose CRC doesn't match —
155    /// substantially reduces the false-decode rate over the naive
156    /// "always accept" verifier.
157    fn verify_info(info: &[u8]) -> bool {
158        if info.len() != Self::PAYLOAD_BITS as usize {
159            return false;
160        }
161        let computed = crc7(&info[..HEAD_BITS]);
162        let mut received: u8 = 0;
163        for &b in &info[HEAD_BITS..(HEAD_BITS + 7)] {
164            received = (received << 1) | (b & 1);
165        }
166        computed == received
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    fn pack(bytes: &[u8]) -> Option<Vec<u8>> {
175        let fields = MessageFields {
176            free_text: Some(unsafe { std::str::from_utf8_unchecked(bytes) }.to_string()),
177            ..Default::default()
178        };
179        PacketBytesMessage.pack(&fields)
180    }
181
182    fn unpack(bits: &[u8]) -> Option<Vec<u8>> {
183        PacketBytesMessage.unpack(bits, &DecodeContext::default())
184    }
185
186    #[test]
187    fn pack_then_unpack_roundtrips_short_payload() {
188        let payload = b"hello";
189        let bits = pack(payload).expect("pack short");
190        let out = unpack(&bits).expect("unpack short");
191        assert_eq!(out, payload);
192    }
193
194    #[test]
195    fn pack_then_unpack_roundtrips_max_length() {
196        let payload = b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a"; // 10 bytes
197        assert_eq!(payload.len(), MAX_PAYLOAD_BYTES);
198        let bits = pack(payload).expect("pack 10");
199        let out = unpack(&bits).expect("unpack 10");
200        assert_eq!(out, payload);
201    }
202
203    #[test]
204    fn pack_then_unpack_roundtrips_single_byte() {
205        let payload = b"\x42";
206        let bits = pack(payload).expect("pack 1");
207        let out = unpack(&bits).expect("unpack 1");
208        assert_eq!(out, payload);
209    }
210
211    #[test]
212    fn pack_rejects_empty_payload() {
213        assert!(pack(b"").is_none(), "empty payload must be rejected");
214    }
215
216    #[test]
217    fn pack_rejects_oversize_payload() {
218        let bytes = vec![0x55_u8; 11]; // one byte over MAX_PAYLOAD_BYTES
219        let fields = MessageFields {
220            free_text: Some(unsafe { String::from_utf8_unchecked(bytes) }),
221            ..Default::default()
222        };
223        assert!(
224            PacketBytesMessage.pack(&fields).is_none(),
225            "11-byte payload must be rejected"
226        );
227    }
228
229    #[test]
230    fn unpack_rejects_wrong_length_buffer() {
231        let bits = vec![0u8; 90]; // off by one
232        assert!(unpack(&bits).is_none(), "bit buffer of length 90 rejected");
233        let bits = vec![0u8; 92];
234        assert!(unpack(&bits).is_none(), "bit buffer of length 92 rejected");
235    }
236
237    #[test]
238    fn unpack_rejects_invalid_length_code() {
239        // 4-bit length code = 10 → decoded length 11 > MAX_PAYLOAD_BYTES.
240        let mut bits = vec![0u8; 91];
241        bits[0] = 1;
242        bits[1] = 0;
243        bits[2] = 1;
244        bits[3] = 0; // 0b1010 = 10 → length 11
245        assert!(
246            unpack(&bits).is_none(),
247            "length code 10 (→ 11 bytes) must reject"
248        );
249    }
250
251    #[test]
252    fn pack_payload_bits_in_correct_positions() {
253        // Sanity-check the bit layout. Single-byte payload 0xAA:
254        //   length code = 0 (encodes 1 byte) → 4 bits of 0
255        //   byte 0 = 0xAA = 0b10101010 → bits[4..12] = 1,0,1,0,1,0,1,0
256        //   bits[12..84] = zero-padded payload tail
257        //   bits[84..91] = CRC-7 over bits[..84]
258        let bits = pack(b"\xAA").expect("pack 0xAA");
259        assert_eq!(bits.len(), 91);
260        assert_eq!(&bits[0..4], &[0, 0, 0, 0], "length code");
261        assert_eq!(&bits[4..12], &[1, 0, 1, 0, 1, 0, 1, 0], "byte 0 bits");
262        for &b in &bits[12..84] {
263            assert_eq!(b, 0, "payload tail must be zero");
264        }
265        // CRC-7 of bits[..84] must match what the codec wrote at [84..91].
266        let computed = crc7(&bits[..84]);
267        let mut stored: u8 = 0;
268        for &b in &bits[84..91] {
269            stored = (stored << 1) | (b & 1);
270        }
271        assert_eq!(stored, computed, "trailer must hold the CRC-7 of the head");
272    }
273
274    #[test]
275    fn unpack_rejects_bit_flip_in_payload() {
276        // A single bit flip anywhere in the head (length + payload)
277        // must invalidate the CRC-7 and cause unpack to return None,
278        // demonstrating the integrity check is wired correctly.
279        let mut bits = pack(b"hello").expect("pack");
280        // Flip a bit in the middle of the payload.
281        bits[20] ^= 1;
282        assert!(
283            unpack(&bits).is_none(),
284            "single bit flip must fail CRC-7 verification"
285        );
286    }
287
288    #[test]
289    fn unpack_rejects_bit_flip_in_crc() {
290        // A bit flip in the CRC-7 trailer alone must also fail.
291        let mut bits = pack(b"hi").expect("pack");
292        bits[88] ^= 1;
293        assert!(
294            unpack(&bits).is_none(),
295            "bit flip in CRC trailer must fail verification"
296        );
297    }
298
299    #[test]
300    fn verify_info_accepts_valid_pack_output() {
301        // Every output of `pack` must satisfy `verify_info` — it's the
302        // codec's own integrity contract that the FEC layer relies on.
303        for payload in [b"x".as_slice(), b"hello", b"\x00\x01\x02\x03\x04"] {
304            let bits = pack(payload).expect("pack");
305            assert!(
306                PacketBytesMessage::verify_info(&bits),
307                "verify_info must accept a fresh pack() output for {:?}",
308                payload
309            );
310        }
311    }
312
313    #[test]
314    fn verify_info_rejects_wrong_length() {
315        // The verifier is wired through `FecOpts::verify_info` and
316        // sees a slice whose length the FEC controls. Any length other
317        // than 91 must reject — guards against accidental misuse from
318        // a different FEC.
319        assert!(!PacketBytesMessage::verify_info(&[0u8; 90]));
320        assert!(!PacketBytesMessage::verify_info(&[0u8; 92]));
321        assert!(!PacketBytesMessage::verify_info(&[0u8; 0]));
322    }
323}