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}