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}