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