use crate::consts::MAX_CHUNKS;
use crate::error::{Error, Result};
const TYPE_SINGLE: u8 = 0x00;
const TYPE_CHUNKED: u8 = 0x01;
pub const SINGLE_HEADER_SYMBOLS: usize = 2;
pub const CHUNKED_HEADER_SYMBOLS: usize = 8;
pub const MAX_CHUNK_SET_ID: u32 = (1 << 20) - 1;
pub const VERSION_V0_1: u8 = 0x00;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StringLayerHeader {
SingleString {
version: u8,
},
Chunked {
version: u8,
chunk_set_id: u32,
total_chunks: u8,
chunk_index: u8,
},
}
impl StringLayerHeader {
pub fn to_5bit_symbols(self) -> Vec<u8> {
match self {
StringLayerHeader::SingleString { version } => {
vec![version & 0x1F, TYPE_SINGLE]
}
StringLayerHeader::Chunked {
version,
chunk_set_id,
total_chunks,
chunk_index,
} => {
let csid = chunk_set_id & MAX_CHUNK_SET_ID;
let total_chunks_wire = (total_chunks - 1) & 0x1F;
vec![
version & 0x1F,
TYPE_CHUNKED,
((csid >> 15) & 0x1F) as u8,
((csid >> 10) & 0x1F) as u8,
((csid >> 5) & 0x1F) as u8,
(csid & 0x1F) as u8,
total_chunks_wire,
chunk_index & 0x1F,
]
}
}
}
pub fn from_5bit_symbols(symbols: &[u8]) -> Result<(Self, usize)> {
if symbols.len() < SINGLE_HEADER_SYMBOLS {
return Err(Error::UnexpectedEnd);
}
let version = symbols[0] & 0x1F;
if version != VERSION_V0_1 {
return Err(Error::UnsupportedVersion(version));
}
let type_byte = symbols[1] & 0x1F;
match type_byte {
TYPE_SINGLE => Ok((
StringLayerHeader::SingleString { version },
SINGLE_HEADER_SYMBOLS,
)),
TYPE_CHUNKED => {
if symbols.len() < CHUNKED_HEADER_SYMBOLS {
return Err(Error::UnexpectedEnd);
}
let csid: u32 = ((symbols[2] as u32 & 0x1F) << 15)
| ((symbols[3] as u32 & 0x1F) << 10)
| ((symbols[4] as u32 & 0x1F) << 5)
| (symbols[5] as u32 & 0x1F);
let total_chunks = (symbols[6] & 0x1F) + 1;
let chunk_index = symbols[7] & 0x1F;
if total_chunks == 0 || total_chunks > MAX_CHUNKS {
return Err(Error::ChunkedHeaderMalformed(format!(
"total_chunks = {total_chunks} (must be in 1..={MAX_CHUNKS})"
)));
}
if chunk_index >= total_chunks {
return Err(Error::ChunkedHeaderMalformed(format!(
"chunk_index = {chunk_index} >= total_chunks = {total_chunks}"
)));
}
Ok((
StringLayerHeader::Chunked {
version,
chunk_set_id: csid,
total_chunks,
chunk_index,
},
CHUNKED_HEADER_SYMBOLS,
))
}
other => Err(Error::UnsupportedCardType(other)),
}
}
pub fn is_chunked(self) -> bool {
matches!(self, StringLayerHeader::Chunked { .. })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_string_round_trip() {
let h = StringLayerHeader::SingleString { version: 0 };
let symbols = h.to_5bit_symbols();
assert_eq!(symbols.len(), SINGLE_HEADER_SYMBOLS);
let (parsed, consumed) = StringLayerHeader::from_5bit_symbols(&symbols).unwrap();
assert_eq!(parsed, h);
assert_eq!(consumed, SINGLE_HEADER_SYMBOLS);
}
#[test]
fn chunked_round_trip() {
let h = StringLayerHeader::Chunked {
version: 0,
chunk_set_id: 0xABCDE,
total_chunks: 5,
chunk_index: 3,
};
let symbols = h.to_5bit_symbols();
assert_eq!(symbols.len(), CHUNKED_HEADER_SYMBOLS);
let (parsed, consumed) = StringLayerHeader::from_5bit_symbols(&symbols).unwrap();
assert_eq!(parsed, h);
assert_eq!(consumed, CHUNKED_HEADER_SYMBOLS);
}
#[test]
fn chunked_round_trip_max_csid() {
let h = StringLayerHeader::Chunked {
version: 0,
chunk_set_id: MAX_CHUNK_SET_ID,
total_chunks: MAX_CHUNKS,
chunk_index: MAX_CHUNKS - 1,
};
let (parsed, _) = StringLayerHeader::from_5bit_symbols(&h.to_5bit_symbols()).unwrap();
assert_eq!(parsed, h);
}
#[test]
fn chunked_round_trip_zero_csid() {
let h = StringLayerHeader::Chunked {
version: 0,
chunk_set_id: 0,
total_chunks: 1,
chunk_index: 0,
};
let (parsed, _) = StringLayerHeader::from_5bit_symbols(&h.to_5bit_symbols()).unwrap();
assert_eq!(parsed, h);
}
#[test]
fn parse_rejects_truncated_input() {
assert!(matches!(
StringLayerHeader::from_5bit_symbols(&[]),
Err(Error::UnexpectedEnd)
));
assert!(matches!(
StringLayerHeader::from_5bit_symbols(&[0]),
Err(Error::UnexpectedEnd)
));
let symbols = vec![0u8, TYPE_CHUNKED, 0, 0, 0];
assert!(matches!(
StringLayerHeader::from_5bit_symbols(&symbols),
Err(Error::UnexpectedEnd)
));
}
#[test]
fn parse_rejects_unsupported_version() {
let symbols = vec![1u8, TYPE_SINGLE];
assert!(matches!(
StringLayerHeader::from_5bit_symbols(&symbols),
Err(Error::UnsupportedVersion(1))
));
}
#[test]
fn parse_rejects_reserved_card_type() {
for ct in 0x02u8..=0x1F {
let symbols = vec![0u8, ct];
let r = StringLayerHeader::from_5bit_symbols(&symbols);
assert!(
matches!(r, Err(Error::UnsupportedCardType(c)) if c == ct),
"card type 0x{ct:02x} not rejected"
);
}
}
#[test]
fn wire_total_chunks_zero_decodes_to_one() {
let h = StringLayerHeader::Chunked {
version: 0,
chunk_set_id: 0,
total_chunks: 1,
chunk_index: 0,
};
let symbols = h.to_5bit_symbols();
assert_eq!(symbols[6], 0, "wire encoding of total_chunks=1 must be 0");
let (parsed, _) = StringLayerHeader::from_5bit_symbols(&symbols).unwrap();
assert_eq!(parsed, h);
}
#[test]
fn parse_rejects_chunk_index_at_or_above_total_chunks() {
let h = StringLayerHeader::Chunked {
version: 0,
chunk_set_id: 0,
total_chunks: 3,
chunk_index: 0,
};
let mut symbols = h.to_5bit_symbols();
symbols[7] = 3; assert!(matches!(
StringLayerHeader::from_5bit_symbols(&symbols),
Err(Error::ChunkedHeaderMalformed(_))
));
}
#[test]
fn is_chunked_discriminator() {
assert!(!StringLayerHeader::SingleString { version: 0 }.is_chunked());
assert!(
StringLayerHeader::Chunked {
version: 0,
chunk_set_id: 0,
total_chunks: 1,
chunk_index: 0,
}
.is_chunked()
);
}
}