use super::Ax25Error;
use super::fcs::{Fcs, SingleBitError, crc16_x25, locate_single_bit_error};
use super::frame::{MIN_FRAME_LEN, UiFrame};
use crate::types::Bit;
pub const FLAG: u8 = 0x7E;
pub const DEFAULT_PREAMBLE_FLAGS: usize = 32;
pub const DEFAULT_TAIL_FLAGS: usize = 2;
pub fn frame_bits(frame: &[u8], preamble_flags: usize, tail_flags: usize) -> FrameBits<'_> {
let fcs = crc16_x25(frame);
FrameBits {
frame,
fcs: fcs.to_le_bytes(),
tail_flags,
state: TxState::Preamble {
flags_left: preamble_flags,
bit: 0,
},
ones: 0,
stuff_pending: false,
}
}
#[derive(Debug, Clone, Copy)]
enum TxState {
Preamble {
flags_left: usize,
bit: u8,
},
Data {
pos: usize,
bit: u8,
},
Tail {
flags_left: usize,
bit: u8,
},
Done,
}
#[derive(Debug, Clone)]
pub struct FrameBits<'a> {
frame: &'a [u8],
fcs: [u8; 2],
tail_flags: usize,
state: TxState,
ones: u8,
stuff_pending: bool,
}
impl FrameBits<'_> {
fn data_byte(&self, pos: usize) -> Option<u8> {
match self.frame.get(pos) {
Some(&b) => Some(b),
None => self.fcs.get(pos.wrapping_sub(self.frame.len())).copied(),
}
}
}
impl Iterator for FrameBits<'_> {
type Item = Bit;
fn next(&mut self) -> Option<Bit> {
loop {
match self.state {
TxState::Preamble { flags_left, bit } => {
if flags_left == 0 {
self.state = TxState::Data { pos: 0, bit: 0 };
continue;
}
let out = Bit::from((FLAG >> bit) & 1 != 0);
self.state = if bit == 7 {
TxState::Preamble {
flags_left: flags_left - 1,
bit: 0,
}
} else {
TxState::Preamble {
flags_left,
bit: bit + 1,
}
};
return Some(out);
}
TxState::Data { pos, bit } => {
if self.stuff_pending {
self.stuff_pending = false;
self.ones = 0;
return Some(Bit::Zero);
}
let Some(byte) = self.data_byte(pos) else {
self.state = TxState::Tail {
flags_left: self.tail_flags,
bit: 0,
};
continue;
};
let out = Bit::from((byte >> bit) & 1 != 0);
match out {
Bit::One => {
self.ones += 1;
if self.ones == 5 {
self.stuff_pending = true;
}
}
Bit::Zero => self.ones = 0,
}
self.state = if bit == 7 {
TxState::Data {
pos: pos + 1,
bit: 0,
}
} else {
TxState::Data { pos, bit: bit + 1 }
};
return Some(out);
}
TxState::Tail { flags_left, bit } => {
if flags_left == 0 {
self.state = TxState::Done;
return None;
}
let out = Bit::from((FLAG >> bit) & 1 != 0);
self.state = if bit == 7 {
TxState::Tail {
flags_left: flags_left - 1,
bit: 0,
}
} else {
TxState::Tail {
flags_left,
bit: bit + 1,
}
};
return Some(out);
}
TxState::Done => return None,
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RecoveryPolicy {
#[default]
None,
SingleBitFlip,
PreDestuffFlip,
}
pub(crate) const RAW_BITS: usize = 4096;
pub(crate) const RAW_BYTES: usize = RAW_BITS / 8;
#[derive(Debug, Clone)]
pub struct HdlcDeframer<const N: usize> {
buf: [u8; N],
len: usize,
nbits: u8,
cur_byte: u8,
ones: u8,
in_frame: bool,
overflowed: bool,
recovery: RecoveryPolicy,
raw: [u8; RAW_BYTES],
raw_len: usize,
#[cfg_attr(not(feature = "tnc"), allow(dead_code))]
last_raw_len: usize,
}
impl<const N: usize> HdlcDeframer<N> {
#[must_use]
pub const fn new() -> Self {
Self {
buf: [0; N],
len: 0,
nbits: 0,
cur_byte: 0,
ones: 0,
in_frame: false,
overflowed: false,
recovery: RecoveryPolicy::None,
raw: [0; RAW_BYTES],
raw_len: 0,
last_raw_len: 0,
}
}
#[must_use]
pub const fn with_recovery(recovery: RecoveryPolicy) -> Self {
let mut d = Self::new();
d.recovery = recovery;
d
}
const fn reset_frame(&mut self) {
self.len = 0;
self.nbits = 0;
self.cur_byte = 0;
self.overflowed = false;
self.raw_len = 0;
}
const fn record_raw(&mut self, bit: Bit) {
if self.raw_len < RAW_BITS {
let byte = self.raw_len / 8;
let mask = 1u8 << (self.raw_len % 8);
match bit {
Bit::One => self.raw[byte] |= mask,
Bit::Zero => self.raw[byte] &= !mask,
}
}
self.raw_len = self.raw_len.saturating_add(1);
}
const fn raw_bit(&self, index: usize) -> bool {
if index >= RAW_BITS {
return false;
}
(self.raw[index / 8] >> (index % 8)) & 1 != 0
}
pub fn push(&mut self, bit: Bit) -> Option<Result<&[u8], Ax25Error>> {
if self.in_frame {
self.record_raw(bit);
}
match bit {
Bit::One => {
self.ones += 1;
if self.ones >= 7 {
self.ones = 7; self.in_frame = false;
self.reset_frame();
return None;
}
if self.in_frame {
self.push_frame_bit(Bit::One);
}
None
}
Bit::Zero => {
let ones = self.ones;
self.ones = 0;
match ones {
5 => {
None
}
6 => {
let was_in_frame = self.in_frame;
self.in_frame = true;
if was_in_frame {
let result = self.close_frame();
self.last_raw_len = self.raw_len;
self.reset_frame();
return match result {
CloseResult::Frame(len) => {
Some(Ok(self.buf.get(..len).unwrap_or(&[])))
}
CloseResult::Error(e) => Some(Err(e)),
CloseResult::Discard => None,
};
}
self.reset_frame();
None
}
_ => {
if self.in_frame {
self.push_frame_bit(Bit::Zero);
}
None
}
}
}
}
}
const fn push_frame_bit(&mut self, bit: Bit) {
if let Bit::One = bit {
self.cur_byte |= 1 << self.nbits;
}
self.nbits += 1;
if self.nbits == 8 {
if self.len < N {
self.buf[self.len] = self.cur_byte;
} else {
self.overflowed = true;
}
self.len = self.len.saturating_add(1);
self.cur_byte = 0;
self.nbits = 0;
}
}
fn close_frame(&mut self) -> CloseResult {
if self.nbits != 7 {
if let RecoveryPolicy::PreDestuffFlip = self.recovery
&& let Some(len) = self.try_predestuff()
{
return CloseResult::Frame(len);
}
return CloseResult::Discard;
}
if self.overflowed || self.len > N {
return CloseResult::Error(Ax25Error::FrameTooLarge {
len: self.len,
max: N,
});
}
if self.len < MIN_FRAME_LEN + 2 {
return CloseResult::Discard;
}
let Some(bytes) = self.buf.get(..self.len) else {
return CloseResult::Discard;
};
let content_len = self.len - 2;
let (content, fcs_bytes) = bytes.split_at(content_len);
let expected = u16::from_le_bytes([
fcs_bytes.first().copied().unwrap_or(0),
fcs_bytes.get(1).copied().unwrap_or(0),
]);
let mut fcs = Fcs::new();
fcs.update_slice(content);
let computed = fcs.finish();
if computed == expected {
CloseResult::Frame(content_len)
} else {
match self.recovery {
RecoveryPolicy::None => {
CloseResult::Error(Ax25Error::FcsMismatch { expected, computed })
}
RecoveryPolicy::SingleBitFlip => self.try_repair(content_len, expected, computed),
RecoveryPolicy::PreDestuffFlip => {
match self.try_repair(content_len, expected, computed) {
CloseResult::Frame(len) => CloseResult::Frame(len),
other => match self.try_predestuff() {
Some(len) => CloseResult::Frame(len),
None => other,
},
}
}
}
}
}
fn try_repair(&mut self, content_len: usize, expected: u16, computed: u16) -> CloseResult {
let mismatch = CloseResult::Error(Ax25Error::FcsMismatch { expected, computed });
let Some(location) = locate_single_bit_error(content_len, expected, computed) else {
return mismatch;
};
match location {
SingleBitError::InFcs => {
mismatch
}
SingleBitError::InContent { index, mask } => {
let Some(byte) = self.buf.get_mut(index) else {
return mismatch;
};
*byte ^= mask;
match self.buf.get(..content_len) {
Some(content) if UiFrame::parse(content).is_ok() => {
CloseResult::Frame(content_len)
}
_ => {
if let Some(byte) = self.buf.get_mut(index) {
*byte ^= mask;
}
mismatch
}
}
}
}
}
fn try_predestuff(&mut self) -> Option<usize> {
let total = self.raw_len;
if !(8..=RAW_BITS).contains(&total) {
return None;
}
let content_bits = total - 8;
if content_bits < (MIN_FRAME_LEN + 2) * 8 {
return None;
}
let mut candidate = [0u8; N];
for flip in 0..content_bits {
let Some(len) = self.destuff_candidate(flip, content_bits, &mut candidate) else {
continue;
};
if len < MIN_FRAME_LEN + 2 || len > N {
continue;
}
let Some(bytes) = candidate.get(..len) else {
continue;
};
let content_len = len - 2;
let (content, fcs_bytes) = bytes.split_at(content_len);
let expected = u16::from_le_bytes([
fcs_bytes.first().copied().unwrap_or(0),
fcs_bytes.get(1).copied().unwrap_or(0),
]);
if crc16_x25(content) != expected || UiFrame::parse(content).is_err() {
continue;
}
if self.len >= 2
&& content_len == self.len - 2
&& matches!(self.buf.get(..content_len), Some(orig) if orig == content)
{
continue;
}
for (dst, src) in self.buf.iter_mut().zip(content.iter()) {
*dst = *src;
}
return Some(content_len);
}
None
}
#[cfg(feature = "tnc")]
pub(crate) fn failed_window(&self) -> Option<(&[u8; RAW_BYTES], usize)> {
let total = self.last_raw_len;
if !(8..=RAW_BITS).contains(&total) {
return None;
}
Some((&self.raw, total))
}
#[cfg(feature = "tnc")]
pub(crate) fn try_voted_window(
&mut self,
bits: &[u8; RAW_BYTES],
total: usize,
) -> Option<usize> {
if !(8..=RAW_BITS).contains(&total) {
return None;
}
let content_bits = total - 8;
if content_bits < (MIN_FRAME_LEN + 2) * 8 {
return None;
}
self.raw = *bits;
self.raw_len = total;
let found = self.check_voted(content_bits);
self.raw_len = 0;
self.len = 0;
found
}
#[cfg(feature = "tnc")]
fn check_voted(&mut self, content_bits: usize) -> Option<usize> {
let mut candidate = [0u8; N];
if let Some(len) = self.destuff_candidate(content_bits, content_bits, &mut candidate)
&& (MIN_FRAME_LEN + 2..=N).contains(&len)
&& let Some(bytes) = candidate.get(..len)
{
let content_len = len - 2;
let (content, fcs_bytes) = bytes.split_at(content_len);
let expected = u16::from_le_bytes([
fcs_bytes.first().copied().unwrap_or(0),
fcs_bytes.get(1).copied().unwrap_or(0),
]);
if crc16_x25(content) == expected && UiFrame::parse(content).is_ok() {
for (dst, src) in self.buf.iter_mut().zip(content.iter()) {
*dst = *src;
}
return Some(content_len);
}
for (dst, src) in self.buf.iter_mut().zip(bytes.iter()) {
*dst = *src;
}
self.len = len;
}
self.try_predestuff()
}
#[cfg(feature = "tnc")]
pub(crate) fn frame_bytes(&self, len: usize) -> &[u8] {
self.buf.get(..len).unwrap_or(&[])
}
fn destuff_candidate(
&self,
flip: usize,
content_bits: usize,
out: &mut [u8; N],
) -> Option<usize> {
let mut ones = 0u8;
let mut nbits = 0u8;
let mut cur = 0u8;
let mut len = 0usize;
for i in 0..content_bits {
let bit = self.raw_bit(i) != (i == flip);
if bit {
ones += 1;
if ones >= 7 {
return None; }
} else {
let run = ones;
ones = 0;
if run == 5 {
continue; }
if run == 6 {
return None; }
}
cur |= u8::from(bit) << nbits;
nbits += 1;
if nbits == 8 {
if len >= N {
return None;
}
out[len] = cur;
len += 1;
cur = 0;
nbits = 0;
}
}
if nbits != 0 || ones >= 5 {
return None;
}
Some(len)
}
}
impl<const N: usize> Default for HdlcDeframer<N> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy)]
enum CloseResult {
Frame(usize),
Error(Ax25Error),
Discard,
}
#[cfg(test)]
mod tests {
extern crate std;
use std::vec::Vec;
use super::*;
fn stuffed_bits(bytes: &[u8]) -> Vec<Bit> {
let mut ones = 0u32;
let mut out = Vec::new();
for &b in bytes {
for i in 0..8 {
let bit = Bit::from((b >> i) & 1 != 0);
out.push(bit);
if bit == Bit::One {
ones += 1;
if ones == 5 {
out.push(Bit::Zero);
ones = 0;
}
} else {
ones = 0;
}
}
}
out
}
fn bits_of(iter: FrameBits<'_>) -> Vec<Bit> {
iter.collect()
}
fn flag_bits(n: usize) -> Vec<Bit> {
let mut out = Vec::new();
for _ in 0..n {
for i in 0..8 {
out.push(Bit::from((FLAG >> i) & 1 != 0));
}
}
out
}
fn deframe_all<const N: usize>(bits: &[Bit]) -> Vec<Result<Vec<u8>, Ax25Error>> {
let mut d = HdlcDeframer::<N>::new();
let mut out = Vec::new();
for &b in bits {
if let Some(r) = d.push(b) {
out.push(r.map(<[u8]>::to_vec));
}
}
out
}
fn min_payload() -> Vec<u8> {
(0u8..16).collect()
}
#[test]
fn frame_bits_layout_matches_reference_stuffing() {
let payload = min_payload();
let bits = bits_of(frame_bits(&payload, 3, 2));
let mut expected = flag_bits(3);
let fcs = crc16_x25(&payload);
let mut with_fcs = payload.clone();
with_fcs.extend_from_slice(&fcs.to_le_bytes());
expected.extend(stuffed_bits(&with_fcs));
expected.extend(flag_bits(2));
assert_eq!(bits, expected);
}
#[test]
fn stuffing_after_exactly_five_ones() {
let bits = bits_of(frame_bits(&[0x1F], 0, 0));
let head: Vec<Bit> = bits.iter().copied().take(9).collect();
assert_eq!(
head,
[
Bit::One,
Bit::One,
Bit::One,
Bit::One,
Bit::One,
Bit::Zero, Bit::Zero,
Bit::Zero,
Bit::Zero,
]
);
}
#[test]
fn five_ones_at_end_of_data_still_stuffed() {
let bits = bits_of(frame_bits(&[0xF8], 0, 0));
let head: Vec<Bit> = bits.iter().copied().take(9).collect();
assert_eq!(
head,
[
Bit::Zero,
Bit::Zero,
Bit::Zero,
Bit::One,
Bit::One,
Bit::One,
Bit::One,
Bit::One,
Bit::Zero, ]
);
}
#[test]
fn payload_flag_byte_is_stuffed_away() {
let payload = [0x7E, 0x7E, 0xFF, 0x7E];
let bits = bits_of(frame_bits(&payload, 1, 1));
let interior = &bits[8..bits.len() - 8];
let flag: Vec<Bit> = flag_bits(1);
assert!(
!interior.windows(8).any(|w| w == flag.as_slice()),
"flag pattern leaked into stuffed data"
);
}
#[test]
fn roundtrip_through_deframer() {
let payload = min_payload();
let bits = bits_of(frame_bits(&payload, 4, 2));
let frames = deframe_all::<64>(&bits);
assert_eq!(frames, [Ok(payload)]);
}
#[test]
fn roundtrip_stuffing_heavy_payload() {
let mut payload = min_payload();
payload.extend_from_slice(&[0xFF, 0xFF, 0x7E, 0xFF, 0x1F, 0xF8, 0x00]);
let bits = bits_of(frame_bits(&payload, 2, 2));
let frames = deframe_all::<64>(&bits);
assert_eq!(frames, [Ok(payload)]);
}
#[test]
fn back_to_back_frames_share_a_flag() {
let a = min_payload();
let mut b = min_payload();
b.push(0xA5);
let mut bits = bits_of(frame_bits(&a, 2, 0));
bits.extend(flag_bits(1));
bits.extend(bits_of(frame_bits(&b, 0, 1)));
let frames = deframe_all::<64>(&bits);
assert_eq!(frames, [Ok(a), Ok(b)]);
}
#[test]
fn corrupted_fcs_is_reported() {
let payload = min_payload();
let mut bits = bits_of(frame_bits(&payload, 2, 1));
let idx = 2 * 8 + 3;
bits[idx] = match bits[idx] {
Bit::Zero => Bit::One,
Bit::One => Bit::Zero,
};
let frames = deframe_all::<64>(&bits);
assert_eq!(frames.len(), 1);
assert!(matches!(frames[0], Err(Ax25Error::FcsMismatch { .. })));
}
#[test]
fn oversize_frame_is_reported() {
let payload: Vec<u8> = (0..40).map(|i| i as u8).collect();
let bits = bits_of(frame_bits(&payload, 1, 1));
let frames = deframe_all::<24>(&bits);
assert_eq!(frames, [Err(Ax25Error::FrameTooLarge { len: 42, max: 24 })]);
}
#[test]
fn runt_frames_discarded_silently() {
let payload = [1u8, 2, 3, 4];
let bits = bits_of(frame_bits(&payload, 2, 2));
assert_eq!(deframe_all::<64>(&bits), []);
}
#[test]
fn garbage_bits_never_panic_and_yield_nothing_valid() {
let mut state = 0x0BAD_5EED_u32 | 1;
let mut d = HdlcDeframer::<32>::new();
for _ in 0..100_000 {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
let bit = Bit::from(state & 1 != 0);
if let Some(Ok(frame)) = d.push(bit) {
assert!(frame.len() >= MIN_FRAME_LEN);
}
}
let payload = min_payload();
let bits = bits_of(frame_bits(&payload, 8, 2));
let mut got = Vec::new();
for b in bits {
if let Some(Ok(frame)) = d.push(b) {
got.push(frame.to_vec());
}
}
assert_eq!(got, [payload]);
}
#[test]
fn abort_sequence_discards_frame_in_progress() {
let payload = min_payload();
let bits = bits_of(frame_bits(&payload, 2, 0));
let mut d = HdlcDeframer::<64>::new();
for &b in &bits[..bits.len() - 4] {
assert!(d.push(b).is_none());
}
for _ in 0..10 {
assert!(d.push(Bit::One).is_none());
}
let fresh = bits_of(frame_bits(&payload, 2, 1));
let mut got = Vec::new();
for b in fresh {
if let Some(r) = d.push(b) {
got.push(r.map(<[u8]>::to_vec));
}
}
assert_eq!(got, [Ok(payload)]);
}
#[test]
fn defaults_documented_values() {
assert_eq!(DEFAULT_PREAMBLE_FLAGS, 32);
assert_eq!(DEFAULT_TAIL_FLAGS, 2);
assert_eq!(FLAG, 0x7E);
assert_eq!(RecoveryPolicy::default(), RecoveryPolicy::None);
}
fn ui_frame_body() -> Vec<u8> {
use crate::ax25::Address;
let dest = Address::new(b"APRS", 0).unwrap();
let src = Address::new(b"N0CALL", 7).unwrap();
let frame = UiFrame::new(dest, src, b"!4903.50N/07201.75W-test");
let mut buf = [0u8; 64];
let len = frame.build(&mut buf).unwrap();
buf[..len].to_vec()
}
fn deframe_with_recovery<const N: usize>(
bits: &[Bit],
recovery: RecoveryPolicy,
) -> Vec<Result<Vec<u8>, Ax25Error>> {
let mut d = HdlcDeframer::<N>::with_recovery(recovery);
let mut out = Vec::new();
for &b in bits {
if let Some(r) = d.push(b) {
out.push(r.map(<[u8]>::to_vec));
}
}
out
}
fn corrupted_bits(body: &[u8], flips: &[(usize, u8)]) -> Vec<Bit> {
let fcs = crc16_x25(body);
let mut raw = body.to_vec();
raw.extend_from_slice(&fcs.to_le_bytes());
for &(i, mask) in flips {
raw[i] ^= mask;
}
let mut bits = flag_bits(2);
bits.extend(stuffed_bits(&raw));
bits.extend(flag_bits(2));
bits
}
#[test]
fn single_bit_content_corruption_is_repaired() {
let body = ui_frame_body();
let bits = corrupted_bits(&body, &[(18, 0x10)]);
let frames = deframe_with_recovery::<64>(&bits, RecoveryPolicy::SingleBitFlip);
assert_eq!(frames, [Ok(body)]);
}
#[test]
fn single_bit_fcs_field_corruption_is_rejected() {
let body = ui_frame_body();
let bits = corrupted_bits(&body, &[(body.len() + 1, 0x04)]);
let frames = deframe_with_recovery::<64>(&bits, RecoveryPolicy::SingleBitFlip);
assert_eq!(frames.len(), 1);
assert!(matches!(frames[0], Err(Ax25Error::FcsMismatch { .. })));
}
#[test]
fn every_single_bit_position_is_repaired() {
let body = ui_frame_body();
for i in 0..body.len() {
for k in 0..8 {
let bits = corrupted_bits(&body, &[(i, 1 << k)]);
let frames = deframe_with_recovery::<64>(&bits, RecoveryPolicy::SingleBitFlip);
assert_eq!(frames, [Ok(body.clone())], "byte {i} bit {k}");
}
}
}
#[test]
fn two_bit_corruption_is_not_accepted() {
let body = ui_frame_body();
let bits = corrupted_bits(&body, &[(3, 0x02), (20, 0x40)]);
let frames = deframe_with_recovery::<64>(&bits, RecoveryPolicy::SingleBitFlip);
assert_eq!(frames.len(), 1);
assert!(
matches!(frames[0], Err(Ax25Error::FcsMismatch { .. })),
"two-bit corruption must be rejected, got {:?}",
frames[0]
);
}
#[test]
fn policy_none_rejects_single_bit_corruption() {
let body = ui_frame_body();
let bits = corrupted_bits(&body, &[(18, 0x10)]);
let frames = deframe_with_recovery::<64>(&bits, RecoveryPolicy::None);
assert_eq!(frames.len(), 1);
assert!(matches!(frames[0], Err(Ax25Error::FcsMismatch { .. })));
}
#[test]
fn clean_frames_unchanged_under_recovery() {
let body = ui_frame_body();
let bits = corrupted_bits(&body, &[]);
let frames = deframe_with_recovery::<64>(&bits, RecoveryPolicy::SingleBitFlip);
assert_eq!(frames, [Ok(body)]);
}
fn stuffy_ui_frame_body() -> Vec<u8> {
use crate::ax25::Address;
let dest = Address::new(b"APRS", 0).unwrap();
let src = Address::new(b"N0CALL", 7).unwrap();
let frame = UiFrame::new(dest, src, b"!test\xff\xff\x1f\xf8data");
let mut buf = [0u8; 64];
let len = frame.build(&mut buf).unwrap();
buf[..len].to_vec()
}
#[test]
fn predestuff_repairs_corrupted_stuffing_run() {
let body = stuffy_ui_frame_body();
let fcs = crc16_x25(&body);
let mut raw = body.clone();
raw.extend_from_slice(&fcs.to_le_bytes());
let data = stuffed_bits(&raw);
let mut ones = 0usize;
let mut stuffed_at = None;
for (i, &b) in data.iter().enumerate() {
if b == Bit::One {
ones += 1;
if ones == 5 {
stuffed_at = Some(i + 1);
break;
}
} else {
ones = 0;
}
}
let stuffed_at = stuffed_at.expect("payload must force stuffing");
let mut bits = flag_bits(2);
bits.extend(data);
bits.extend(flag_bits(2));
let idx = 16 + stuffed_at - 1;
assert_eq!(bits[idx], Bit::One);
bits[idx] = Bit::Zero;
let frames = deframe_with_recovery::<64>(&bits, RecoveryPolicy::SingleBitFlip);
assert!(
!frames.contains(&Ok(body.clone())),
"syndrome repair unexpectedly fixed a stuffing error"
);
let frames = deframe_with_recovery::<64>(&bits, RecoveryPolicy::PreDestuffFlip);
assert!(
frames.contains(&Ok(body)),
"pre-destuff repair failed: {frames:?}"
);
}
#[test]
fn predestuff_clean_frames_unchanged() {
let body = ui_frame_body();
let bits = corrupted_bits(&body, &[]);
let frames = deframe_with_recovery::<64>(&bits, RecoveryPolicy::PreDestuffFlip);
assert_eq!(frames, [Ok(body)]);
}
#[test]
fn predestuff_rejects_fcs_field_flip() {
let body = ui_frame_body();
let bits = corrupted_bits(&body, &[(body.len() + 1, 0x04)]);
let frames = deframe_with_recovery::<64>(&bits, RecoveryPolicy::PreDestuffFlip);
assert_eq!(frames.len(), 1);
assert!(matches!(frames[0], Err(Ax25Error::FcsMismatch { .. })));
}
#[test]
fn predestuff_repairs_single_content_bit_too() {
let body = ui_frame_body();
let bits = corrupted_bits(&body, &[(18, 0x10)]);
let frames = deframe_with_recovery::<64>(&bits, RecoveryPolicy::PreDestuffFlip);
assert_eq!(frames, [Ok(body)]);
}
}