Skip to main content

oxideav_h261/
bch.rs

1//! H.261 §5.4 — BCH (511, 493) forward error-correction framing.
2//!
3//! H.261 transmits the video bitstream wrapped in an outer FEC layer
4//! intended for the noisy ISDN p × 64 kbit/s channels of 1990. The
5//! decoder's use of this layer is optional (§2.7) — for a clean file or
6//! a reliable transport (RTP, MP4) the inner video bitstream is enough.
7//! When present, the FEC frame layout is:
8//!
9//! ```text
10//!  ┌────────┬────┬───────────────────────┬──────────┐
11//!  │  Si    │ Fi │      Coded data       │  Parity  │
12//!  │ 1 bit  │1bit│       492 bits        │  18 bits │
13//!  └────────┴────┴───────────────────────┴──────────┘
14//!   total = 512 bits per error-correcting frame
15//! ```
16//!
17//! The BCH (511, 493) code protects the 493-bit `Fi || coded-data`
18//! field with an 18-bit parity field (493 + 18 = 511 protected bits);
19//! the leading 1-bit `Si` framing bit is unprotected, bringing the
20//! per-frame transmitted total to 512 bits.
21//!
22//! `Si` is the i-th bit of the 8-frame multiframe alignment pattern
23//! `S1..S8 = 0 0 0 1 1 0 1 1`. `Fi=1` indicates the 492-bit field carries
24//! coded video; `Fi=0` indicates a stuffing frame whose 492 bits are all
25//! `1`. The BCH parity is computed over the **493 bits** consisting of
26//! `Fi` followed by the 492-bit data field (so the parity covers
27//! whichever of the two `Fi` interpretations applies).
28//!
29//! ## Generator polynomial (§5.4.2)
30//!
31//! ```text
32//!   g(x) = (x^9 + x^4 + 1)(x^9 + x^6 + x^4 + x^3 + 1)
33//!        = x^18 + x^15 + x^12 + x^10 + x^8 + x^7 + x^6 + x^3 + 1
34//! ```
35//!
36//! As a 19-bit binary value with x^18 in the most-significant position,
37//! `g = 0b100_1001_0101_1100_1001 = 0x495C9`. The BCH (511, 493) code
38//! is a double-error-correcting / single-error-detecting code over
39//! GF(2^9): each factor is the primitive minimal polynomial of a 9-bit
40//! root, jointly enabling correction of up to t = (511 - 493) / (2·9) =
41//! 1 error in a 511-bit codeword (this is a t = 1 code despite the
42//! 18-bit parity — the 18-bit parity buys 1-bit correction + 1-bit
43//! detection rather than 2-bit correction; see RFC 4587 for typical
44//! deployment patterns).
45//!
46//! ## What this module provides
47//!
48//! * [`parity18`] — compute the 18-bit BCH parity over a 493-bit input.
49//! * [`syndrome18`] — compute the 18-bit syndrome over a 511-bit codeword;
50//!   zero ⇒ no error, non-zero ⇒ at least one bit error.
51//! * [`locate_single_error`] — for a non-zero syndrome, attempt to map it
52//!   to the position of a single-bit error in the 511-bit codeword. The
53//!   BCH (511, 493) code is a `t = 1` correcting code, so this is the
54//!   maximum-likelihood correction guaranteed by the spec.
55//! * [`encode_multiframe`] — wrap a slice of `coded_data: &[u8]` into an
56//!   integer number of 8-frame multiframes (4096 bits = 512 bytes each),
57//!   inserting alignment bits and `Fi`/fill frames as required by §5.4.3.
58//! * [`decode_multiframe`] — strip the FEC framing from a byte buffer
59//!   produced by [`encode_multiframe`], returning the inner coded data.
60//!   The decoder achieves frame lock after 3 consecutive valid alignment
61//!   sequences (§5.4.4); it then strips parity / framing bits and
62//!   surfaces the inner data. A syndrome check is run on every frame and
63//!   the number of corrupted (non-zero-syndrome) frames is reported back
64//!   to the caller as a diagnostic.
65//! * [`decode_multiframe_with_correction`] — same as [`decode_multiframe`]
66//!   but additionally applies the `t = 1` BCH correction to every frame
67//!   with a non-zero syndrome, reporting per-frame correction success /
68//!   failure separately from the raw `corrupted_frames` detection count.
69//!
70//! The H.261 video bitstream the rest of this crate emits / consumes is
71//! oblivious to this layer. Callers that need framed output (e.g. for
72//! transport over a raw bit-serial p × 64 kbit/s link) wrap their bytes
73//! with [`encode_multiframe`]; callers receiving a framed stream (RFC
74//! 4587 §6.2 is one historical example) recover the inner stream with
75//! [`decode_multiframe`].
76
77/// Generator polynomial g(x), 19 bits wide (degree 18). MSB = x^18.
78/// `(x^9 + x^4 + 1)(x^9 + x^6 + x^4 + x^3 + 1)
79///   = x^18 + x^15 + x^12 + x^10 + x^8 + x^7 + x^6 + x^3 + 1`.
80pub const GEN_POLY: u32 = 0x4_95C9;
81
82/// Width of the BCH parity field, in bits.
83pub const PARITY_BITS: u32 = 18;
84
85/// Width of the BCH data field per error-correcting frame, in bits.
86/// Includes the `Fi` bit (1) + the 492 coded-data bits = 493 bits total.
87pub const DATA_BITS: u32 = 493;
88
89/// Total error-correcting frame width = framing bit + data + parity = 512 bits.
90/// (The BCH code itself is (511, 493): 493 protected bits + 18 parity bits.
91/// The 1-bit `Si` framing bit is transmitted alongside but not BCH-protected.)
92pub const FRAME_BITS: u32 = 1 + DATA_BITS + PARITY_BITS;
93
94/// Number of frames per multiframe. The 8 framing bits across these
95/// frames form the alignment pattern `S1..S8 = 0 0 0 1 1 0 1 1` (§5.4.3).
96pub const MULTIFRAME_FRAMES: u32 = 8;
97
98/// 8-frame alignment pattern, MSB-first as transmitted: `0 0 0 1 1 0 1 1`.
99/// Indexable as `ALIGNMENT_PATTERN[frame_index_in_multiframe]`; frame 0
100/// is `S1`, frame 7 is `S8`.
101pub const ALIGNMENT_PATTERN: [u8; 8] = [0, 0, 0, 1, 1, 0, 1, 1];
102
103/// Compute the 18-bit BCH parity for a 493-bit input message.
104///
105/// The message is taken from the most-significant bits of `data`, with
106/// bit 0 of `data[0]` being the first (MSB-first) input bit. Bits beyond
107/// position 492 are ignored.
108///
109/// Algorithm: long division of (msg << 18) by `GEN_POLY` over GF(2),
110/// which is the textbook shift-register implementation. The returned
111/// `u32` has the parity in its low 18 bits; bits 18..31 are zero.
112pub fn parity18(data: &[u8]) -> u32 {
113    debug_assert!(
114        data.len() * 8 >= DATA_BITS as usize,
115        "parity18 needs at least {} bits of input ({} bytes)",
116        DATA_BITS,
117        DATA_BITS.div_ceil(8)
118    );
119    // 19-bit shift register; we keep 19 bits in `reg` and XOR `GEN_POLY`
120    // whenever bit 18 is set after shifting in a message bit.
121    let mut reg: u32 = 0;
122    for i in 0..(DATA_BITS as usize) {
123        let bit = (data[i / 8] >> (7 - (i & 7))) & 1;
124        reg = (reg << 1) | bit as u32;
125        if (reg >> 18) & 1 != 0 {
126            reg ^= GEN_POLY;
127        }
128    }
129    // After 493 input bits, shift in 18 zero "tail" bits to flush the
130    // register; the residue in the low 18 bits is the parity.
131    for _ in 0..PARITY_BITS {
132        reg <<= 1;
133        if (reg >> 18) & 1 != 0 {
134            reg ^= GEN_POLY;
135        }
136    }
137    reg & ((1 << PARITY_BITS) - 1)
138}
139
140/// Compute the syndrome of a 511-bit codeword: data ‖ parity, with `data`
141/// in the high 493 bits and `parity` in the low 18 bits.
142///
143/// A zero return value means the codeword is consistent with the BCH
144/// generator polynomial. A non-zero return value means at least one bit
145/// was corrupted. For ≤ t = 1 single-bit errors the syndrome uniquely
146/// identifies the position via [`locate_single_error`]; the integrated
147/// correction path is [`decode_multiframe_with_correction`]. Callers
148/// that want only detection (or are confident that frame-rate drop-out
149/// from the inner H.261 VLC's GOB resync is cheaper than acting on the
150/// corrected bit) use [`decode_multiframe`], which surfaces the syndrome
151/// via `corrupted_frames` without correcting.
152pub fn syndrome18(data: &[u8], parity: u32) -> u32 {
153    let mut reg: u32 = 0;
154    for i in 0..(DATA_BITS as usize) {
155        let bit = (data[i / 8] >> (7 - (i & 7))) & 1;
156        reg = (reg << 1) | bit as u32;
157        if (reg >> 18) & 1 != 0 {
158            reg ^= GEN_POLY;
159        }
160    }
161    // Now shift in the 18 parity bits MSB-first.
162    for i in 0..PARITY_BITS {
163        let bit = (parity >> (PARITY_BITS - 1 - i)) & 1;
164        reg = (reg << 1) | bit;
165        if (reg >> 18) & 1 != 0 {
166            reg ^= GEN_POLY;
167        }
168    }
169    reg & ((1 << PARITY_BITS) - 1)
170}
171
172/// Attempt to locate a single-bit error in a 511-bit BCH codeword from its
173/// 18-bit syndrome.
174///
175/// The BCH (511, 493) code defined by `g(x)` has minimum distance `d = 3`
176/// and corrects up to `t = (d − 1) / 2 = 1` errors. For a clean codeword
177/// `c(x)` we have `c(x) mod g(x) == 0`. For a single-bit error at
178/// position `p` (0 = first transmitted protected bit = `Fi`, 510 = last
179/// parity bit), the error polynomial is `e(x) = x^(510 − p)` and the
180/// syndrome equals `x^(510 − p) mod g(x)`.
181///
182/// This function returns `Some(p)` if the syndrome exactly matches a
183/// single-bit error pattern; otherwise it returns `None` (either the
184/// syndrome was zero — no error — or it corresponds to an error pattern
185/// of weight ≥ 2 that the t = 1 code cannot uniquely resolve). A
186/// `None` return for a non-zero syndrome is the spec's documented
187/// behaviour: §5.4 promises single-bit correction only; multi-bit
188/// patterns must be passed through for the inner H.261 GOB-resync to
189/// recover.
190///
191/// The implementation walks `pow = x^i mod g(x)` for `i = 0..511` and
192/// stops when `pow == syndrome`. The walk costs at most 511 18-bit
193/// shift-XOR steps and allocates nothing.
194pub fn locate_single_error(syndrome: u32) -> Option<u32> {
195    let mask = (1u32 << PARITY_BITS) - 1;
196    let synd = syndrome & mask;
197    if synd == 0 {
198        return None;
199    }
200    // pow tracks x^i mod g(x), starting at i = 0 ⇒ x^0 = 1.
201    let mut pow: u32 = 1;
202    for i in 0..(FRAME_BITS - 1) as u32 {
203        if pow == synd {
204            // Found: error position p such that (510 - p) == i.
205            return Some((FRAME_BITS - 2) - i);
206        }
207        // pow := (pow << 1) mod g(x).
208        pow <<= 1;
209        if (pow >> PARITY_BITS) & 1 != 0 {
210            pow ^= GEN_POLY;
211        }
212        pow &= mask;
213    }
214    // After 511 iterations the syndrome did not match any single-bit
215    // error pattern: must be a multi-bit error the t = 1 code can't
216    // correct.
217    None
218}
219
220/// Wrap `coded_data` (an MSB-first packed bitstream) into FEC multiframes
221/// per §5.4.3. The returned `Vec<u8>` is MSB-first packed too. Length is
222/// always an integer multiple of `FRAME_BITS * MULTIFRAME_FRAMES / 8 =
223/// 512 bytes` (8 frames × 512 bits each).
224///
225/// If `coded_data` does not fill an integer number of frames, the final
226/// frame's data field is filled with `Fi = 0` + 492 one-bits per §5.4.3.
227/// The caller's bits always land before any fill (so a decoder that
228/// honours `Fi` will not surface the fill bits to its consumer).
229///
230/// `coded_data_bits` is the number of valid bits in `coded_data`. If
231/// `coded_data_bits` is greater than `coded_data.len() * 8`, the function
232/// panics (this is a programming error, not an FEC condition). If
233/// `coded_data_bits == 0`, a single stuffing-only multiframe is emitted.
234pub fn encode_multiframe(coded_data: &[u8], coded_data_bits: usize) -> Vec<u8> {
235    assert!(
236        coded_data_bits <= coded_data.len() * 8,
237        "encode_multiframe: coded_data_bits ({}) exceeds buffer ({} bits)",
238        coded_data_bits,
239        coded_data.len() * 8
240    );
241
242    // Each FEC frame carries 492 coded bits when Fi=1.
243    let coded_per_frame = (DATA_BITS - 1) as usize; // 492
244    let frames_needed = if coded_data_bits == 0 {
245        MULTIFRAME_FRAMES as usize
246    } else {
247        let n = coded_data_bits.div_ceil(coded_per_frame);
248        // Round up to a whole multiframe (8 frames).
249        n.div_ceil(MULTIFRAME_FRAMES as usize) * MULTIFRAME_FRAMES as usize
250    };
251
252    // Bit-write buffer: 511 bits/frame.
253    let total_bits = frames_needed * FRAME_BITS as usize;
254    let mut out = vec![0u8; total_bits.div_ceil(8)];
255    let mut write_pos: usize = 0;
256    let put_bit = |out: &mut [u8], pos: usize, bit: u8| {
257        let byte = pos / 8;
258        let shift = 7 - (pos & 7);
259        out[byte] = (out[byte] & !(1u8 << shift)) | ((bit & 1) << shift);
260    };
261
262    let read_bit_in = |coded: &[u8], i: usize| -> u8 { (coded[i / 8] >> (7 - (i & 7))) & 1 };
263
264    // 493-bit scratch (Fi + 492 data bits) for parity computation.
265    let mut scratch = [0u8; 62]; // ceil(493/8) = 62
266
267    let mut consumed_bits = 0usize;
268    for f in 0..frames_needed {
269        let s_idx = f % MULTIFRAME_FRAMES as usize;
270        let s_bit = ALIGNMENT_PATTERN[s_idx];
271
272        // Decide Fi: 1 if any coded data remains, else 0 (stuffing).
273        let fi: u8 = if consumed_bits < coded_data_bits {
274            1
275        } else {
276            0
277        };
278
279        // Build the 493-bit `Fi || data` scratch buffer MSB-first.
280        for b in scratch.iter_mut() {
281            *b = 0;
282        }
283        // Bit 0 of scratch = Fi.
284        scratch[0] = fi << 7;
285        for j in 0..coded_per_frame {
286            let bit = if fi == 1 {
287                if consumed_bits < coded_data_bits {
288                    let v = read_bit_in(coded_data, consumed_bits);
289                    consumed_bits += 1;
290                    v
291                } else {
292                    // The frame is Fi=1 but coded_data didn't run out
293                    // exactly at the frame boundary; pad the tail with
294                    // ones (consistent with MBA-stuffing convention).
295                    1
296                }
297            } else {
298                // Fi=0 ⇒ all-ones fill.
299                1
300            };
301            // scratch position j+1 (since bit 0 was Fi).
302            let pos = j + 1;
303            scratch[pos / 8] |= bit << (7 - (pos & 7));
304        }
305
306        let par = parity18(&scratch);
307
308        // Emit: S || Fi || data || parity, MSB-first.
309        put_bit(&mut out, write_pos, s_bit);
310        write_pos += 1;
311        put_bit(&mut out, write_pos, fi);
312        write_pos += 1;
313        for j in 0..coded_per_frame {
314            let bit = (scratch[(j + 1) / 8] >> (7 - ((j + 1) & 7))) & 1;
315            put_bit(&mut out, write_pos, bit);
316            write_pos += 1;
317        }
318        for j in 0..(PARITY_BITS as usize) {
319            let bit = ((par >> (PARITY_BITS as usize - 1 - j)) & 1) as u8;
320            put_bit(&mut out, write_pos, bit);
321            write_pos += 1;
322        }
323    }
324
325    debug_assert_eq!(write_pos, total_bits);
326    out
327}
328
329/// Outcome of [`decode_multiframe`] and [`decode_multiframe_with_correction`].
330#[derive(Clone, Debug, PartialEq, Eq)]
331pub struct DecodedMultiframe {
332    /// The recovered inner coded video bitstream, MSB-first packed.
333    pub data: Vec<u8>,
334    /// Number of bits in `data` (a multiple of 492 when input is whole
335    /// frames; the caller can trim further if it knows the inner-stream
336    /// byte boundary).
337    pub data_bits: usize,
338    /// Number of FEC frames consumed.
339    pub frames_consumed: usize,
340    /// Number of frames whose 18-bit syndrome was non-zero (i.e. at
341    /// least one bit error was detected). Surfaced to the caller for
342    /// diagnostics; the data from those frames is included in `data`
343    /// regardless. With [`decode_multiframe`] this is the only error
344    /// signal; with [`decode_multiframe_with_correction`] the breakdown
345    /// is reported via `corrected_frames` + `uncorrectable_frames`.
346    pub corrupted_frames: usize,
347    /// Number of fill-only frames skipped (Fi=0 / inner data all ones).
348    pub fill_frames: usize,
349    /// Number of frames whose single-bit error was successfully located
350    /// and flipped by [`locate_single_error`] before the data was
351    /// emitted. Always `0` for [`decode_multiframe`] (which never
352    /// corrects). A subset of `corrupted_frames`.
353    pub corrected_frames: usize,
354    /// Number of frames with a non-zero syndrome that could not be
355    /// resolved as a single-bit error (i.e. weight ≥ 2 errors that the
356    /// `t = 1` code cannot correct). Always `0` for
357    /// [`decode_multiframe`]. The complement of `corrected_frames`
358    /// within `corrupted_frames`: `corrupted_frames == corrected_frames
359    /// + uncorrectable_frames` after correction.
360    pub uncorrectable_frames: usize,
361}
362
363/// Strip the BCH-framing layer from `framed`, beginning at bit 0.
364///
365/// Lock criterion per §5.4.4: three consecutive complete alignment
366/// sequences (24 framing bits ≡ 3 full multiframes' worth of `S_i`
367/// bits) must match the pattern `0 0 0 1 1 0 1 1` (repeated). The
368/// decoder scans every candidate `bit0` in `[0, FRAME_BITS)` for the
369/// earliest position where 24 consecutive framing bits at the proper
370/// `FRAME_BITS` stride form `(00011011)^3`. Once lock is established
371/// the decoder strips framing + parity from every subsequent frame in
372/// `framed`; it does **not** re-seek for the pattern within a frame
373/// (a single corrupted S-bit is reported as one corrupted frame —
374/// surfaced via the per-frame syndrome — but does not break lock).
375///
376/// The `(00011011)^3` requirement is strong enough to reject random
377/// bitstreams with high probability: the chance of a 24-bit specific
378/// pattern appearing randomly at the right stride is 2^-24.
379///
380/// Returns `None` if no valid alignment lock can be obtained anywhere
381/// in `framed`.
382pub fn decode_multiframe(framed: &[u8]) -> Option<DecodedMultiframe> {
383    let total_bits = framed.len() * 8;
384    // We need at least 3 full multiframes (24 frames) to establish lock.
385    let lock_frames = 3 * MULTIFRAME_FRAMES as usize;
386    let lock_span_bits = lock_frames * FRAME_BITS as usize;
387    if total_bits < lock_span_bits {
388        return None;
389    }
390
391    let read_bit = |pos: usize| -> u8 { (framed[pos / 8] >> (7 - (pos & 7))) & 1 };
392
393    let mut lock: Option<usize> = None;
394    'outer: for bit0 in 0..FRAME_BITS as usize {
395        if bit0 + lock_span_bits > total_bits {
396            break;
397        }
398        // The first frame at `bit0` must be the start of a multiframe
399        // (phase 0 ⇒ S1=0). Verify 24 framing bits across 3 multiframes.
400        for k in 0..lock_frames {
401            let s = read_bit(bit0 + k * FRAME_BITS as usize);
402            if s != ALIGNMENT_PATTERN[k % MULTIFRAME_FRAMES as usize] {
403                continue 'outer;
404            }
405        }
406        lock = Some(bit0);
407        break;
408    }
409
410    let bit0 = lock?;
411    let phase0 = 0usize; // lock is always to a multiframe boundary
412
413    let mut data: Vec<u8> = Vec::new();
414    let mut data_bits = 0usize;
415    let mut put_data_bit = |bit: u8| {
416        if data_bits % 8 == 0 {
417            data.push(0);
418        }
419        let byte_idx = data_bits / 8;
420        let shift = 7 - (data_bits & 7);
421        data[byte_idx] |= (bit & 1) << shift;
422        data_bits += 1;
423    };
424
425    let mut frames_consumed = 0usize;
426    let mut corrupted_frames = 0usize;
427    let mut fill_frames = 0usize;
428
429    let mut cursor = bit0;
430    while cursor + FRAME_BITS as usize <= total_bits {
431        let frame_idx = (phase0 + frames_consumed) % MULTIFRAME_FRAMES as usize;
432        let _expected_s = ALIGNMENT_PATTERN[frame_idx];
433        // S bit
434        let _s = read_bit(cursor);
435        let fi = read_bit(cursor + 1);
436
437        // Re-pack `Fi || data` into a scratch for syndrome verification.
438        let mut scratch = [0u8; 62];
439        scratch[0] = fi << 7;
440        let data_start = cursor + 2;
441        for j in 0..((DATA_BITS - 1) as usize) {
442            let bit = read_bit(data_start + j);
443            let pos = j + 1;
444            scratch[pos / 8] |= bit << (7 - (pos & 7));
445        }
446        // Read parity (18 bits).
447        let mut par = 0u32;
448        let par_start = data_start + (DATA_BITS - 1) as usize;
449        for j in 0..(PARITY_BITS as usize) {
450            par = (par << 1) | read_bit(par_start + j) as u32;
451        }
452        if syndrome18(&scratch, par) != 0 {
453            corrupted_frames += 1;
454        }
455        if fi == 1 {
456            // Emit the 492 coded-data bits.
457            for j in 0..((DATA_BITS - 1) as usize) {
458                let bit = read_bit(data_start + j);
459                put_data_bit(bit);
460            }
461        } else {
462            fill_frames += 1;
463        }
464
465        frames_consumed += 1;
466        cursor += FRAME_BITS as usize;
467    }
468
469    Some(DecodedMultiframe {
470        data,
471        data_bits,
472        frames_consumed,
473        corrupted_frames,
474        fill_frames,
475        corrected_frames: 0,
476        uncorrectable_frames: 0,
477    })
478}
479
480/// Like [`decode_multiframe`] but actively performs the `t = 1` BCH
481/// single-bit correction the (511, 493) code guarantees.
482///
483/// For every frame whose 18-bit syndrome is non-zero,
484/// [`locate_single_error`] is called to map the syndrome to a candidate
485/// error position. If a position is found, the bit at that position
486/// inside the 511-bit codeword (`Fi || 492-bit data || 18-bit parity`)
487/// is flipped before the per-frame `Fi` / data interpretation runs, and
488/// the frame counts toward `corrected_frames`. If the syndrome cannot
489/// be resolved as a single-bit error (i.e. it does not appear in the
490/// 511 single-bit-error pattern set — implying weight ≥ 2 errors that
491/// the t = 1 code cannot correct), the frame is left unmodified and
492/// counts toward `uncorrectable_frames` as well as `corrupted_frames`.
493///
494/// `corrupted_frames` still reports the total non-zero-syndrome count
495/// (so callers comparing channel raw-error rates against pre- and
496/// post-correction outputs see the same denominator). Frame lock is
497/// established via the alignment-pattern criterion exactly as in
498/// [`decode_multiframe`]; correction does not change the §5.4.4 lock
499/// search.
500///
501/// Spec rationale: H.261 §5.4.1 explicitly labels the outer layer an
502/// "error correcting code", and §5.4.2 specifies the BCH (511, 493)
503/// generator polynomial that mathematically supports `t = 1`
504/// correction. [`decode_multiframe`] surfaces the syndrome without
505/// acting on it; this function actually applies the correction the
506/// spec sanctions.
507pub fn decode_multiframe_with_correction(framed: &[u8]) -> Option<DecodedMultiframe> {
508    let total_bits = framed.len() * 8;
509    let lock_frames = 3 * MULTIFRAME_FRAMES as usize;
510    let lock_span_bits = lock_frames * FRAME_BITS as usize;
511    if total_bits < lock_span_bits {
512        return None;
513    }
514
515    let read_bit = |pos: usize| -> u8 { (framed[pos / 8] >> (7 - (pos & 7))) & 1 };
516
517    let mut lock: Option<usize> = None;
518    'outer: for bit0 in 0..FRAME_BITS as usize {
519        if bit0 + lock_span_bits > total_bits {
520            break;
521        }
522        for k in 0..lock_frames {
523            let s = read_bit(bit0 + k * FRAME_BITS as usize);
524            if s != ALIGNMENT_PATTERN[k % MULTIFRAME_FRAMES as usize] {
525                continue 'outer;
526            }
527        }
528        lock = Some(bit0);
529        break;
530    }
531
532    let bit0 = lock?;
533
534    let mut data: Vec<u8> = Vec::new();
535    let mut data_bits = 0usize;
536    let mut put_data_bit = |bit: u8| {
537        if data_bits % 8 == 0 {
538            data.push(0);
539        }
540        let byte_idx = data_bits / 8;
541        let shift = 7 - (data_bits & 7);
542        data[byte_idx] |= (bit & 1) << shift;
543        data_bits += 1;
544    };
545
546    let mut frames_consumed = 0usize;
547    let mut corrupted_frames = 0usize;
548    let mut corrected_frames = 0usize;
549    let mut uncorrectable_frames = 0usize;
550    let mut fill_frames = 0usize;
551
552    // 493-bit working buffer for the Fi || data field of each frame.
553    // The codeword positions used by `locate_single_error` are:
554    //   pos 0           → Fi
555    //   pos 1..493      → data bits 0..491
556    //   pos 493..511    → parity bits 0..17
557    let coded_per_frame = (DATA_BITS - 1) as usize; // 492
558
559    let mut cursor = bit0;
560    while cursor + FRAME_BITS as usize <= total_bits {
561        // Skip the S bit (not part of the 511-bit BCH codeword).
562        let _s = read_bit(cursor);
563        let mut fi = read_bit(cursor + 1);
564
565        // Pack Fi || 492 data bits into `scratch` for the syndrome /
566        // correction calculation; bit 0 of `scratch[0]` (MSB-first) is
567        // Fi, bits 1..493 are the data field.
568        let mut scratch = [0u8; 62];
569        scratch[0] = fi << 7;
570        let data_start = cursor + 2;
571        for j in 0..coded_per_frame {
572            let bit = read_bit(data_start + j);
573            let pos = j + 1;
574            scratch[pos / 8] |= bit << (7 - (pos & 7));
575        }
576        let mut par = 0u32;
577        let par_start = data_start + coded_per_frame;
578        for j in 0..(PARITY_BITS as usize) {
579            par = (par << 1) | read_bit(par_start + j) as u32;
580        }
581
582        let synd = syndrome18(&scratch, par);
583        if synd != 0 {
584            corrupted_frames += 1;
585            if let Some(p) = locate_single_error(synd) {
586                let p = p as usize;
587                if p == 0 {
588                    // Error in Fi: flip it.
589                    fi ^= 1;
590                    scratch[0] ^= 0x80;
591                } else if p < (DATA_BITS as usize) {
592                    // Error in data bit (p - 1) of the 492-bit data field.
593                    let bit_in_scratch = p; // pos within Fi || data = p
594                    scratch[bit_in_scratch / 8] ^= 1 << (7 - (bit_in_scratch & 7));
595                } else {
596                    // Error in parity bit (p - 493). The parity isn't
597                    // emitted to `data`, but we flip it for completeness
598                    // so a downstream caller running a verification
599                    // syndrome on the corrected codeword sees zero.
600                    let par_bit = p - DATA_BITS as usize;
601                    par ^= 1 << (PARITY_BITS as usize - 1 - par_bit);
602                }
603                corrected_frames += 1;
604                // Sanity check: the corrected codeword's syndrome is zero.
605                debug_assert_eq!(
606                    syndrome18(&scratch, par),
607                    0,
608                    "post-correction syndrome should be zero (p={p})"
609                );
610            } else {
611                uncorrectable_frames += 1;
612            }
613        }
614
615        if fi == 1 {
616            // Emit the (possibly-corrected) 492 coded-data bits from
617            // `scratch` (positions 1..493).
618            for j in 0..coded_per_frame {
619                let pos = j + 1;
620                let bit = (scratch[pos / 8] >> (7 - (pos & 7))) & 1;
621                put_data_bit(bit);
622            }
623        } else {
624            fill_frames += 1;
625        }
626
627        frames_consumed += 1;
628        cursor += FRAME_BITS as usize;
629    }
630
631    Some(DecodedMultiframe {
632        data,
633        data_bits,
634        frames_consumed,
635        corrupted_frames,
636        fill_frames,
637        corrected_frames,
638        uncorrectable_frames,
639    })
640}
641
642#[cfg(test)]
643mod tests {
644    use super::*;
645
646    /// Sanity-check that the generator polynomial encodes the factored form.
647    ///
648    /// `(x^9 + x^4 + 1)` has bits at positions {9, 4, 0} → `0b10_0001_0001 = 0x211`.
649    /// `(x^9 + x^6 + x^4 + x^3 + 1)` has bits at positions {9, 6, 4, 3, 0} →
650    /// `0b10_0101_1001 = 0x259`. Their GF(2) product must equal `GEN_POLY`.
651    #[test]
652    fn generator_polynomial_factors_match_spec() {
653        let a: u32 = 0b10_0001_0001; // 0x211
654        let b: u32 = 0b10_0101_1001; // 0x259
655        assert_eq!(a, 0x211);
656        assert_eq!(b, 0x259);
657        let mut prod: u32 = 0;
658        for i in 0..16 {
659            if (b >> i) & 1 == 1 {
660                prod ^= a << i;
661            }
662        }
663        assert_eq!(prod, GEN_POLY, "g(x) factors don't match: got 0x{prod:X}");
664    }
665
666    /// A zero input must yield zero parity (the codeword `0…0` is trivially
667    /// divisible by g(x)).
668    #[test]
669    fn parity_of_all_zeros_is_zero() {
670        let zero = [0u8; 62];
671        assert_eq!(parity18(&zero), 0);
672    }
673
674    /// The all-ones 493-bit input is a useful smoke-test: the parity is
675    /// determined by the polynomial alone. We compute it via the encoder
676    /// and verify the matching syndrome cancels out.
677    #[test]
678    fn parity_then_syndrome_cancels_for_all_ones() {
679        let mut ones = [0xFFu8; 62];
680        // Mask off bits beyond position 492 (the high bit of the last
681        // byte should be the last data bit; ceil(493/8) = 62 bytes →
682        // 496 bits total → mask out the trailing 3 bits).
683        ones[61] &= 0b1110_0000;
684        let par = parity18(&ones);
685        assert_eq!(syndrome18(&ones, par), 0);
686    }
687
688    /// Spec §5.4.2 worked example: for the 493-bit input
689    /// `0 followed by 492 ones`, the BCH parity is exactly
690    /// `011011010100011011` (= 0x1B51B). This is the published
691    /// validation vector from ITU-T Rec. H.261 (03/93) §5.4.2.
692    #[test]
693    fn parity_matches_spec_5_4_2_worked_example() {
694        // Build the 493-bit input: bit 0 = 0, bits 1..493 = 1.
695        // Pack into ceil(493/8) = 62 bytes, MSB-first.
696        let mut msg = [0u8; 62];
697        for i in 1..493 {
698            msg[i / 8] |= 1 << (7 - (i & 7));
699        }
700        let par = parity18(&msg);
701        // Expected: 011011010100011011₂ = 0x1B51B.
702        assert_eq!(
703            par, 0x1_B51B,
704            "spec §5.4.2 worked example: parity should be 0x1B51B (binary 011011010100011011), got 0x{par:X}"
705        );
706        // And the syndrome of (msg || par) must be zero.
707        assert_eq!(syndrome18(&msg, par), 0);
708    }
709
710    /// A single-bit flip anywhere in the codeword must produce a non-zero
711    /// syndrome (single-error detection — the t=1 BCH minimum).
712    #[test]
713    fn single_bit_flip_in_data_is_detected() {
714        let mut msg = [0u8; 62];
715        // A bit of structure to make sure parity is non-trivial.
716        msg[3] = 0xA5;
717        msg[20] = 0x3C;
718        msg[55] = 0xF0;
719        let par = parity18(&msg);
720        assert_eq!(syndrome18(&msg, par), 0, "untouched codeword");
721        // Flip bit 17.
722        msg[2] ^= 0b0100_0000;
723        assert_ne!(syndrome18(&msg, par), 0, "single bit flip in data");
724    }
725
726    #[test]
727    fn single_bit_flip_in_parity_is_detected() {
728        let msg = [0u8; 62];
729        let par = parity18(&msg);
730        // Flip one parity bit — say bit 5 — and check the syndrome is
731        // non-zero. (For an all-zero message, par == 0 so flipping bit 5
732        // gives par = 0x20.)
733        assert_eq!(par, 0);
734        assert_ne!(syndrome18(&msg, 0x20), 0);
735    }
736
737    /// Round-trip: encode an arbitrary inner-bitstream payload and verify
738    /// `decode_multiframe` recovers it exactly.
739    ///
740    /// Per §5.4.4, lock requires three complete multiframes (24 bits of
741    /// framing pattern), so we send three multiframes of mostly-fill
742    /// content with a small data payload in the first frame.
743    #[test]
744    fn encode_then_decode_round_trip() {
745        // 50 bytes of pseudo-random payload (400 bits). Less than one
746        // multiframe (8 frames × 492 bits = 3936 bits) so this exercises
747        // the fill-frame path too. We still emit 3 multiframes so the
748        // decoder can lock.
749        let mut payload = [0u8; 50];
750        let mut s: u32 = 0xDEAD_BEEF;
751        for b in payload.iter_mut() {
752            s = s.wrapping_mul(1664525).wrapping_add(1013904223);
753            *b = (s >> 16) as u8;
754        }
755        // Pad the framed output to 3 multiframes by appending two whole
756        // multiframes of stuffing.
757        let mut framed = encode_multiframe(&payload, 50 * 8);
758        framed.extend(encode_multiframe(&[], 0));
759        framed.extend(encode_multiframe(&[], 0));
760        assert_eq!(framed.len(), 3 * 512);
761        let decoded = decode_multiframe(&framed).expect("frame lock");
762        assert_eq!(decoded.corrupted_frames, 0);
763        // Decoded `data_bits` should be 492 (one Fi=1 frame) since
764        // 400 bits < 492.
765        assert_eq!(decoded.data_bits, 492);
766        // The first 400 bits of decoded.data should equal `payload`.
767        for i in 0..(50 * 8) {
768            let want = (payload[i / 8] >> (7 - (i & 7))) & 1;
769            let got = (decoded.data[i / 8] >> (7 - (i & 7))) & 1;
770            assert_eq!(got, want, "bit {i} mismatch");
771        }
772        // 24 total frames consumed, 23 of which are stuffing.
773        assert_eq!(decoded.fill_frames, 23);
774        assert_eq!(decoded.frames_consumed, 24);
775    }
776
777    /// Round-trip three full multiframes of data (3 × 8 × 492 = 11_808
778    /// bits) — every frame should be Fi=1 with no fill.
779    #[test]
780    fn encode_then_decode_full_multiframe() {
781        let mut payload = vec![0u8; 11_808 / 8];
782        for (i, b) in payload.iter_mut().enumerate() {
783            *b = (i as u8).wrapping_mul(31).wrapping_add(7);
784        }
785        let framed = encode_multiframe(&payload, payload.len() * 8);
786        assert_eq!(framed.len(), 3 * 512);
787        let decoded = decode_multiframe(&framed).expect("frame lock");
788        assert_eq!(decoded.corrupted_frames, 0);
789        assert_eq!(decoded.fill_frames, 0);
790        assert_eq!(decoded.frames_consumed, 24);
791        assert_eq!(decoded.data_bits, 11_808);
792        assert_eq!(&decoded.data[..payload.len()], &payload[..]);
793    }
794
795    /// Six whole multiframes round-trip cleanly (twice the lock window).
796    #[test]
797    fn encode_then_decode_two_multiframes() {
798        let mut payload = vec![0u8; (6 * 3936) / 8];
799        for (i, b) in payload.iter_mut().enumerate() {
800            *b = (i as u8 ^ 0x5A).wrapping_mul(13).wrapping_add(1);
801        }
802        let framed = encode_multiframe(&payload, payload.len() * 8);
803        assert_eq!(framed.len(), 6 * 512);
804        let decoded = decode_multiframe(&framed).expect("frame lock");
805        assert_eq!(decoded.frames_consumed, 48);
806        assert_eq!(decoded.fill_frames, 0);
807        assert_eq!(decoded.corrupted_frames, 0);
808        assert_eq!(decoded.data_bits, 6 * 3936);
809        assert_eq!(&decoded.data[..payload.len()], &payload[..]);
810    }
811
812    /// A flipped data bit in a single frame is detected via the syndrome
813    /// and reported in `corrupted_frames`, but lock is preserved and the
814    /// rest of the multiframe is still recovered.
815    #[test]
816    fn data_corruption_surfaces_via_syndrome() {
817        // 3 multiframes so we can lock; corrupt a bit deep inside one of
818        // them. Flip a bit at byte 700 (well inside the second multiframe
819        // at bytes 512..1024).
820        let payload = vec![0xC3u8; (3 * 3936) / 8];
821        let mut framed = encode_multiframe(&payload, payload.len() * 8);
822        assert_eq!(framed.len(), 3 * 512);
823        framed[700] ^= 0b0001_0000;
824        let decoded = decode_multiframe(&framed).expect("still lockable");
825        assert_eq!(decoded.frames_consumed, 24);
826        assert!(
827            decoded.corrupted_frames >= 1,
828            "syndrome should flag at least one corrupted frame"
829        );
830    }
831
832    /// Frame-lock can be obtained when the framed stream is preceded by
833    /// 4 non-zero "pre-roll" bits and a fourth bit of run-up. Per §5.4.4,
834    /// three consecutive complete multiframes are required for lock, so
835    /// we send three multiframes' worth of data and the decoder should
836    /// pick up at the first one.
837    #[test]
838    fn decoder_acquires_lock_with_leading_padding() {
839        // Three multiframes of payload = 3 × 3936 = 11_808 bits = 1476 bytes.
840        let payload = vec![0x42u8; 11_808 / 8];
841        let framed = encode_multiframe(&payload, payload.len() * 8);
842        assert_eq!(framed.len(), 3 * 512);
843        // Prepend 4 "junk" bits (1011) = shift the whole stream right by 4.
844        // The 4 junk bits are at positions 0..4 of `shifted`; bit 4 is the
845        // start of the real frame.
846        let mut shifted = vec![0u8; framed.len() + 1];
847        shifted[0] = 0b1011_0000; // junk in the top 4 bits
848        for i in 0..(framed.len() * 8) {
849            let bit = (framed[i / 8] >> (7 - (i & 7))) & 1;
850            let new_pos = i + 4;
851            shifted[new_pos / 8] |= bit << (7 - (new_pos & 7));
852        }
853        let decoded = decode_multiframe(&shifted).expect("lock with offset");
854        // We should consume all 24 frames (3 multiframes) — possibly minus
855        // one if the trailing 4 bits don't complete a frame.
856        assert!(
857            decoded.frames_consumed >= 23,
858            "should recover at least 23 of 24 frames, got {}",
859            decoded.frames_consumed
860        );
861        assert_eq!(decoded.corrupted_frames, 0);
862    }
863
864    /// An input of pure noise must NOT obtain frame lock (no false
865    /// positives on alignment when no real frame is present).
866    #[test]
867    fn random_noise_does_not_obtain_lock() {
868        // All-ones: every framing-bit position holds a 1, but the
869        // alignment pattern `00011011` requires three 0-bits up front,
870        // so no candidate `bit0` can satisfy the 24-bit lock criterion.
871        // Use 3 multiframes of buffer so the search exhausts everywhere
872        // it would normally try.
873        let buf = vec![0xFFu8; 3 * 512];
874        let decoded = decode_multiframe(&buf);
875        assert!(decoded.is_none(), "all-ones stream must not lock");
876    }
877
878    /// `encode_multiframe` of an empty payload produces one multiframe
879    /// of pure stuffing frames (Fi=0, fill all ones). Concatenating
880    /// three of those is what an idle channel would send, and the
881    /// decoder should lock onto and unframe it.
882    #[test]
883    fn empty_payload_emits_one_stuffing_multiframe() {
884        let mut framed = encode_multiframe(&[], 0);
885        assert_eq!(framed.len(), 512);
886        framed.extend(encode_multiframe(&[], 0));
887        framed.extend(encode_multiframe(&[], 0));
888        let decoded = decode_multiframe(&framed).expect("stuffing locks");
889        assert_eq!(decoded.frames_consumed, 24);
890        assert_eq!(decoded.fill_frames, 24);
891        assert_eq!(decoded.data_bits, 0);
892        assert_eq!(decoded.corrupted_frames, 0);
893    }
894
895    // ---------- §5.4.1 single-bit BCH correction tests ----------
896
897    /// `locate_single_error(0)` must report no error (the empty syndrome
898    /// is reserved for the "codeword unchanged" case).
899    #[test]
900    fn locate_single_error_zero_syndrome_returns_none() {
901        assert_eq!(locate_single_error(0), None);
902    }
903
904    /// `locate_single_error` covers every position `0..511` and only
905    /// those positions: for each position `p`, compute the syndrome of
906    /// the all-zero codeword with bit `p` flipped, then verify that
907    /// `locate_single_error(syndrome) == Some(p)`. This is the
908    /// fundamental invariant of the t = 1 lookup.
909    #[test]
910    fn locate_single_error_round_trips_every_codeword_position() {
911        for p in 0..((FRAME_BITS - 1) as usize) {
912            // Build a 511-bit codeword that is all zeros except for a 1
913            // at position `p` (where p=0 is Fi, p=1..493 is data bit
914            // p-1, p=493..511 is parity bit p-493).
915            let mut scratch = [0u8; 62];
916            let mut par: u32 = 0;
917            if p < (DATA_BITS as usize) {
918                scratch[p / 8] |= 1 << (7 - (p & 7));
919            } else {
920                let pb = p - DATA_BITS as usize;
921                par |= 1 << (PARITY_BITS as usize - 1 - pb);
922            }
923            let s = syndrome18(&scratch, par);
924            assert_ne!(s, 0, "non-zero error must yield non-zero syndrome (p={p})");
925            assert_eq!(
926                locate_single_error(s),
927                Some(p as u32),
928                "syndrome 0x{s:X} should map to position {p}"
929            );
930        }
931    }
932
933    /// For weight-2 error patterns the t = 1 code cannot guarantee
934    /// correction. Some weight-2 syndromes coincidentally equal
935    /// weight-1 syndromes (the code's minimum distance is 3, so two
936    /// errors can never look like zero errors but can look like one
937    /// error). `locate_single_error` is documented to return *some*
938    /// position for those — that's the unavoidable miscorrection
939    /// hazard for a t = 1 code over a busy channel. We assert here
940    /// only that the return is well-defined (no panic, no infinite
941    /// loop) for a representative set of weight-2 patterns.
942    #[test]
943    fn locate_single_error_handles_weight_two_patterns() {
944        for (a, b) in [(0, 5), (3, 200), (100, 492), (1, 510), (0, 510)] {
945            let mut scratch = [0u8; 62];
946            let mut par: u32 = 0;
947            for &p in &[a, b] {
948                if p < (DATA_BITS as usize) {
949                    scratch[p / 8] ^= 1 << (7 - (p & 7));
950                } else {
951                    let pb = p - DATA_BITS as usize;
952                    par ^= 1 << (PARITY_BITS as usize - 1 - pb);
953                }
954            }
955            let s = syndrome18(&scratch, par);
956            // s could be zero only if the two errors map to the same
957            // syndrome — impossible for d = 3, but the loop here just
958            // confirms the function returns without crashing.
959            let _ = locate_single_error(s);
960        }
961    }
962
963    /// `decode_multiframe_with_correction` recovers the original
964    /// payload bit-exact when the framed buffer is clean (the
965    /// correction path is a no-op on zero-syndrome frames).
966    #[test]
967    fn decode_with_correction_no_error_round_trip() {
968        let mut payload = vec![0u8; 11_808 / 8];
969        for (i, b) in payload.iter_mut().enumerate() {
970            *b = (i as u8).wrapping_mul(31).wrapping_add(7);
971        }
972        let framed = encode_multiframe(&payload, payload.len() * 8);
973        let decoded = decode_multiframe_with_correction(&framed).expect("lock on clean stream");
974        assert_eq!(decoded.frames_consumed, 24);
975        assert_eq!(decoded.corrupted_frames, 0);
976        assert_eq!(decoded.corrected_frames, 0);
977        assert_eq!(decoded.uncorrectable_frames, 0);
978        assert_eq!(&decoded.data[..payload.len()], &payload[..]);
979    }
980
981    /// A single-bit flip inside the data field of one frame is
982    /// detected and corrected: the recovered payload matches the
983    /// pre-corruption original bit-exact, and the counts are
984    /// `corrupted_frames = 1, corrected_frames = 1,
985    /// uncorrectable_frames = 0`.
986    #[test]
987    fn decode_with_correction_recovers_single_bit_data_error() {
988        let payload = vec![0xC3u8; (3 * 3936) / 8];
989        let framed_clean = encode_multiframe(&payload, payload.len() * 8);
990        assert_eq!(framed_clean.len(), 3 * 512);
991        // Flip bit 12 of byte 700 (deep in the second multiframe).
992        let mut framed = framed_clean.clone();
993        framed[700] ^= 0b0001_0000;
994        let decoded =
995            decode_multiframe_with_correction(&framed).expect("lock survives single-bit error");
996        assert_eq!(decoded.frames_consumed, 24);
997        assert_eq!(decoded.corrupted_frames, 1);
998        assert_eq!(decoded.corrected_frames, 1);
999        assert_eq!(decoded.uncorrectable_frames, 0);
1000        // The corrected payload bit-exactly matches the original.
1001        assert_eq!(&decoded.data[..payload.len()], &payload[..]);
1002    }
1003
1004    /// A single-bit flip in the parity field is detected and the
1005    /// (data) payload is recovered bit-exact. (The parity bit itself
1006    /// isn't emitted; the corrected codeword is internally consistent
1007    /// but the data field was never wrong.)
1008    #[test]
1009    fn decode_with_correction_recovers_single_bit_parity_error() {
1010        let payload = vec![0x5Au8; (3 * 3936) / 8];
1011        let framed_clean = encode_multiframe(&payload, payload.len() * 8);
1012        let mut framed = framed_clean.clone();
1013        // Frame 0 starts at bit 0; its parity field is at bits
1014        // 494..512. Flip a bit there (byte 62, mid-parity).
1015        framed[62] ^= 0b0000_1000;
1016        let decoded =
1017            decode_multiframe_with_correction(&framed).expect("lock survives parity flip");
1018        assert_eq!(decoded.corrupted_frames, 1);
1019        assert_eq!(decoded.corrected_frames, 1);
1020        assert_eq!(decoded.uncorrectable_frames, 0);
1021        assert_eq!(&decoded.data[..payload.len()], &payload[..]);
1022    }
1023
1024    /// A single-bit flip of the `Fi` flag in a Fi=1 (carrying-data)
1025    /// frame is correctable: the corrected Fi reads back as 1 and the
1026    /// 492 data bits are emitted unchanged. (If we incorrectly took Fi
1027    /// as 0, the frame would be dropped as fill.)
1028    #[test]
1029    fn decode_with_correction_recovers_single_bit_fi_error() {
1030        let payload = vec![0xA5u8; (3 * 3936) / 8];
1031        let framed_clean = encode_multiframe(&payload, payload.len() * 8);
1032        let mut framed = framed_clean.clone();
1033        // Fi for frame 0 is at bit 1 (after the S bit at bit 0). Flip
1034        // it: bit position 1 in the framed stream ⇒ byte 0, mask 0x40.
1035        framed[0] ^= 0b0100_0000;
1036        let decoded = decode_multiframe_with_correction(&framed).expect("lock survives Fi flip");
1037        assert_eq!(decoded.corrupted_frames, 1);
1038        assert_eq!(decoded.corrected_frames, 1);
1039        assert_eq!(decoded.uncorrectable_frames, 0);
1040        // All Fi=1 frames recovered: payload survives byte-exact.
1041        assert_eq!(&decoded.data[..payload.len()], &payload[..]);
1042        // Fill count should still be zero (frame 0 wasn't mis-classified
1043        // as fill).
1044        assert_eq!(decoded.fill_frames, 0);
1045    }
1046
1047    /// Two bit flips inside the same frame's protected region exceed
1048    /// the t = 1 code's correction capability. The decoder must either
1049    /// (a) report the frame as uncorrectable when the weight-2 syndrome
1050    /// doesn't accidentally collide with a weight-1 pattern, or (b)
1051    /// miscorrect to a different codeword (the unavoidable hazard of a
1052    /// t = 1 code over a busy channel). Either way, no panic, no
1053    /// silent dropping.
1054    #[test]
1055    fn decode_with_correction_two_bit_error_does_not_panic() {
1056        let payload = vec![0x33u8; (3 * 3936) / 8];
1057        let framed_clean = encode_multiframe(&payload, payload.len() * 8);
1058        let mut framed = framed_clean.clone();
1059        // Two flips in the same frame (bytes 5 and 25 both within
1060        // frame 0: bytes 0..64). Spaced apart in different bytes so we
1061        // hit two different data positions.
1062        framed[5] ^= 0b0000_0001;
1063        framed[25] ^= 0b0010_0000;
1064        let decoded =
1065            decode_multiframe_with_correction(&framed).expect("frame lock survives weight-2 error");
1066        assert_eq!(decoded.frames_consumed, 24);
1067        assert_eq!(decoded.corrupted_frames, 1);
1068        // The weight-2 syndrome may or may not look like a weight-1
1069        // pattern, so either branch is valid here; the invariant we
1070        // care about is that the function returns and the breakdown
1071        // is internally consistent.
1072        assert_eq!(
1073            decoded.corrected_frames + decoded.uncorrectable_frames,
1074            decoded.corrupted_frames
1075        );
1076    }
1077
1078    /// `decode_multiframe_with_correction` correctly handles the
1079    /// stuffing-frame path: an idle channel (Fi=0 in every frame, 492
1080    /// fill bits all 1s) is detected, recovered as zero payload, and
1081    /// no spurious corrections fire.
1082    #[test]
1083    fn decode_with_correction_stuffing_only_path() {
1084        let mut framed = encode_multiframe(&[], 0);
1085        framed.extend(encode_multiframe(&[], 0));
1086        framed.extend(encode_multiframe(&[], 0));
1087        let decoded = decode_multiframe_with_correction(&framed).expect("stuffing locks");
1088        assert_eq!(decoded.frames_consumed, 24);
1089        assert_eq!(decoded.fill_frames, 24);
1090        assert_eq!(decoded.data_bits, 0);
1091        assert_eq!(decoded.corrupted_frames, 0);
1092        assert_eq!(decoded.corrected_frames, 0);
1093        assert_eq!(decoded.uncorrectable_frames, 0);
1094    }
1095
1096    /// Sweep every protected bit position in a single frame: flip one
1097    /// bit, decode with correction, verify the payload is recovered
1098    /// bit-exact and the position-count invariants hold. This is the
1099    /// stress test that proves every protected bit (Fi + 492 data + 18
1100    /// parity = 511 positions) is correctable end-to-end through the
1101    /// per-frame public API.
1102    #[test]
1103    fn decode_with_correction_sweeps_every_protected_bit() {
1104        // 3 multiframes of payload (the minimum for lock). All Fi=1.
1105        let payload = vec![0x9Cu8; (3 * 3936) / 8];
1106        let framed_clean = encode_multiframe(&payload, payload.len() * 8);
1107
1108        // Sweep every of the 511 protected bit positions within frame 0
1109        // (which occupies bits 0..512 of `framed_clean`). The S bit at
1110        // codeword-bit 0 is unprotected, so we start at codeword-bit 1
1111        // (Fi) and run through codeword-bit 511 (last parity bit).
1112        for protected_pos in 0..((FRAME_BITS - 1) as usize) {
1113            // Map protected_pos (0..511, Fi at 0, parity at 493..511) to
1114            // its bit index in `framed_clean`: skip the S bit (bit 0).
1115            let bit_idx = 1 + protected_pos;
1116            let mut framed = framed_clean.clone();
1117            framed[bit_idx / 8] ^= 1 << (7 - (bit_idx & 7));
1118            let decoded = decode_multiframe_with_correction(&framed)
1119                .unwrap_or_else(|| panic!("lock at pos {protected_pos}"));
1120            assert_eq!(
1121                decoded.corrupted_frames, 1,
1122                "exactly one frame should be flagged (pos {protected_pos})"
1123            );
1124            assert_eq!(
1125                decoded.corrected_frames, 1,
1126                "single-bit error must be corrected (pos {protected_pos})"
1127            );
1128            assert_eq!(decoded.uncorrectable_frames, 0);
1129            assert_eq!(
1130                &decoded.data[..payload.len()],
1131                &payload[..],
1132                "payload should match after correction at pos {protected_pos}"
1133            );
1134        }
1135    }
1136}