use core::fmt;
use crate::ax25::{Address, Ax25Error, UiFrame};
use crate::rs::{RsCodec, RsError, RsParity};
use crate::types::Bit;
pub const SYNC_WORD: u32 = 0xF1_5E48;
pub const SYNC_BYTES: [u8; 3] = [0xF1, 0x5E, 0x48];
pub const SYNC_LEN: usize = 3;
pub const SYNC_TOLERANCE: u32 = 1;
pub const PREAMBLE_BYTE: u8 = 0x55;
pub const HEADER_LEN: usize = 13;
pub const HEADER_PARITY_LEN: usize = 2;
pub const PAYLOAD_MAX: usize = 1023;
pub const MAX_BLOCK_DATA: usize = 239;
pub const MAX_BASELINE_BLOCK_DATA: usize = 247;
pub const SCRAMBLER_SEED: u16 = 0x1FF;
pub const ENCODED_MAX: usize = SYNC_LEN + HEADER_LEN + HEADER_PARITY_LEN + PAYLOAD_MAX + 5 * 16;
pub const RX_FRAME_MAX: usize = ENCODED_MAX - SYNC_LEN;
pub const PID_TABLE: [(u8, u8); 10] = [
(0x2, 0x20), (0x3, 0x01), (0x4, 0x06), (0x5, 0x07), (0x6, 0x08), (0xB, 0xCC), (0xC, 0xCD), (0xD, 0xCE), (0xE, 0xCF), (0xF, 0xF0), ];
pub const PID_CODE_NO_LAYER3: u8 = 0xF;
pub const CONTROL_UI_OPCODE: u8 = 0b010_1000;
pub const CONTROL_UI_COMMAND: u8 = 0b010_1100;
pub const CONTROL_UI_OPCODE_MASK: u8 = 0b011_1000;
const LFSR_STAGES: u16 = 9;
const LFSR_MASK: u16 = (1 << LFSR_STAGES) - 1;
const LFSR_TAP_A: u16 = 4;
const LFSR_TAP_B: u16 = 9;
const fn lfsr_taps(state: u16) -> u16 {
((state >> (LFSR_TAP_A - 1)) ^ (state >> (LFSR_TAP_B - 1))) & 1
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Il2pScrambler {
state: u16,
}
impl Il2pScrambler {
#[must_use]
pub const fn new() -> Self {
Self {
state: SCRAMBLER_SEED & LFSR_MASK,
}
}
pub const fn scramble(&mut self, bytes: &mut [u8]) {
let mut i = 0;
while i < bytes.len() {
let mut byte = bytes[i];
let mut k = 8;
while k > 0 {
k -= 1;
let bit = (byte >> k) & 1;
let out = (bit as u16 ^ lfsr_taps(self.state)) & 1;
self.state = ((self.state << 1) | out) & LFSR_MASK;
byte = (byte & !(1 << k)) | ((out as u8) << k);
}
bytes[i] = byte;
i += 1;
}
}
pub const fn descramble(&mut self, bytes: &mut [u8]) {
let mut i = 0;
while i < bytes.len() {
let mut byte = bytes[i];
let mut k = 8;
while k > 0 {
k -= 1;
let bit = (byte >> k) & 1;
let out = (bit as u16 ^ lfsr_taps(self.state)) & 1;
self.state = ((self.state << 1) | bit as u16) & LFSR_MASK;
byte = (byte & !(1 << k)) | ((out as u8) << k);
}
bytes[i] = byte;
i += 1;
}
}
}
impl Default for Il2pScrambler {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Il2pParity {
Two,
Four,
Six,
Eight,
Sixteen,
}
impl Il2pParity {
pub const ALL: [Self; 5] = [Self::Two, Self::Four, Self::Six, Self::Eight, Self::Sixteen];
#[must_use]
pub const fn baseline_for_block(size: usize) -> Self {
if size <= 61 {
Self::Two
} else if size <= 123 {
Self::Four
} else if size <= 185 {
Self::Six
} else {
Self::Eight
}
}
#[must_use]
pub const fn is_max_fec(self) -> bool {
matches!(self, Self::Sixteen)
}
#[must_use]
pub const fn len(self) -> usize {
match self {
Self::Two => 2,
Self::Four => 4,
Self::Six => 6,
Self::Eight => 8,
Self::Sixteen => 16,
}
}
#[must_use]
pub const fn is_empty(self) -> bool {
false
}
#[must_use]
pub const fn correctable(self) -> usize {
self.len() / 2
}
const fn rs(self) -> RsParity {
match self {
Self::Two => RsParity::Two,
Self::Four => RsParity::Four,
Self::Six => RsParity::Six,
Self::Eight => RsParity::Eight,
Self::Sixteen => RsParity::Sixteen,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Il2pError {
PayloadTooLong {
got: usize,
max: usize,
},
BufferTooSmall {
needed: usize,
got: usize,
},
FrameTooShort {
got: usize,
needed: usize,
},
HeaderUncorrectable,
BlockUncorrectable {
block: usize,
},
UnsupportedPid {
got: u8,
},
UnsupportedControl {
got: u8,
},
Ax25(Ax25Error),
Rs(RsError),
}
impl fmt::Display for Il2pError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Il2pError::PayloadTooLong { got, max } => {
write!(f, "payload of {got} bytes exceeds IL2P capacity {max}")
}
Il2pError::BufferTooSmall { needed, got } => {
write!(f, "buffer of {got} bytes, need {needed}")
}
Il2pError::FrameTooShort { got, needed } => {
write!(f, "received {got} bytes of an IL2P frame needing {needed}")
}
Il2pError::HeaderUncorrectable => {
write!(f, "IL2P header uncorrectable")
}
Il2pError::BlockUncorrectable { block } => {
write!(f, "IL2P payload block {block} uncorrectable")
}
Il2pError::UnsupportedPid { got } => {
write!(f, "unsupported IL2P PID code {got:#x}")
}
Il2pError::UnsupportedControl { got } => {
write!(f, "unsupported IL2P control code {got:#x} (UI only)")
}
Il2pError::Ax25(ref e) => write!(f, "AX.25 layer: {e}"),
Il2pError::Rs(ref e) => write!(f, "Reed-Solomon layer: {e}"),
}
}
}
impl core::error::Error for Il2pError {}
impl From<Ax25Error> for Il2pError {
fn from(e: Ax25Error) -> Self {
Il2pError::Ax25(e)
}
}
impl From<RsError> for Il2pError {
fn from(e: RsError) -> Self {
Il2pError::Rs(e)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Il2pHeader {
Transparent {
payload_len: u16,
},
Translated {
dest: Address,
src: Address,
pid: u8,
payload_len: u16,
command: bool,
},
}
impl Il2pHeader {
#[must_use]
pub const fn payload_len(&self) -> usize {
match *self {
Il2pHeader::Transparent { payload_len }
| Il2pHeader::Translated { payload_len, .. } => payload_len as usize,
}
}
pub fn pack(&self, max_fec: bool) -> Result<[u8; HEADER_LEN], Il2pError> {
let mut h = [0u8; HEADER_LEN];
if max_fec {
h[0] |= 0x80;
}
let count = self.payload_len() as u16;
for (k, slot) in h.iter_mut().enumerate().skip(2).take(10) {
if (count >> (11 - k)) & 1 != 0 {
*slot |= 0x80;
}
}
match *self {
Il2pHeader::Transparent { .. } => {}
Il2pHeader::Translated {
dest,
src,
pid,
command,
..
} => {
h[1] |= 0x80; h[0] |= 0x40; let code = pid_to_code(pid).ok_or(Il2pError::UnsupportedPid { got: pid })?;
for (k, slot) in h.iter_mut().enumerate().skip(1).take(4) {
if (code >> (4 - k)) & 1 != 0 {
*slot |= 0x40;
}
}
let control = if command {
CONTROL_UI_COMMAND
} else {
CONTROL_UI_OPCODE
};
for (k, slot) in h.iter_mut().enumerate().skip(5).take(7) {
if (control >> (11 - k)) & 1 != 0 {
*slot |= 0x40;
}
}
pack_callsign(&dest, &mut h, 0);
pack_callsign(&src, &mut h, 6);
h[12] = (dest.ssid.value() << 4) | src.ssid.value();
}
}
Ok(h)
}
pub fn unpack(h: &[u8; HEADER_LEN]) -> Result<Self, Il2pError> {
let mut count = 0u16;
for (k, &byte) in h.iter().enumerate().skip(2).take(10) {
count = (count << 1) | u16::from(byte >> 7);
let _ = k;
}
if h[1] & 0x80 == 0 {
return Ok(Il2pHeader::Transparent { payload_len: count });
}
let ui = h[0] & 0x40 != 0;
let mut code = 0u8;
for &byte in h.iter().skip(1).take(4) {
code = (code << 1) | ((byte >> 6) & 1);
}
let mut control = 0u8;
for &byte in h.iter().skip(5).take(7) {
control = (control << 1) | ((byte >> 6) & 1);
}
if !ui || control & CONTROL_UI_OPCODE_MASK != CONTROL_UI_OPCODE {
return Err(Il2pError::UnsupportedControl { got: control });
}
let pid = code_to_pid(code).ok_or(Il2pError::UnsupportedPid { got: code })?;
let dest_call = unpack_callsign(h, 0)?;
let src_call = unpack_callsign(h, 6)?;
let dest = Address::new(dest_call.text(), h[12] >> 4)?;
let src = Address::new(src_call.text(), h[12] & 0x0F)?;
Ok(Il2pHeader::Translated {
command: control & 0b100 != 0,
dest,
src,
pid,
payload_len: count,
})
}
}
struct SixbitCall {
chars: [u8; 6],
len: usize,
}
impl SixbitCall {
fn text(&self) -> &[u8] {
self.chars.get(..self.len).unwrap_or(&[])
}
}
fn pack_callsign(addr: &Address, h: &mut [u8; HEADER_LEN], at: usize) {
let text = addr.callsign.as_bytes();
for k in 0..6 {
let c = text.get(k).copied().unwrap_or(b' ');
if let Some(slot) = h.get_mut(at + k) {
*slot |= (c - 0x20) & 0x3F;
}
}
}
fn unpack_callsign(h: &[u8; HEADER_LEN], at: usize) -> Result<SixbitCall, Il2pError> {
let mut chars = [b' '; 6];
for (k, slot) in chars.iter_mut().enumerate() {
*slot = (h.get(at + k).copied().unwrap_or(0) & 0x3F) + 0x20;
}
let mut len = 6;
while len > 0 && chars[len - 1] == b' ' {
len -= 1;
}
if len == 0 {
return Err(Il2pError::Ax25(Ax25Error::CallsignLengthInvalid { got: 0 }));
}
let mut i = 0;
while i < len {
let c = chars[i];
if !c.is_ascii_uppercase() && !c.is_ascii_digit() {
return Err(Il2pError::Ax25(Ax25Error::InvalidCallsignChar { got: c }));
}
i += 1;
}
Ok(SixbitCall { chars, len })
}
fn code_to_pid(code: u8) -> Option<u8> {
PID_TABLE
.iter()
.find(|&&(c, _)| c == code)
.map(|&(_, pid)| pid)
}
fn pid_to_code(pid: u8) -> Option<u8> {
PID_TABLE.iter().find(|&&(_, p)| p == pid).map(|&(c, _)| c)
}
#[must_use]
pub const fn block_count_for(payload_len: usize, max_fec: bool) -> usize {
if payload_len == 0 {
0
} else if max_fec {
payload_len.div_ceil(MAX_BLOCK_DATA)
} else {
payload_len.div_ceil(MAX_BASELINE_BLOCK_DATA)
}
}
#[must_use]
pub const fn payload_wire_len(payload_len: usize, max_fec: bool) -> usize {
let blocks = block_count_for(payload_len, max_fec);
payload_len + blocks * payload_parity(payload_len, max_fec).len()
}
#[must_use]
pub const fn payload_parity(payload_len: usize, max_fec: bool) -> Il2pParity {
if max_fec {
return Il2pParity::Sixteen;
}
let blocks = block_count_for(payload_len, false);
if blocks == 0 {
return Il2pParity::Two;
}
Il2pParity::baseline_for_block(payload_len / blocks)
}
#[must_use]
pub const fn encoded_len(payload_len: usize, parity: Il2pParity) -> usize {
SYNC_LEN + HEADER_LEN + HEADER_PARITY_LEN + payload_wire_len(payload_len, parity.is_max_fec())
}
fn header_codec() -> RsCodec {
RsCodec::with_fcr(RsParity::Two, 0)
}
fn block_codec(parity: Il2pParity) -> RsCodec {
RsCodec::with_fcr(parity.rs(), 0)
}
pub fn encode(
header: &Il2pHeader,
payload: &[u8],
parity: Il2pParity,
out: &mut [u8],
) -> Result<usize, Il2pError> {
if payload.len() > PAYLOAD_MAX {
return Err(Il2pError::PayloadTooLong {
got: payload.len(),
max: PAYLOAD_MAX,
});
}
debug_assert_eq!(header.payload_len(), payload.len());
let total = encoded_len(payload.len(), parity);
if out.len() < total {
return Err(Il2pError::BufferTooSmall {
needed: total,
got: out.len(),
});
}
let mut pos = 0usize;
let put = |bytes: &[u8], pos: &mut usize, out: &mut [u8]| {
if let Some(slot) = out.get_mut(*pos..*pos + bytes.len()) {
slot.copy_from_slice(bytes);
}
*pos += bytes.len();
};
put(&SYNC_BYTES, &mut pos, out);
let max_fec = parity.is_max_fec();
let parity = payload_parity(payload.len(), max_fec);
let mut h = header.pack(max_fec)?;
debug_assert_eq!(
h[0] & 0x80 != 0,
max_fec,
"header FEC level must match the payload parity"
);
debug_assert_eq!(
total,
SYNC_LEN + HDR_BLOCK + payload_wire_len(payload.len(), max_fec),
"emitted length must equal what the header tells a receiver to expect"
);
Il2pScrambler::new().scramble(&mut h);
let mut hp = [0u8; HEADER_PARITY_LEN];
header_codec().encode(&h, &mut hp)?;
put(&h, &mut pos, out);
put(&hp, &mut pos, out);
let nblocks = block_count_for(payload.len(), max_fec);
if let Some(small) = payload.len().checked_div(nblocks) {
let big_blocks = payload.len() % nblocks;
let codec = block_codec(parity);
let mut offset = 0usize;
let mut block = [0u8; MAX_BASELINE_BLOCK_DATA];
let mut bp = [0u8; Il2pParity::Sixteen.len()];
for i in 0..nblocks {
let size = small + usize::from(i < big_blocks);
let chunk = payload.get(offset..offset + size).unwrap_or(&[]);
for (dst, src) in block.iter_mut().zip(chunk.iter()) {
*dst = *src;
}
offset += size;
let scrambled = block.get_mut(..size).unwrap_or(&mut []);
Il2pScrambler::new().scramble(scrambled);
let bp_slice = bp.get_mut(..parity.len()).unwrap_or(&mut []);
codec.encode(block.get(..size).unwrap_or(&[]), bp_slice)?;
put(block.get(..size).unwrap_or(&[]), &mut pos, out);
put(bp.get(..parity.len()).unwrap_or(&[]), &mut pos, out);
}
}
debug_assert_eq!(
pos, total,
"emitted byte count must equal the length reported to the caller"
);
Ok(pos)
}
pub fn encode_ui_frame(
frame: &UiFrame<'_>,
parity: Il2pParity,
out: &mut [u8],
) -> Result<usize, Il2pError> {
if frame.path().is_empty() {
if frame.info.len() > PAYLOAD_MAX {
return Err(Il2pError::PayloadTooLong {
got: frame.info.len(),
max: PAYLOAD_MAX,
});
}
#[allow(clippy::cast_possible_truncation)] let header = Il2pHeader::Translated {
dest: frame.dest,
src: frame.src,
pid: 0xF0,
payload_len: frame.info.len() as u16,
command: true,
};
return encode(&header, frame.info, parity, out);
}
let needed = frame.encoded_len();
if needed > PAYLOAD_MAX {
return Err(Il2pError::PayloadTooLong {
got: needed,
max: PAYLOAD_MAX,
});
}
let mut body = [0u8; PAYLOAD_MAX];
let len = frame.build(&mut body)?;
encode_raw(body.get(..len).unwrap_or(&[]), parity, out)
}
pub fn encode_raw(payload: &[u8], parity: Il2pParity, out: &mut [u8]) -> Result<usize, Il2pError> {
if payload.len() > PAYLOAD_MAX {
return Err(Il2pError::PayloadTooLong {
got: payload.len(),
max: PAYLOAD_MAX,
});
}
#[allow(clippy::cast_possible_truncation)] let header = Il2pHeader::Transparent {
payload_len: payload.len() as u16,
};
encode(&header, payload, parity, out)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Il2pDecoded {
pub header: Il2pHeader,
pub payload_len: usize,
pub header_corrected: usize,
pub payload_corrected: usize,
}
impl Il2pDecoded {
#[must_use]
pub const fn corrected(&self) -> usize {
self.header_corrected + self.payload_corrected
}
}
pub fn decode(
bytes: &[u8],
_parity_ignored: Il2pParity,
payload_out: &mut [u8],
) -> Result<Il2pDecoded, Il2pError> {
let Some(hdr_bytes) = bytes.get(..HDR_BLOCK) else {
return Err(Il2pError::FrameTooShort {
got: bytes.len(),
needed: HDR_BLOCK,
});
};
let mut hdr_block = [0u8; HDR_BLOCK];
hdr_block.copy_from_slice(hdr_bytes);
let header_corrected = header_codec()
.decode(&mut hdr_block)
.map_err(|_| Il2pError::HeaderUncorrectable)?;
let mut h = [0u8; HEADER_LEN];
h.copy_from_slice(hdr_block.get(..HEADER_LEN).unwrap_or(&[]));
Il2pScrambler::new().descramble(&mut h);
let header = Il2pHeader::unpack(&h)?;
let max_fec = h[0] & 0x80 != 0;
let payload_len = header.payload_len();
let nblocks = block_count_for(payload_len, max_fec);
let parity = payload_parity(payload_len, max_fec);
let needed = HDR_BLOCK + payload_len + nblocks * parity.len();
if bytes.len() < needed {
return Err(Il2pError::FrameTooShort {
got: bytes.len(),
needed,
});
}
if payload_out.len() < payload_len {
return Err(Il2pError::BufferTooSmall {
needed: payload_len,
got: payload_out.len(),
});
}
let mut payload_corrected = 0usize;
if let Some(small) = payload_len.checked_div(nblocks) {
let big_blocks = payload_len % nblocks;
let codec = block_codec(parity);
let mut pos = HDR_BLOCK;
let mut written = 0usize;
let mut block = [0u8; crate::rs::BLOCK_MAX];
for i in 0..nblocks {
let size = small + usize::from(i < big_blocks);
let coded = size + parity.len();
let chunk = bytes.get(pos..pos + coded).unwrap_or(&[]);
for (dst, src) in block.iter_mut().zip(chunk.iter()) {
*dst = *src;
}
pos += coded;
let word = block.get_mut(..coded).unwrap_or(&mut []);
payload_corrected += codec
.decode(word)
.map_err(|_| Il2pError::BlockUncorrectable { block: i })?;
let data = block.get_mut(..size).unwrap_or(&mut []);
Il2pScrambler::new().descramble(data);
if let Some(slot) = payload_out.get_mut(written..written + size) {
slot.copy_from_slice(block.get(..size).unwrap_or(&[]));
}
written += size;
}
}
Ok(Il2pDecoded {
header,
payload_len,
header_corrected,
payload_corrected,
})
}
const HDR_BLOCK: usize = HEADER_LEN + HEADER_PARITY_LEN;
pub fn to_ui_frame<'a>(header: &Il2pHeader, payload: &'a [u8]) -> Result<UiFrame<'a>, Il2pError> {
match *header {
Il2pHeader::Transparent { .. } => Ok(UiFrame::parse(payload)?),
Il2pHeader::Translated { dest, src, .. } => Ok(UiFrame::new(dest, src, payload)),
}
}
#[must_use]
pub fn tx_bits(frame: &[u8], preamble_bytes: usize, tail_bytes: usize) -> Il2pTxBits<'_> {
Il2pTxBits {
frame,
preamble: preamble_bytes,
tail: tail_bytes,
pos: 0,
}
}
#[derive(Debug, Clone)]
pub struct Il2pTxBits<'a> {
frame: &'a [u8],
preamble: usize,
tail: usize,
pos: usize,
}
impl Iterator for Il2pTxBits<'_> {
type Item = Bit;
fn next(&mut self) -> Option<Bit> {
let index = self.pos / 8;
let byte = if index < self.preamble {
PREAMBLE_BYTE
} else if let Some(&b) = self.frame.get(index - self.preamble) {
b
} else if index < self.preamble + self.frame.len() + self.tail {
PREAMBLE_BYTE
} else {
return None;
};
let bit = Bit::from((byte >> (7 - self.pos % 8)) & 1 != 0);
self.pos += 1;
Some(bit)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Il2pRxFrame<'a> {
pub decoded: Il2pDecoded,
payload: &'a [u8],
}
impl<'a> Il2pRxFrame<'a> {
#[must_use]
pub const fn payload(&self) -> &'a [u8] {
self.payload
}
#[must_use]
pub const fn header(&self) -> &Il2pHeader {
&self.decoded.header
}
#[must_use]
pub const fn corrected(&self) -> usize {
self.decoded.corrected()
}
pub fn ui_frame(&self) -> Result<UiFrame<'a>, Il2pError> {
to_ui_frame(&self.decoded.header, self.payload)
}
}
#[derive(Debug, Clone, Copy)]
enum Il2pRxState {
Hunt,
Collect {
count: usize,
nbits: u8,
cur: u8,
needed: usize,
have_header: bool,
},
}
#[derive(Debug, Clone)]
pub struct Il2pReceiver {
parity: Il2pParity,
accum: u32,
seen: u32,
state: Il2pRxState,
buf: [u8; RX_FRAME_MAX],
payload: [u8; PAYLOAD_MAX],
}
impl Il2pReceiver {
#[must_use]
pub const fn new(parity: Il2pParity) -> Self {
Self {
parity,
accum: 0,
seen: 0,
state: Il2pRxState::Hunt,
buf: [0; RX_FRAME_MAX],
payload: [0; PAYLOAD_MAX],
}
}
#[must_use]
pub const fn parity(&self) -> Il2pParity {
self.parity
}
const fn reset(&mut self) {
self.accum = 0;
self.seen = 0;
self.state = Il2pRxState::Hunt;
}
pub fn push(&mut self, bit: Bit) -> Option<Result<Il2pRxFrame<'_>, Il2pError>> {
match self.state {
Il2pRxState::Hunt => {
self.accum = (self.accum << 1) & 0x00FF_FFFF;
if let Bit::One = bit {
self.accum |= 1;
}
self.seen = self.seen.saturating_add(1);
if self.seen >= 24 && (self.accum ^ SYNC_WORD).count_ones() <= SYNC_TOLERANCE {
self.state = Il2pRxState::Collect {
count: 0,
nbits: 0,
cur: 0,
needed: HDR_BLOCK,
have_header: false,
};
}
None
}
Il2pRxState::Collect {
mut count,
mut nbits,
mut cur,
mut needed,
mut have_header,
} => {
cur <<= 1;
if let Bit::One = bit {
cur |= 1;
}
nbits += 1;
if nbits == 8 {
if let Some(slot) = self.buf.get_mut(count) {
*slot = cur;
}
count += 1;
nbits = 0;
cur = 0;
if count == needed && !have_header {
match Self::peek_payload_len(&self.buf) {
Ok((payload_len, max_fec)) => {
needed = HDR_BLOCK + payload_wire_len(payload_len, max_fec);
have_header = true;
}
Err(e) => {
self.reset();
return Some(Err(e));
}
}
}
if count == needed && have_header {
self.reset();
return Some(self.finish(needed));
}
}
self.state = Il2pRxState::Collect {
count,
nbits,
cur,
needed,
have_header,
};
None
}
}
}
fn peek_payload_len(buf: &[u8; RX_FRAME_MAX]) -> Result<(usize, bool), Il2pError> {
let mut hdr_block = [0u8; HDR_BLOCK];
hdr_block.copy_from_slice(buf.get(..HDR_BLOCK).unwrap_or(&[]));
header_codec()
.decode(&mut hdr_block)
.map_err(|_| Il2pError::HeaderUncorrectable)?;
let mut h = [0u8; HEADER_LEN];
h.copy_from_slice(hdr_block.get(..HEADER_LEN).unwrap_or(&[]));
Il2pScrambler::new().descramble(&mut h);
let max_fec = h[0] & 0x80 != 0;
Ok((Il2pHeader::unpack(&h)?.payload_len(), max_fec))
}
fn finish(&mut self, needed: usize) -> Result<Il2pRxFrame<'_>, Il2pError> {
let bytes = self.buf.get(..needed).unwrap_or(&[]);
let decoded = decode(bytes, self.parity, &mut self.payload)?;
Ok(Il2pRxFrame {
decoded,
payload: self.payload.get(..decoded.payload_len).unwrap_or(&[]),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn reference_scramble(bytes: &[u8]) -> impl Iterator<Item = u8> + '_ {
let mut history = [0u8; 9];
for (d, slot) in history.iter_mut().enumerate() {
*slot = ((SCRAMBLER_SEED >> d) & 1) as u8;
}
bytes.iter().map(move |&byte| {
let mut out_byte = 0u8;
for k in (0..8).rev() {
let bit = (byte >> k) & 1;
let out = bit ^ history[3] ^ history[8];
history.rotate_right(1);
history[0] = out;
out_byte |= out << k;
}
out_byte
})
}
#[test]
fn scrambler_matches_reference_recurrence() {
let data: [u8; 64] = core::array::from_fn(|i| (i as u8).wrapping_mul(37) ^ 0xA5);
let mut scrambled = data;
Il2pScrambler::new().scramble(&mut scrambled);
for (got, want) in scrambled.iter().zip(reference_scramble(&data)) {
assert_eq!(*got, want);
}
}
#[test]
fn scrambler_known_answer_vector() {
let mut data = [0u8; 4];
Il2pScrambler::new().scramble(&mut data);
let mut expected = [0u8; 4];
for (slot, b) in expected.iter_mut().zip(reference_scramble(&[0u8; 4])) {
*slot = b;
}
assert_eq!(data, expected);
assert_eq!(data, [0x0F, 0x70, 0xB3, 0x6F]);
}
#[test]
fn scrambler_self_inverse() {
let data: [u8; 100] = core::array::from_fn(|i| (i as u8).wrapping_mul(151));
let mut work = data;
Il2pScrambler::new().scramble(&mut work);
Il2pScrambler::new().descramble(&mut work);
assert_eq!(work, data);
}
#[test]
fn header_pack_unpack_type1() {
let header = Il2pHeader::Translated {
command: true,
dest: Address::new(b"APRS", 0).unwrap(),
src: Address::new(b"N0CALL", 15).unwrap(),
pid: 0xF0,
payload_len: 1023,
};
let packed = header.pack(true).unwrap();
assert_eq!(Il2pHeader::unpack(&packed).unwrap(), header);
}
#[test]
fn header_pack_unpack_type0() {
for len in [0u16, 1, 204, 205, 206, 1023] {
let header = Il2pHeader::Transparent { payload_len: len };
let packed = header.pack(true).unwrap();
assert_eq!(Il2pHeader::unpack(&packed).unwrap(), header);
}
}
#[test]
fn header_known_answer() {
let header = Il2pHeader::Translated {
command: true,
dest: Address::new(b"AB", 1).unwrap(),
src: Address::new(b"C", 2).unwrap(),
pid: 0xF0,
payload_len: 5,
};
let h = header.pack(true).unwrap();
assert_eq!(h[0] & 0x3F, 0x21);
assert_eq!(h[1] & 0x3F, 0x22);
assert_eq!(h[6] & 0x3F, 0x23); assert_eq!(h[1] & 0x80, 0x80); assert_eq!(h[0] & 0x40, 0x40); assert_eq!(
[h[1] >> 6 & 1, h[2] >> 6 & 1, h[3] >> 6 & 1, h[4] >> 6 & 1],
[1, 1, 1, 1]
);
let mut control_bits = [0u8; 7];
for (k, slot) in control_bits.iter_mut().enumerate() {
*slot = h[k + 5] >> 6 & 1;
}
assert_eq!(control_bits, [0, 1, 0, 1, 1, 0, 0]);
let mut count_bits = [0u8; 10];
for (k, slot) in count_bits.iter_mut().enumerate() {
*slot = h[k + 2] >> 7;
}
assert_eq!(count_bits, [0, 0, 0, 0, 0, 0, 0, 1, 0, 1]);
assert_eq!(h[12], 0x12); }
#[test]
fn pid_constant_agrees_with_table() {
assert_eq!(pid_to_code(0xF0), Some(PID_CODE_NO_LAYER3));
assert_eq!(code_to_pid(PID_CODE_NO_LAYER3), Some(0xF0));
}
#[test]
fn pid_table_is_a_bijection() {
for (i, (code, pid)) in PID_TABLE.iter().enumerate() {
assert!(*code <= 0xF, "code 0x{code:X} exceeds 4 bits");
for (other_code, other_pid) in PID_TABLE.iter().skip(i + 1) {
assert_ne!(code, other_code, "duplicate IL2P code 0x{code:X}");
assert_ne!(pid, other_pid, "duplicate AX.25 PID 0x{pid:02X}");
}
}
for (code, _) in PID_TABLE {
assert!(
!matches!(code, 0x0 | 0x1 | 0x7 | 0x8 | 0x9 | 0xA),
"code 0x{code:X} is reserved or Future in spec v0.6"
);
}
}
#[test]
fn block_layout() {
assert_eq!(block_count_for(0, true), 0);
assert_eq!(block_count_for(1, true), 1);
assert_eq!(block_count_for(239, true), 1);
assert_eq!(block_count_for(240, true), 2);
assert_eq!(block_count_for(478, true), 2);
assert_eq!(block_count_for(479, true), 3);
assert_eq!(block_count_for(1023, true), 5);
assert_eq!(encoded_len(1023, Il2pParity::Sixteen), ENCODED_MAX);
assert_eq!(block_count_for(0, false), 0);
assert_eq!(block_count_for(1, false), 1);
assert_eq!(block_count_for(MAX_BASELINE_BLOCK_DATA, false), 1);
assert_eq!(block_count_for(MAX_BASELINE_BLOCK_DATA + 1, false), 2);
assert_eq!(block_count_for(494, false), 2);
assert_eq!(block_count_for(495, false), 3);
assert_eq!(block_count_for(1023, false), 5);
for (len, baseline, max_fec) in [(240, 1, 2), (479, 2, 3), (718, 3, 4), (957, 4, 5)] {
assert_eq!(block_count_for(len, false), baseline, "{len} baseline");
assert_eq!(block_count_for(len, true), max_fec, "{len} max FEC");
}
}
#[test]
fn raw_roundtrip_all_operating_points() {
let payload: [u8; 300] = core::array::from_fn(|i| (i as u8) ^ 0x3C);
for parity in Il2pParity::ALL {
let mut tx = [0u8; ENCODED_MAX];
let len = encode_raw(&payload, parity, &mut tx).unwrap();
assert_eq!(len, encoded_len(payload.len(), parity));
assert_eq!(&tx[..SYNC_LEN], &SYNC_BYTES);
let mut out = [0u8; PAYLOAD_MAX];
let decoded = decode(&tx[SYNC_LEN..len], parity, &mut out).unwrap();
assert_eq!(decoded.payload_len, payload.len());
assert_eq!(&out[..decoded.payload_len], &payload);
assert_eq!(decoded.corrected(), 0);
}
}
#[test]
fn empty_payload_roundtrip() {
let mut tx = [0u8; ENCODED_MAX];
let len = encode_raw(&[], Il2pParity::Sixteen, &mut tx).unwrap();
assert_eq!(len, SYNC_LEN + HEADER_LEN + HEADER_PARITY_LEN);
let mut out = [0u8; 0];
let decoded = decode(&tx[SYNC_LEN..len], Il2pParity::Sixteen, &mut out).unwrap();
assert_eq!(decoded.payload_len, 0);
}
}