use crate::crc::{Crc16Stream, Crc32Stream};
use crate::error::{try_vec, Error, ParseError};
use std::io::{self, Read, Seek, SeekFrom, Write};
pub const MAGIC: &[u8; 4] = b"CBIN";
const HEAD_LEN: usize = 0x18;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Generation {
V0,
V1,
}
impl Generation {
pub fn body_start(self) -> u64 {
match self {
Generation::V0 => 0x18,
Generation::V1 => 0x2c,
}
}
pub(crate) fn trailer_len(self) -> u64 {
match self {
Generation::V0 => 2,
Generation::V1 => 0,
}
}
}
pub type Tag = [u8; 4];
#[track_caller]
fn tag(format: &str) -> Tag {
format
.as_bytes()
.try_into()
.unwrap_or_else(|_| panic!("format tag {format:?} is not 4 bytes — bug in format module"))
}
fn tag_str(tag: &Tag) -> String {
String::from_utf8_lossy(tag).into_owned()
}
#[derive(Clone, PartialEq, Eq)]
pub struct Header {
pub generation: Generation,
pub tag: Tag,
pub location: u32,
pub aux: u32,
pub version: u32,
}
impl Header {
pub fn new(format: &str, location: (u16, u16), version: u32) -> Header {
Header {
generation: Generation::V1,
tag: tag(format),
location: (location.0 as u32) | ((location.1 as u32) << 16),
aux: 0xFFFF_FFFF,
version,
}
}
pub fn slot(&self) -> (u16, u16) {
(self.location as u16, (self.location >> 16) as u16)
}
pub fn category(&self) -> Option<u16> {
match (self.aux >> 16, self.aux as u16) {
(0, id) => Some(id),
_ => None,
}
}
pub fn set_slot(&mut self, (bank, slot): (u16, u16)) {
self.location = (bank as u32) | ((slot as u32) << 16);
}
fn head_bytes(&self) -> [u8; HEAD_LEN] {
let generation: u32 = match self.generation {
Generation::V0 => 0,
Generation::V1 => 1,
};
let mut out = [0u8; HEAD_LEN];
out[0..4].copy_from_slice(MAGIC);
out[4..8].copy_from_slice(&generation.to_le_bytes());
out[8..12].copy_from_slice(&self.tag);
out[12..16].copy_from_slice(&self.location.to_le_bytes());
out[16..20].copy_from_slice(&self.aux.to_le_bytes());
out[20..24].copy_from_slice(&self.version.to_le_bytes());
out
}
}
impl std::fmt::Debug for Header {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Header")
.field("generation", &self.generation)
.field("tag", &tag_str(&self.tag))
.field("location", &format_args!("{:#010x}", self.location))
.field("aux", &format_args!("{:#010x}", self.aux))
.field("version", &self.version)
.finish()
}
}
pub trait Body: Sized {
const LEN: Option<u64> = None;
fn read<R: Read + Seek>(r: &mut BodyReader<'_, R>, header: &Header) -> Result<Self, Error>;
fn write<W: Write + Seek>(&self, w: &mut BodyWriter<'_, W>) -> Result<(), Error>;
}
#[derive(Debug)]
pub struct Cbin<B> {
pub header: Header,
pub body: B,
}
impl<B> std::ops::Deref for Cbin<B> {
type Target = B;
fn deref(&self) -> &B {
&self.body
}
}
impl<B> std::ops::DerefMut for Cbin<B> {
fn deref_mut(&mut self) -> &mut B {
&mut self.body
}
}
enum Hash {
V0(Crc16Stream<'static>),
V1(Crc32Stream<'static>),
}
impl Hash {
fn new(generation: Generation) -> Hash {
match generation {
Generation::V0 => Hash::V0(Crc16Stream::new()),
Generation::V1 => Hash::V1(Crc32Stream::new()),
}
}
fn update(&mut self, bytes: &[u8]) {
match self {
Hash::V0(h) => h.update(bytes),
Hash::V1(h) => h.update(bytes),
}
}
}
fn le_u32(bytes: &[u8], at: usize) -> u32 {
u32::from_le_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]])
}
fn tag_from_slice(bytes: &[u8]) -> Tag {
[bytes[0], bytes[1], bytes[2], bytes[3]]
}
pub(crate) fn read_header(r: &mut impl Read) -> Result<(Header, u32), Error> {
let mut head = [0u8; HEAD_LEN];
r.read_exact(&mut head)?;
if &head[0..4] != MAGIC {
return Err(ParseError::UnknownFileType(tag_str(&tag_from_slice(&head[0..4]))).into());
}
let le = |at: usize| le_u32(&head, at);
let generation = match le(4) {
0 => Generation::V0,
1 => Generation::V1,
other => {
return Err(ParseError::UnknownFormat(format!("CBIN header type {other}")).into());
}
};
let header = Header {
generation,
tag: tag_from_slice(&head[8..12]),
location: le(12),
aux: le(16),
version: le(20),
};
let mut stored_crc32 = 0;
if generation == Generation::V1 {
let mut rest = [0u8; 20];
r.read_exact(&mut rest)?;
stored_crc32 = le_u32(&rest, 0);
if rest[4..] != [0u8; 16] {
return Err(ParseError::AssertFail(
"nonzero bytes in the 0x1c..0x2c header pad".into(),
)
.into());
}
}
Ok((header, stored_crc32))
}
fn stream_end(r: &mut impl Seek) -> io::Result<u64> {
let pos = r.stream_position()?;
let end = r.seek(SeekFrom::End(0))?;
r.seek(SeekFrom::Start(pos))?;
Ok(end)
}
pub fn read<B: Body>(r: &mut (impl Read + Seek), format: &'static str) -> Result<Cbin<B>, Error> {
read_inner(r, Some(format))
}
pub fn read_raw(r: &mut (impl Read + Seek)) -> Result<Cbin<RawBody>, Error> {
read_inner(r, None)
}
fn read_inner<B: Body>(
r: &mut (impl Read + Seek),
format: Option<&'static str>,
) -> Result<Cbin<B>, Error> {
let start = r.stream_position()?;
let (header, stored_crc32) = read_header(r)?;
if let Some(expected) = format {
if header.tag != tag(expected) {
return Err(ParseError::WrongFormat {
expected,
got: tag_str(&header.tag),
}
.into());
}
}
let format = format.map_or_else(|| tag_str(&header.tag), str::to_string);
let end = stream_end(r)?;
let overhead = header.generation.body_start() + header.generation.trailer_len();
if end < start + overhead {
return Err(ParseError::AssertFail(format!(
"{format}: {} bytes is shorter than the {overhead}-byte container",
end - start,
))
.into());
}
let body_start = start + header.generation.body_start();
let body_len = end - body_start - header.generation.trailer_len();
if let Some(expected) = B::LEN {
if body_len != expected {
return Err(ParseError::WrongBodyLength {
format,
got: body_len,
expected,
}
.into());
}
}
let mut hash = Hash::new(header.generation);
if header.generation == Generation::V0 {
hash.update(&header.head_bytes());
}
let mut reader = BodyReader {
inner: r,
start: body_start,
len: body_len,
pos: 0,
hashed: 0,
hash,
};
let body = B::read(&mut reader, &header)?;
reader.verify(stored_crc32, &format)?;
Ok(Cbin { header, body })
}
impl<B: Body> Cbin<B> {
pub fn write_to(&self, w: &mut (impl Write + Seek)) -> Result<(), Error> {
let start = w.stream_position()?;
let head = self.header.head_bytes();
let mut hash = Hash::new(self.header.generation);
w.write_all(&head)?;
match self.header.generation {
Generation::V1 => w.write_all(&[0u8; 20])?,
Generation::V0 => hash.update(&head),
}
let body_start = start + self.header.generation.body_start();
let mut writer = BodyWriter {
inner: w,
pos: 0,
hash,
};
self.body.write(&mut writer)?;
let BodyWriter {
pos: written, hash, ..
} = writer;
if let Some(expected) = B::LEN {
if written != expected {
return Err(ParseError::WrongBodyLength {
format: tag_str(&self.header.tag),
got: written,
expected,
}
.into());
}
}
match hash {
Hash::V1(h) => {
w.seek(SeekFrom::Start(start + 0x18))?;
w.write_all(&h.value().to_le_bytes())?;
w.seek(SeekFrom::Start(body_start + written))?;
}
Hash::V0(h) => w.write_all(&h.value().to_le_bytes())?,
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawBody(pub Vec<u8>);
impl Body for RawBody {
fn read<R: Read + Seek>(r: &mut BodyReader<'_, R>, _: &Header) -> Result<RawBody, Error> {
let len = usize::try_from(r.len()).map_err(|_| ParseError::OutOfBounds {
value: format!("{} body bytes", r.len()),
bound: "a length that fits this platform's usize".into(),
})?;
let mut bytes = try_vec(len)?;
r.read_exact(&mut bytes)?;
Ok(RawBody(bytes))
}
fn write<W: Write + Seek>(&self, w: &mut BodyWriter<'_, W>) -> Result<(), Error> {
w.write_all(&self.0)?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct Info {
pub header: Header,
pub body_len: u64,
pub checksum_ok: bool,
}
pub fn inspect(r: &mut (impl Read + Seek)) -> Result<Info, Error> {
let start = r.stream_position()?;
let (header, stored_crc32) = read_header(r)?;
let end = stream_end(r)?;
let overhead = header.generation.body_start() + header.generation.trailer_len();
if end < start + overhead {
return Err(ParseError::AssertFail(format!(
"{}: {} bytes is shorter than the {overhead}-byte container",
tag_str(&header.tag),
end - start,
))
.into());
}
let body_len = end - start - overhead;
let mut hash = Hash::new(header.generation);
if header.generation == Generation::V0 {
hash.update(&header.head_bytes());
}
let mut remaining = body_len;
let mut scratch = [0u8; 8192];
while remaining > 0 {
let take = remaining.min(scratch.len() as u64) as usize;
r.read_exact(&mut scratch[..take])?;
hash.update(&scratch[..take]);
remaining -= take as u64;
}
let checksum_ok = match hash {
Hash::V1(h) => h.value() == stored_crc32,
Hash::V0(h) => {
let mut trailer = [0u8; 2];
r.read_exact(&mut trailer)?;
h.value() == u16::from_le_bytes(trailer)
}
};
Ok(Info {
header,
body_len,
checksum_ok,
})
}
pub struct BodyReader<'a, R: Read + Seek> {
inner: &'a mut R,
start: u64,
len: u64,
pos: u64,
hashed: u64,
hash: Hash,
}
impl<R: Read + Seek> BodyReader<'_, R> {
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> u64 {
self.len
}
pub fn remaining(&self) -> u64 {
self.len - self.pos
}
fn verify(mut self, stored_crc32: u32, format: &str) -> Result<(), Error> {
self.seek(SeekFrom::Start(self.len))?;
match self.hash {
Hash::V1(h) => {
let computed = h.value();
if computed != stored_crc32 {
return Err(ParseError::AssertFail(format!(
"{format}: stored checksum {stored_crc32:#010x} does not match the \
body's {computed:#010x}"
))
.into());
}
}
Hash::V0(h) => {
let mut trailer = [0u8; 2];
self.inner.read_exact(&mut trailer)?;
let stored = u16::from_le_bytes(trailer);
let computed = h.value();
if computed != stored {
return Err(ParseError::AssertFail(format!(
"{format}: stored checksum {stored:#06x} does not match the \
file's {computed:#06x}"
))
.into());
}
}
}
Ok(())
}
}
impl<R: Read + Seek> Read for BodyReader<'_, R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let remaining = self.len - self.pos;
if remaining == 0 || buf.is_empty() {
return Ok(0);
}
let want = (buf.len() as u64).min(remaining) as usize;
let n = self.inner.read(&mut buf[..want])?;
let end = self.pos + n as u64;
if end > self.hashed {
let from = (self.hashed - self.pos) as usize;
self.hash.update(&buf[from..n]);
self.hashed = end;
}
self.pos = end;
Ok(n)
}
}
impl<R: Read + Seek> Seek for BodyReader<'_, R> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let target = match pos {
SeekFrom::Start(p) => p as i128,
SeekFrom::Current(d) => self.pos as i128 + d as i128,
SeekFrom::End(d) => self.len as i128 + d as i128,
};
if target < 0 || target > self.len as i128 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("seek to {target} outside the {}-byte body", self.len),
));
}
let target = target as u64;
if target <= self.hashed {
self.inner.seek(SeekFrom::Start(self.start + target))?;
} else {
self.inner.seek(SeekFrom::Start(self.start + self.hashed))?;
let mut scratch = [0u8; 8192];
while self.hashed < target {
let take = (target - self.hashed).min(scratch.len() as u64) as usize;
self.inner.read_exact(&mut scratch[..take])?;
self.hash.update(&scratch[..take]);
self.hashed += take as u64;
}
}
self.pos = target;
Ok(target)
}
}
pub struct BodyWriter<'a, W: Write + Seek> {
inner: &'a mut W,
pos: u64,
hash: Hash,
}
impl<W: Write + Seek> Write for BodyWriter<'_, W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let n = self.inner.write(buf)?;
self.hash.update(&buf[..n]);
self.pos += n as u64;
Ok(n)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crc::{crc16, crc32};
use std::io::Cursor;
#[test]
fn category_reads_only_the_program_shape() {
let mut h = Header::new("ne5p", (0, 0), 4);
assert_eq!(h.category(), None, "0xFFFFFFFF is no category");
h.aux = 0x0000_0017;
assert_eq!(h.category(), Some(0x17));
h.aux = 0x0000_0000;
assert_eq!(h.category(), Some(0), "zero is a value, not unset");
h.aux = 0x000a_0004; assert_eq!(
h.category(),
None,
"a set high u16 is not the category shape"
);
}
#[derive(Debug)]
struct Five([u8; 5]);
impl Body for Five {
const LEN: Option<u64> = Some(5);
fn read<R: Read + Seek>(r: &mut BodyReader<'_, R>, _: &Header) -> Result<Five, Error> {
let mut b = [0u8; 5];
r.read_exact(&mut b)?;
Ok(Five(b))
}
fn write<W: Write + Seek>(&self, w: &mut BodyWriter<'_, W>) -> Result<(), Error> {
w.write_all(&self.0)?;
Ok(())
}
}
fn v1_file(body: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(b"CBIN");
out.extend_from_slice(&1u32.to_le_bytes());
out.extend_from_slice(b"test");
out.extend_from_slice(&0x0007_0003u32.to_le_bytes());
out.extend_from_slice(&u32::MAX.to_le_bytes());
out.extend_from_slice(&4u32.to_le_bytes());
out.extend_from_slice(&crc32(body).to_le_bytes());
out.extend_from_slice(&[0u8; 16]);
out.extend_from_slice(body);
out
}
fn v0_file(body: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(b"CBIN");
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(b"test");
out.extend_from_slice(&0x0007_0003u32.to_le_bytes());
out.extend_from_slice(&u32::MAX.to_le_bytes());
out.extend_from_slice(&4u32.to_le_bytes());
out.extend_from_slice(body);
let crc = crc16(&out);
out.extend_from_slice(&crc.to_le_bytes());
out
}
#[test]
fn both_generations_round_trip_and_differ_by_18_bytes() {
let body = [0xaa, 0xbb, 0xcc, 0xdd, 0xee];
for bytes in [v1_file(&body), v0_file(&body)] {
let file: Cbin<Five> = read(&mut Cursor::new(&bytes), "test").unwrap();
assert_eq!(file.body.0, body);
assert_eq!(file.header.slot(), (3, 7));
let mut out = Cursor::new(Vec::new());
file.write_to(&mut out).unwrap();
assert_eq!(out.into_inner(), bytes, "round trip changed the bytes");
}
assert_eq!(v1_file(&body).len() - v0_file(&body).len(), 18);
}
#[test]
fn a_corrupted_byte_fails_either_checksum() {
let body = [1, 2, 3, 4, 5];
for mut bytes in [v1_file(&body), v0_file(&body)] {
let at = bytes.len() - 3;
bytes[at] ^= 0xff;
assert!(
read::<Five>(&mut Cursor::new(&bytes), "test").is_err(),
"a corrupted body must not verify"
);
}
}
#[test]
fn the_v0_checksum_covers_the_header() {
let mut bytes = v0_file(&[1, 2, 3, 4, 5]);
bytes[0x0c] ^= 0xff;
assert!(read::<Five>(&mut Cursor::new(&bytes), "test").is_err());
let mut bytes = v1_file(&[1, 2, 3, 4, 5]);
bytes[0x0c] ^= 0xff;
assert!(read::<Five>(&mut Cursor::new(&bytes), "test").is_ok());
}
#[test]
fn the_wrong_tag_is_refused_by_name() {
let bytes = v1_file(&[1, 2, 3, 4, 5]);
let err = read::<Five>(&mut Cursor::new(&bytes), "ne5p").unwrap_err();
assert!(
matches!(
err,
Error::Parse(ParseError::WrongFormat {
expected: "ne5p",
..
})
),
"refused for the wrong reason: {err}",
);
}
#[test]
fn the_wrong_length_is_refused_before_the_body_decodes() {
let bytes = v1_file(&[1, 2, 3]);
let err = read::<Five>(&mut Cursor::new(&bytes), "test").unwrap_err();
assert!(
matches!(
err,
Error::Parse(ParseError::WrongBodyLength {
got: 3,
expected: 5,
..
})
),
"refused for the wrong reason: {err}",
);
}
#[test]
fn an_unread_tail_is_still_verified() {
struct TwoOfFive;
impl Body for TwoOfFive {
fn read<R: Read + Seek>(
r: &mut BodyReader<'_, R>,
_: &Header,
) -> Result<TwoOfFive, Error> {
let mut b = [0u8; 2];
r.read_exact(&mut b)?;
Ok(TwoOfFive)
}
fn write<W: Write + Seek>(&self, _: &mut BodyWriter<'_, W>) -> Result<(), Error> {
Ok(())
}
}
let mut bytes = v1_file(&[1, 2, 3, 4, 5]);
assert!(read::<TwoOfFive>(&mut Cursor::new(&bytes), "test").is_ok());
*bytes.last_mut().unwrap() ^= 0xff;
assert!(
read::<TwoOfFive>(&mut Cursor::new(&bytes), "test").is_err(),
"a corrupt byte the body never read must still fail verification",
);
}
#[test]
fn seeking_bodies_keep_the_checksum_exact() {
struct Skipper;
impl Body for Skipper {
fn read<R: Read + Seek>(
r: &mut BodyReader<'_, R>,
_: &Header,
) -> Result<Skipper, Error> {
r.seek(SeekFrom::Start(4))?; let mut b = [0u8; 1];
r.read_exact(&mut b)?;
r.seek(SeekFrom::Start(0))?; r.read_exact(&mut b)?; Ok(Skipper)
}
fn write<W: Write + Seek>(&self, _: &mut BodyWriter<'_, W>) -> Result<(), Error> {
Ok(())
}
}
let bytes = v1_file(&[9, 8, 7, 6, 5]);
assert!(read::<Skipper>(&mut Cursor::new(&bytes), "test").is_ok());
}
#[test]
fn inspect_reports_both_generations_without_a_body() {
for (bytes, generation) in [
(v1_file(&[1, 2, 3]), Generation::V1),
(v0_file(&[1, 2, 3]), Generation::V0),
] {
let info = inspect(&mut Cursor::new(&bytes)).unwrap();
assert_eq!(info.header.generation, generation);
assert_eq!(info.body_len, 3);
assert!(info.checksum_ok);
let mut corrupt = bytes.clone();
let at = corrupt.len() - 3;
corrupt[at] ^= 0xff;
let info = inspect(&mut Cursor::new(&corrupt)).unwrap();
assert!(!info.checksum_ok, "inspect reports, it does not refuse");
}
}
#[test]
fn a_header_type_that_is_neither_generation_is_refused() {
let mut bytes = v1_file(&[1, 2, 3, 4, 5]);
bytes[4..8].copy_from_slice(&2u32.to_le_bytes());
let err = read::<Five>(&mut Cursor::new(&bytes), "test").unwrap_err();
assert!(
matches!(err, Error::Parse(ParseError::UnknownFormat(ref what)) if what.contains("type 2")),
"refused for the wrong reason: {err}",
);
}
#[test]
fn a_nonzero_header_pad_is_refused() {
for at in 0x1c..0x2c {
let mut bytes = v1_file(&[1, 2, 3, 4, 5]);
bytes[at] = 0xff;
let err = read::<Five>(&mut Cursor::new(&bytes), "test")
.unwrap_err()
.to_string();
assert!(err.contains("header pad"), "byte {at:#x}: {err}");
}
}
#[test]
fn a_file_shorter_than_its_container_is_refused() {
let short = &v0_file(&[1, 2, 3, 4, 5])[..HEAD_LEN + 1];
let err = read::<Five>(&mut Cursor::new(short), "test").unwrap_err();
assert!(
matches!(&err, Error::Parse(ParseError::AssertFail(why))
if why.contains("shorter than the 26-byte container")),
"refused for the wrong reason: {err}",
);
assert!(inspect(&mut Cursor::new(short)).is_err());
let truncated = &v1_file(&[1, 2, 3, 4, 5])[..0x2b];
assert!(read::<Five>(&mut Cursor::new(truncated), "test").is_err());
}
#[test]
fn raw_bodies_round_trip_any_tag() {
let bytes = v0_file(&[1, 2, 3, 4, 5, 6, 7]);
let file = read_raw(&mut Cursor::new(&bytes)).unwrap();
assert_eq!(file.body.0, [1, 2, 3, 4, 5, 6, 7]);
let mut out = Cursor::new(Vec::new());
file.write_to(&mut out).unwrap();
assert_eq!(out.into_inner(), bytes);
}
}