use viterbi::puncture::{DePuncturer, Puncturer};
use viterbi::{
CcsdsSoftDecoder, CcsdsViterbiDecoder, CodeParams, CodeProfile, CodedBlock, ConfigError,
DecodeError, EncodeError, HardHamming, ParamError, PunctureError, PunctureMatrix,
PunctureOrder, PuncturedRate, SoftLlr, ViterbiDecoder, ViterbiEncoder, MAX_SUPPORTED_INFO_BITS,
};
mod common;
fn bit(bytes: &[u8], i: usize) -> u8 {
(bytes[i / 8] >> (7 - (i % 8))) & 1
}
fn coded_bits_hard(cb: &CodedBlock) -> Vec<u8> {
(0..cb.nbits).map(|i| bit(&cb.bytes, i)).collect()
}
fn coded_bits_soft(cb: &CodedBlock) -> Vec<i8> {
(0..cb.nbits)
.map(|i| if bit(&cb.bytes, i) == 0 { 64 } else { -64 })
.collect()
}
fn pack_first(data: &[u8], nbits: usize) -> Vec<u8> {
let mut out = vec![0u8; nbits.div_ceil(8)];
for i in 0..nbits {
if bit(data, i) == 1 {
out[i / 8] |= 1 << (7 - (i % 8));
}
}
out
}
fn flip_bit(bytes: &mut [u8], i: usize) {
bytes[i / 8] ^= 1 << (7 - (i % 8));
}
#[test]
fn empty_and_minimal_inputs_round_trip() {
let (enc, mut dec) = common::codec();
for raw in [vec![], vec![0x00], vec![0xFF], vec![0x80]] {
let coded = enc.encode(&raw).unwrap();
assert_eq!(dec.decode_block(&coded).unwrap().bytes, raw);
}
}
#[test]
fn empty_payload_round_trips_to_empty_block() {
let (enc, mut dec) = common::codec();
let coded = enc.encode(&[]).unwrap();
let decoded = dec.decode_block(&coded).unwrap();
assert!(
decoded.bytes.is_empty(),
"empty payload must decode to no bytes"
);
assert_eq!(
decoded.nbits, 0,
"empty payload must decode to zero info bits"
);
}
#[test]
fn soft_minimal_inputs_round_trip() {
let enc = ViterbiEncoder::new(CodeParams::ccsds_r1_2()).unwrap();
let mut dec = CcsdsSoftDecoder::new(CodeParams::ccsds_r1_2(), 1_000).unwrap();
let coded = enc.encode_bits(&[], 0).unwrap();
let out = dec.decode(&coded_bits_soft(&coded), 0).unwrap();
assert!(out.bytes.is_empty());
assert_eq!(out.nbits, 0);
let coded = enc.encode_bits(&[0x80], 1).unwrap();
let out = dec.decode(&coded_bits_soft(&coded), 1).unwrap();
assert_eq!(out.nbits, 1);
assert_eq!(out.bytes, pack_first(&[0x80], 1));
let coded = enc.encode(&[0xC3]).unwrap();
let out = dec.decode(&coded_bits_soft(&coded), 8).unwrap();
assert_eq!(out.bytes, vec![0xC3]);
}
#[test]
fn rate_1_3_minimal_inputs_round_trip() {
let enc = ViterbiEncoder::new(CodeParams::ccsds_r1_3()).unwrap();
let mut dec = ViterbiDecoder::<64, HardHamming>::new(CodeParams::ccsds_r1_3(), 1_000).unwrap();
let coded = enc.encode_bits(&[], 0).unwrap();
let out = dec.decode(&coded_bits_hard(&coded), 0).unwrap();
assert!(out.bytes.is_empty());
assert_eq!(out.nbits, 0);
let coded = enc.encode_bits(&[0x80], 1).unwrap();
assert_eq!(dec.decode(&coded_bits_hard(&coded), 1).unwrap().nbits, 1);
let coded = enc.encode(&[0x5A]).unwrap();
assert_eq!(
dec.decode(&coded_bits_hard(&coded), 8).unwrap().bytes,
vec![0x5A]
);
}
#[test]
fn k9_minimal_inputs_round_trip() {
let params = CodeProfile::K9R1_2.params();
let enc = ViterbiEncoder::new(params.clone()).unwrap();
let mut dec = ViterbiDecoder::<256, HardHamming>::new(params, 1_000).unwrap();
for raw in [vec![], vec![0x00], vec![0xFF], vec![0x80]] {
let coded = enc.encode(&raw).unwrap();
let out = dec.decode_block(&coded).unwrap();
assert_eq!(out.bytes, raw);
assert_eq!(out.nbits, raw.len() * 8);
}
}
#[test]
fn all_zeros_and_all_ones_max_block() {
let (enc, mut dec) = common::codec();
for raw in [vec![0x00; 512], vec![0xFF; 512]] {
let coded = enc.encode(&raw).unwrap();
assert_eq!(dec.decode_block(&coded).unwrap().bytes, raw);
}
}
#[test]
fn payload_extremes_round_trip_soft_and_rate_1_3() {
let enc2 = ViterbiEncoder::new(CodeParams::ccsds_r1_2()).unwrap();
let mut soft = CcsdsSoftDecoder::new(CodeParams::ccsds_r1_2(), 4_096).unwrap();
for raw in [vec![0x00u8; 64], vec![0xFFu8; 64]] {
let coded = enc2.encode(&raw).unwrap();
assert_eq!(
soft.decode(&coded_bits_soft(&coded), raw.len() * 8)
.unwrap()
.bytes,
raw
);
}
let enc3 = ViterbiEncoder::new(CodeParams::ccsds_r1_3()).unwrap();
let mut dec3 = ViterbiDecoder::<64, HardHamming>::new(CodeParams::ccsds_r1_3(), 4_096).unwrap();
for raw in [vec![0x00u8; 64], vec![0xFFu8; 64]] {
let coded = enc3.encode(&raw).unwrap();
assert_eq!(
dec3.decode(&coded_bits_hard(&coded), raw.len() * 8)
.unwrap()
.bytes,
raw
);
}
}
#[test]
fn non_byte_aligned_nbits_round_trip() {
let enc = ViterbiEncoder::new(CodeParams::ccsds_r1_2()).unwrap();
let mut hard = CcsdsViterbiDecoder::new(CodeParams::ccsds_r1_2(), 4_096).unwrap();
let mut soft = CcsdsSoftDecoder::new(CodeParams::ccsds_r1_2(), 4_096).unwrap();
let data = [0b1011_0010u8, 0b1110_0001, 0b0101_1010];
for &nbits in &[1usize, 3, 7, 9, 13, 17, 23] {
let coded = enc.encode_bits(&data, nbits).unwrap();
let expected = pack_first(&data, nbits);
let out = hard.decode(&coded_bits_hard(&coded), nbits).unwrap();
assert_eq!(out.nbits, nbits, "hard nbits mismatch for nbits={nbits}");
assert_eq!(
out.bytes, expected,
"hard payload mismatch for nbits={nbits}"
);
let out = soft.decode(&coded_bits_soft(&coded), nbits).unwrap();
assert_eq!(out.nbits, nbits, "soft nbits mismatch for nbits={nbits}");
assert_eq!(
out.bytes, expected,
"soft payload mismatch for nbits={nbits}"
);
}
}
#[test]
fn soft_extreme_llr_saturated_samples_never_panic() {
let nbits = 64usize;
let mut dec = CcsdsSoftDecoder::new(CodeParams::ccsds_r1_2(), 1_000).unwrap();
let len = (nbits + 6) * 2; for &fill in &[i8::MIN, i8::MAX] {
let samples = vec![fill; len];
let out = dec.decode(&samples, nbits).unwrap();
assert_eq!(
out.nbits, nbits,
"extreme-LLR fill {fill} must yield a full-length block"
);
assert_eq!(out.bytes.len(), nbits / 8);
}
}
#[test]
fn soft_all_erasure_llr_decodes_best_effort() {
let nbits = 64usize;
let mut dec = CcsdsSoftDecoder::new(CodeParams::ccsds_r1_2(), 1_000).unwrap();
let samples = vec![0i8; (nbits + 6) * 2];
let out = dec.decode(&samples, nbits).unwrap();
assert_eq!(out.nbits, nbits);
assert_eq!(out.bytes.len(), nbits / 8);
}
#[test]
fn rate_1_2_capacity_boundary() {
let (enc, mut dec) = common::codec();
let raw: Vec<u8> = (0..64).collect();
let mut coded = enc.encode(&raw).unwrap();
for &i in &[50usize, 300, 550, 800] {
flip_bit(&mut coded.bytes, i);
}
assert_eq!(
dec.decode_block(&coded).unwrap().bytes,
raw,
"4 well-spaced errors are within rate-1/2 (t=4) capability"
);
let (enc, mut dec) = common::codec();
let mut coded = enc.encode(&raw).unwrap();
for i in 500..=505 {
flip_bit(&mut coded.bytes, i);
}
let out = dec.decode_block(&coded).unwrap();
assert_eq!(
out.nbits,
raw.len() * 8,
"beyond-capacity decode must still return a full block"
);
}
#[test]
fn rate_1_3_capacity_boundary() {
let enc = ViterbiEncoder::new(CodeParams::ccsds_r1_3()).unwrap();
let raw = vec![0xA5u8; 16]; let nbits = raw.len() * 8;
let coded = enc.encode(&raw).unwrap();
let mut samples = coded_bits_hard(&coded);
let span = samples.len() / 6;
for e in 0..6 {
samples[e * span] ^= 1;
}
let mut dec = ViterbiDecoder::<64, HardHamming>::new(CodeParams::ccsds_r1_3(), 4096).unwrap();
assert_eq!(
dec.decode(&samples, nbits).unwrap().bytes,
raw,
"6 well-spaced errors are within rate-1/3 (t=6) capability"
);
let mut samples = coded_bits_hard(&coded);
for i in (0..samples.len()).step_by(3) {
samples[i] ^= 1;
}
let out = dec.decode(&samples, nbits).unwrap();
assert_eq!(
out.nbits, nbits,
"beyond-capacity rate-1/3 decode must still return a full block"
);
}
#[test]
fn max_info_bits_boundary_decodes_and_rejects_beyond() {
let cap = 256usize;
let enc = ViterbiEncoder::new(CodeParams::ccsds_r1_2()).unwrap();
let mut dec = CcsdsViterbiDecoder::new(CodeParams::ccsds_r1_2(), cap).unwrap();
let raw: Vec<u8> = (0..(cap / 8) as u32)
.map(|i| (i as u8).wrapping_mul(31))
.collect();
let coded = enc.encode(&raw).unwrap();
assert_eq!(dec.decode_block(&coded).unwrap().bytes, raw);
assert!(matches!(
dec.decode(&[], cap + 1),
Err(DecodeError::InputTooLong { max_bits, got_bits })
if max_bits == cap && got_bits == cap + 1
));
}
#[test]
fn config_max_info_bits_zero_and_over_cap_rejected() {
assert_eq!(
CcsdsViterbiDecoder::new(CodeParams::ccsds_r1_2(), 0)
.err()
.unwrap(),
ConfigError::MaxBlockZero
);
assert!(matches!(
CcsdsViterbiDecoder::new(CodeParams::ccsds_r1_2(), MAX_SUPPORTED_INFO_BITS + 1),
Err(ConfigError::MaxBlockTooLarge { cap, got })
if cap == MAX_SUPPORTED_INFO_BITS && got == MAX_SUPPORTED_INFO_BITS + 1
));
}
#[test]
fn encode_resource_limits_rejected() {
let enc = ViterbiEncoder::new(CodeParams::ccsds_r1_2()).unwrap();
let big = vec![0u8; MAX_SUPPORTED_INFO_BITS / 8 + 1];
assert!(matches!(
enc.encode(&big),
Err(EncodeError::PayloadTooLarge { .. })
));
assert_eq!(
enc.encode_bits(&[0x00], 9),
Err(EncodeError::NbitsExceedsData {
nbits: 9,
available: 8
})
);
}
#[test]
fn every_reachable_param_error_variant() {
assert_eq!(
CodeParams::new(7, vec![0o171, 0], vec![false, true]),
Err(ParamError::ZeroGenerator)
);
assert_eq!(
CodeParams::new(3, vec![0b1000, 0b101], vec![false, false]),
Err(ParamError::GeneratorOutOfRange { g: 0b1000, k: 3 })
);
assert_eq!(
CodeParams::new(10, vec![1, 1], vec![false, false]),
Err(ParamError::KOutOfRange { k: 10 })
);
assert_eq!(
CodeParams::new(7, vec![0o171, 0o133], vec![false]),
Err(ParamError::InconsistentLengths)
);
assert_eq!(
CodeParams::new(7, vec![0o171, 0o171], vec![false, false]),
Err(ParamError::Catastrophic)
);
let n1 = CodeParams::new(7, vec![0o100], vec![false]).unwrap();
assert_eq!(
ViterbiEncoder::new(n1).err().unwrap(),
ParamError::RateUnsupported { n: 1 }
);
let k3 = CodeParams::new(3, vec![0b101, 0b111], vec![false, false]).unwrap();
assert!(matches!(
ViterbiDecoder::<64, HardHamming>::new(k3, 64),
Err(ConfigError::Param(ParamError::StateCountMismatch {
states: 64,
k: 3
}))
));
}
#[test]
fn every_reachable_config_error_variant() {
assert_eq!(
CcsdsViterbiDecoder::new(CodeParams::ccsds_r1_2(), 0)
.err()
.unwrap(),
ConfigError::MaxBlockZero
);
assert!(matches!(
CcsdsViterbiDecoder::new(CodeParams::ccsds_r1_2(), MAX_SUPPORTED_INFO_BITS + 1),
Err(ConfigError::MaxBlockTooLarge { .. })
));
let n1 = CodeParams::new(7, vec![0o100], vec![false]).unwrap();
assert!(matches!(
ViterbiDecoder::<64, HardHamming>::new(n1, 64),
Err(ConfigError::Param(ParamError::RateUnsupported { n: 1 }))
));
}
#[test]
fn every_reachable_decode_error_variant() {
let mut dec = CcsdsViterbiDecoder::new(CodeParams::ccsds_r1_2(), 8).unwrap();
assert!(matches!(
dec.decode(&[], 16),
Err(DecodeError::InputTooLong {
max_bits: 8,
got_bits: 16
})
));
assert!(matches!(
dec.decode_block(&CodedBlock {
bytes: vec![0; 4],
nbits: 15
}),
Err(DecodeError::LengthMismatch)
));
let expected = (4 + 6) * 2; assert!(matches!(
dec.decode(&vec![0u8; expected + 1], 4),
Err(DecodeError::LengthMismatch)
));
}
#[test]
fn every_reachable_puncture_error_variant() {
assert_eq!(
PunctureMatrix::new(vec![], PunctureOrder::ByColumn).unwrap_err(),
PunctureError::EmptyPattern
);
assert_eq!(
PunctureMatrix::new(vec![vec![], vec![]], PunctureOrder::ByColumn).unwrap_err(),
PunctureError::EmptyPattern
);
assert_eq!(
PunctureMatrix::new(vec![vec![true, false], vec![true]], PunctureOrder::ByColumn)
.unwrap_err(),
PunctureError::RaggedRows
);
let long_row = vec![true; viterbi::puncture::MAX_PERIOD + 1];
assert!(matches!(
PunctureMatrix::new(vec![long_row.clone(), long_row], PunctureOrder::ByColumn),
Err(PunctureError::PeriodTooLarge { period, cap })
if period == viterbi::puncture::MAX_PERIOD + 1 && cap == viterbi::puncture::MAX_PERIOD
));
assert_eq!(
PunctureMatrix::new(
vec![vec![true, false], vec![true, false]],
PunctureOrder::ByColumn
)
.unwrap_err(),
PunctureError::AllVoidPeriod
);
assert_eq!(
PunctureMatrix::ccsds_rate(3, PuncturedRate::R2_3).unwrap_err(),
PunctureError::UnsupportedRate
);
let cat = PunctureMatrix::new(
vec![vec![true, true], vec![false, false]],
PunctureOrder::ByColumn,
)
.unwrap();
assert_eq!(
Puncturer::new(cat.clone(), &CodeParams::ccsds_r1_2()).unwrap_err(),
PunctureError::Catastrophic
);
assert_eq!(
DePuncturer::<HardHamming>::new(cat, &CodeParams::ccsds_r1_2()).unwrap_err(),
PunctureError::Catastrophic
);
let m2 = PunctureMatrix::ccsds_rate(2, PuncturedRate::R2_3).unwrap();
assert!(matches!(
Puncturer::new(m2, &CodeParams::ccsds_r1_3()),
Err(PunctureError::NMismatch {
matrix_n: 2,
params_n: 3
})
));
let good = PunctureMatrix::ccsds_rate(2, PuncturedRate::R2_3).unwrap();
let punc = Puncturer::new(good.clone(), &CodeParams::ccsds_r1_2()).unwrap();
let bad_coded = CodedBlock {
bytes: vec![0u8; 4],
nbits: 7,
};
assert!(matches!(
punc.puncture(&bad_coded, 8),
Err(PunctureError::MisalignedInput { .. })
));
let dp = DePuncturer::<HardHamming>::new(good, &CodeParams::ccsds_r1_2()).unwrap();
let expected = dp.expected_punctured_len(16).unwrap();
assert!(matches!(
dp.depuncture(&vec![0u8; expected + 1], 16),
Err(PunctureError::MisalignedInput { got, .. }) if got == expected + 1
));
}
#[test]
fn decode_is_deterministic_across_features() {
let (enc, mut dec) = common::codec();
let coded = enc.encode(b"deterministic hard").unwrap();
assert_eq!(
dec.decode_block(&coded).unwrap(),
dec.decode_block(&coded).unwrap()
);
let enc = ViterbiEncoder::new(CodeParams::ccsds_r1_2()).unwrap();
let coded = enc.encode(b"deterministic soft").unwrap();
let llrs = coded_bits_soft(&coded);
let mut soft = CcsdsSoftDecoder::new(CodeParams::ccsds_r1_2(), 1_000).unwrap();
assert_eq!(
soft.decode(&llrs, coded.nbits / 2 - 6).unwrap(),
soft.decode(&llrs, coded.nbits / 2 - 6).unwrap()
);
let enc = ViterbiEncoder::new(CodeParams::ccsds_r1_3()).unwrap();
let raw = b"deterministic r13";
let coded = enc.encode(raw).unwrap();
let samples = coded_bits_hard(&coded);
let mut dec3 = ViterbiDecoder::<64, HardHamming>::new(CodeParams::ccsds_r1_3(), 1_000).unwrap();
assert_eq!(
dec3.decode(&samples, raw.len() * 8).unwrap(),
dec3.decode(&samples, raw.len() * 8).unwrap()
);
let params = CodeProfile::K9R1_2.params();
let enc = ViterbiEncoder::new(params.clone()).unwrap();
let coded = enc.encode(b"deterministic k9").unwrap();
let mut dec9 = ViterbiDecoder::<256, HardHamming>::new(params, 1_000).unwrap();
assert_eq!(
dec9.decode_block(&coded).unwrap(),
dec9.decode_block(&coded).unwrap()
);
}
#[test]
fn long_stream_hard_round_trip_exercises_renorm() {
let enc = ViterbiEncoder::new(CodeParams::ccsds_r1_2()).unwrap();
let raw: Vec<u8> = (0..1_500u32).map(|i| (i as u8).wrapping_mul(53)).collect(); let coded = enc.encode(&raw).unwrap();
let mut dec = CcsdsViterbiDecoder::new(CodeParams::ccsds_r1_2(), 20_000).unwrap();
assert_eq!(dec.decode_block(&coded).unwrap().bytes, raw);
}
#[test]
fn long_stream_soft_round_trip_exercises_renorm() {
let enc = ViterbiEncoder::new(CodeParams::ccsds_r1_2()).unwrap();
let raw: Vec<u8> = (0..1_500u32).map(|i| (i as u8).wrapping_mul(37)).collect(); let coded = enc.encode(&raw).unwrap();
let llrs = coded_bits_soft(&coded);
let mut dec = CcsdsSoftDecoder::new(CodeParams::ccsds_r1_2(), 20_000).unwrap();
assert_eq!(dec.decode(&llrs, raw.len() * 8).unwrap().bytes, raw);
}
#[test]
fn k9_realistic_scale_round_trip_and_correction() {
let params = CodeProfile::K9R1_2.params();
let enc = ViterbiEncoder::new(params.clone()).unwrap();
let mut dec = ViterbiDecoder::<256, HardHamming>::new(params, 20_000).unwrap();
let raw: Vec<u8> = (0..1_250u32)
.map(|i| (i as u8).wrapping_mul(29).wrapping_add(7))
.collect();
let coded = enc.encode(&raw).unwrap();
assert_eq!(dec.decode_block(&coded).unwrap().bytes, raw);
let mut corrupted = coded.clone();
flip_bit(&mut corrupted.bytes, 4_321);
assert_eq!(
dec.decode_block(&corrupted).unwrap().bytes,
raw,
"a single K=9 coded-bit error must recover at 10k-bit scale"
);
}
#[test]
fn k9_with_puncturing_round_trips_with_m_times_n_tail() {
let params = CodeProfile::K9R1_2.params();
let enc = ViterbiEncoder::new(params.clone()).unwrap();
let matrix = PunctureMatrix::ccsds_rate(2, PuncturedRate::R2_3).unwrap();
let punc = Puncturer::new(matrix.clone(), ¶ms).unwrap();
let dp = DePuncturer::<HardHamming>::new(matrix.clone(), ¶ms).unwrap();
let raw = b"pk9"; let nbits = raw.len() * 8;
let coded = enc.encode(raw).unwrap();
assert_eq!(
coded.nbits,
(nbits + 8) * 2,
"K=9 encode tail must use m = 8"
);
let punctured = punc.puncture(&coded, nbits).unwrap();
let kept_body = (nbits / matrix.period()) * matrix.kept_per_period();
assert_eq!(
punctured.nbits,
kept_body + 8 * 2,
"punctured tail must be m·n = 16 (K=9)"
);
assert_eq!(dp.expected_punctured_len(nbits), Some(kept_body + 8 * 2));
let full = dp.depuncture(&coded_bits_hard(&punctured), nbits).unwrap();
let mut dec = ViterbiDecoder::<256, HardHamming>::new(params, 4096).unwrap();
assert_eq!(dec.decode(&full, nbits).unwrap().bytes, raw.to_vec());
}
#[test]
fn punctured_soft_round_trip_clean_channel() {
let params = CodeParams::ccsds_r1_2();
let enc = ViterbiEncoder::new(params.clone()).unwrap();
let matrix = PunctureMatrix::ccsds_rate(2, PuncturedRate::R3_4).unwrap();
let punc = Puncturer::new(matrix.clone(), ¶ms).unwrap();
let dp = DePuncturer::<SoftLlr>::new(matrix, ¶ms).unwrap();
let raw = b"punctured soft ok"; let nbits = raw.len() * 8;
let coded = enc.encode(raw).unwrap();
let punctured = punc.puncture(&coded, nbits).unwrap();
let on_air = coded_bits_soft(&punctured);
let full = dp.depuncture(&on_air, nbits).unwrap();
let mut dec = ViterbiDecoder::<64, SoftLlr>::new(params, 4096).unwrap();
assert_eq!(dec.decode(&full, nbits).unwrap().bytes, raw.to_vec());
}