use crate::error::{Error, Result};
use crate::wire::Location;
use nord_format::cbin::{self, Cbin, Header, RawBody};
use std::io::Cursor;
pub fn crc32(data: &[u8]) -> u32 {
nord_format::crc::crc32(data)
}
fn slot(at: Location) -> Result<(u16, u16)> {
let bank = u16::try_from(at.bank)
.map_err(|_| Error::InvalidArgument(format!("bank {} does not fit in CBIN", at.bank)))?;
let slot = u16::try_from(at.slot)
.map_err(|_| Error::InvalidArgument(format!("slot {} does not fit in CBIN", at.slot)))?;
Ok((bank, slot))
}
pub fn location(header: &Header) -> Location {
let (bank, slot) = header.slot();
Location {
bank: bank as u32,
slot: slot as u32,
}
}
pub fn tag(header: &Header) -> String {
String::from_utf8_lossy(&header.tag).into_owned()
}
pub fn wrap(format: &str, at: Location, version: u32, body: &[u8]) -> Result<Vec<u8>> {
if format.len() != 4 {
return Err(Error::Envelope(format!(
"format tag {format:?} is not 4 characters"
)));
}
let file = Cbin {
header: Header::new(format, slot(at)?, version),
body: RawBody(body.to_vec()),
};
let mut out = Cursor::new(Vec::new());
file.write_to(&mut out)
.map_err(|e| Error::Envelope(e.to_string()))?;
Ok(out.into_inner())
}
pub fn unwrap(file: &[u8]) -> Result<Cbin<RawBody>> {
let read =
cbin::read_raw(&mut Cursor::new(file)).map_err(|e| Error::Envelope(e.to_string()))?;
if read.body.0.is_empty() {
return Err(Error::Envelope(
"the file is a bare CBIN header with no body to send".into(),
));
}
Ok(read)
}
#[cfg(test)]
mod tests {
use super::*;
const HEADER: &str =
"4342494e010000006e65357007000d00ffffffff04000000b65d46a500000000000000000000000000000000";
const BODY: &str = "000401df06781fc60000000000000000000000000000000000000100000000000000000000400000000000000002200000000000022000400000008888000008008888000008000000000080000000000080000000000000000000800000000800800000000800020010060401020408140010000000000000";
fn hex(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
#[test]
fn wrap_rebuilds_the_original_file() {
let body = hex(BODY);
let file = [hex(HEADER), body.clone()].concat();
let built = wrap("ne5p", Location::from_user(8, 14), 4, &body).unwrap();
assert_eq!(built, file, "rebuilt header differs from the real file");
}
#[test]
fn wrap_writes_the_version_it_is_given() {
let body = hex(BODY);
for version in [0u32, 1, 4, 540] {
let file = wrap("ne5t", Location::from_user(1, 1), version, &body).unwrap();
assert_eq!(
u32::from_le_bytes(file[0x14..0x18].try_into().unwrap()),
version
);
}
}
#[test]
fn unwrap_is_the_inverse() {
let body = hex(BODY);
let file = wrap("ne5p", Location::from_user(8, 14), 4, &body).unwrap();
let got = unwrap(&file).unwrap();
assert_eq!(tag(&got.header), "ne5p");
assert_eq!(location(&got.header), Location::from_user(8, 14));
assert_eq!(got.header.version, 4);
assert_eq!(got.body.0, body);
}
#[test]
fn unwrap_rejects_a_headers_worth_of_file() {
let file = Cbin {
header: Header::new("ne5p", (0, 0), 4),
body: RawBody(Vec::new()),
};
let mut bytes = Cursor::new(Vec::new());
file.write_to(&mut bytes).unwrap();
let bytes = bytes.into_inner();
assert!(cbin::read_raw(&mut Cursor::new(&bytes)).is_ok());
assert!(unwrap(&bytes).is_err(), "an empty body has nothing to send");
}
#[test]
fn unwrap_rejects_a_corrupted_body() {
let body = hex(BODY);
let mut file = wrap("ne5p", Location::from_user(8, 14), 4, &body).unwrap();
*file.last_mut().unwrap() ^= 0xFF;
assert!(
unwrap(&file).is_err(),
"a corrupted body should fail the checksum"
);
}
#[test]
fn wrap_rejects_an_address_the_container_cannot_represent() {
let at = Location {
bank: u16::MAX as u32 + 1,
slot: 0,
};
assert!(matches!(
wrap("ne5p", at, 4, &[1]),
Err(Error::InvalidArgument(_))
));
}
}