use crc::{CRC_16_IBM_3740, Crc};
use super::puncture::Mode;
pub const HEADER_BYTES: usize = 4;
pub const INFO_BYTES_PER_BLOCK: usize = 12;
pub const MAX_BLOCKS_PER_FRAME: usize = 32;
pub const MAX_PAYLOAD_BYTES: usize = MAX_BLOCKS_PER_FRAME * INFO_BYTES_PER_BLOCK - HEADER_BYTES;
const CRC16_ALGO: Crc<u16> = Crc::<u16>::new(&CRC_16_IBM_3740);
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct FrameHeader {
pub mode: Mode,
pub block_count: u8,
pub app_type: u8,
pub sequence: u8,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum PackError {
InvalidBlockCount(u8),
InvalidAppType(u8),
InvalidSequence(u8),
PayloadTooLarge(usize),
}
pub fn pack(header: &FrameHeader, payload: &[u8]) -> Result<Vec<u8>, PackError> {
pack_to_size(header, payload, HEADER_BYTES + payload.len())
}
pub fn pack_to_size(
header: &FrameHeader,
payload: &[u8],
target_size: usize,
) -> Result<Vec<u8>, PackError> {
if !(1..=32).contains(&header.block_count) {
return Err(PackError::InvalidBlockCount(header.block_count));
}
if header.app_type > 15 {
return Err(PackError::InvalidAppType(header.app_type));
}
if header.sequence > 31 {
return Err(PackError::InvalidSequence(header.sequence));
}
if payload.len() > MAX_PAYLOAD_BYTES {
return Err(PackError::PayloadTooLarge(payload.len()));
}
if target_size < HEADER_BYTES + payload.len() {
return Err(PackError::PayloadTooLarge(payload.len()));
}
let mode_bits = u16::from(header.mode.header_code()) & 0x3;
let blocks_bits = u16::from(header.block_count - 1) & 0x1F;
let app_bits = u16::from(header.app_type) & 0xF;
let seq_bits = u16::from(header.sequence) & 0x1F;
let header_word: u16 = (mode_bits << 14) | (blocks_bits << 9) | (app_bits << 5) | seq_bits;
let header_be = header_word.to_be_bytes();
let mut out = vec![0u8; target_size];
out[0..2].copy_from_slice(&header_be);
out[HEADER_BYTES..HEADER_BYTES + payload.len()].copy_from_slice(payload);
let mut crc_input = Vec::with_capacity(2 + (target_size - HEADER_BYTES));
crc_input.extend_from_slice(&out[0..2]);
crc_input.extend_from_slice(&out[HEADER_BYTES..target_size]);
let crc = crc16(&crc_input);
out[2..4].copy_from_slice(&crc.to_be_bytes());
Ok(out)
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum UnpackError {
Truncated,
CrcMismatch { expected: u16, computed: u16 },
UnknownMode(u8),
}
pub fn unpack(bytes: &[u8]) -> Result<(FrameHeader, &[u8]), UnpackError> {
if bytes.len() < HEADER_BYTES {
return Err(UnpackError::Truncated);
}
let header_word = u16::from_be_bytes([bytes[0], bytes[1]]);
let crc_recv = u16::from_be_bytes([bytes[2], bytes[3]]);
let payload = &bytes[HEADER_BYTES..];
let mut crc_input = Vec::with_capacity(2 + payload.len());
crc_input.extend_from_slice(&bytes[..2]);
crc_input.extend_from_slice(payload);
let crc_calc = crc16(&crc_input);
if crc_calc != crc_recv {
return Err(UnpackError::CrcMismatch {
expected: crc_recv,
computed: crc_calc,
});
}
let mode_code = (header_word >> 14) as u8;
let blocks_code = ((header_word >> 9) & 0x1F) as u8;
let app_type = ((header_word >> 5) & 0x0F) as u8;
let sequence = (header_word & 0x1F) as u8;
let mode = Mode::from_header_code(mode_code).ok_or(UnpackError::UnknownMode(mode_code))?;
let block_count = blocks_code + 1;
Ok((
FrameHeader {
mode,
block_count,
app_type,
sequence,
},
payload,
))
}
pub fn crc16(bytes: &[u8]) -> u16 {
CRC16_ALGO.checksum(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_header() -> FrameHeader {
FrameHeader {
mode: Mode::Robust,
block_count: 5,
app_type: 1,
sequence: 7,
}
}
#[test]
fn pack_unpack_roundtrip_empty_payload() {
let h = sample_header();
let bytes = pack(&h, &[]).unwrap();
assert_eq!(bytes.len(), HEADER_BYTES);
let (h2, p2) = unpack(&bytes).unwrap();
assert_eq!(h, h2);
assert!(p2.is_empty());
}
#[test]
fn pack_unpack_roundtrip_with_payload() {
let h = sample_header();
let payload: Vec<u8> = (0..60).collect();
let bytes = pack(&h, &payload).unwrap();
assert_eq!(bytes.len(), HEADER_BYTES + 60);
let (h2, p2) = unpack(&bytes).unwrap();
assert_eq!(h, h2);
assert_eq!(p2, &payload[..]);
}
#[test]
fn pack_unpack_roundtrip_all_modes() {
for mode in [Mode::Robust, Mode::Standard, Mode::Fast, Mode::Express] {
let h = FrameHeader {
mode,
block_count: 1,
app_type: 0,
sequence: 0,
};
let bytes = pack(&h, b"hi").unwrap();
let (h2, p2) = unpack(&bytes).unwrap();
assert_eq!(h, h2);
assert_eq!(p2, b"hi");
}
}
#[test]
fn pack_unpack_boundary_field_values() {
let h = FrameHeader {
mode: Mode::Express,
block_count: 32,
app_type: 15,
sequence: 31,
};
let bytes = pack(&h, b"x").unwrap();
let (h2, _) = unpack(&bytes).unwrap();
assert_eq!(h, h2);
let h = FrameHeader {
mode: Mode::Robust,
block_count: 1,
app_type: 0,
sequence: 0,
};
let bytes = pack(&h, &[]).unwrap();
let (h2, _) = unpack(&bytes).unwrap();
assert_eq!(h, h2);
}
#[test]
fn pack_rejects_invalid_field_values() {
let invalid = [
(
FrameHeader {
mode: Mode::Robust,
block_count: 0,
app_type: 0,
sequence: 0,
},
PackError::InvalidBlockCount(0),
),
(
FrameHeader {
mode: Mode::Robust,
block_count: 33,
app_type: 0,
sequence: 0,
},
PackError::InvalidBlockCount(33),
),
(
FrameHeader {
mode: Mode::Robust,
block_count: 1,
app_type: 16,
sequence: 0,
},
PackError::InvalidAppType(16),
),
(
FrameHeader {
mode: Mode::Robust,
block_count: 1,
app_type: 0,
sequence: 32,
},
PackError::InvalidSequence(32),
),
];
for (h, want) in invalid {
assert_eq!(pack(&h, &[]).unwrap_err(), want);
}
}
#[test]
fn pack_rejects_oversize_payload() {
let h = sample_header();
let big = vec![0u8; MAX_PAYLOAD_BYTES + 1];
assert_eq!(
pack(&h, &big).unwrap_err(),
PackError::PayloadTooLarge(MAX_PAYLOAD_BYTES + 1),
);
}
#[test]
fn unpack_detects_header_bit_flip() {
let h = sample_header();
let mut bytes = pack(&h, b"hello").unwrap();
bytes[0] ^= 0x40;
match unpack(&bytes) {
Err(UnpackError::CrcMismatch { .. }) => {}
other => panic!("expected CrcMismatch, got {other:?}"),
}
}
#[test]
fn unpack_detects_payload_bit_flip() {
let h = sample_header();
let mut bytes = pack(&h, b"hello world").unwrap();
bytes[HEADER_BYTES + 3] ^= 0x01;
match unpack(&bytes) {
Err(UnpackError::CrcMismatch { .. }) => {}
other => panic!("expected CrcMismatch, got {other:?}"),
}
}
#[test]
fn unpack_detects_crc_bit_flip() {
let h = sample_header();
let mut bytes = pack(&h, b"hello").unwrap();
bytes[2] ^= 0x10;
match unpack(&bytes) {
Err(UnpackError::CrcMismatch { .. }) => {}
other => panic!("expected CrcMismatch, got {other:?}"),
}
}
#[test]
fn unpack_rejects_truncated_input() {
for n in 0..HEADER_BYTES {
let bytes = vec![0u8; n];
assert_eq!(unpack(&bytes).unwrap_err(), UnpackError::Truncated);
}
}
#[test]
fn crc16_matches_published_check_value() {
assert_eq!(crc16(b"123456789"), 0x29B1);
}
#[test]
fn header_bit_layout_is_msb_first() {
let h = FrameHeader {
mode: Mode::Fast, block_count: 1, app_type: 0b1010, sequence: 0b10101, };
let bytes = pack(&h, &[]).unwrap();
assert_eq!(bytes[0], 0x81);
assert_eq!(bytes[1], 0x55);
}
#[test]
fn capacity_constants_are_consistent() {
assert_eq!(MAX_BLOCKS_PER_FRAME, 32);
assert_eq!(INFO_BYTES_PER_BLOCK, 12);
assert_eq!(MAX_PAYLOAD_BYTES, 32 * 12 - 4); }
}