use super::Limits;
use super::PortableError;
pub const MAGIC: [u8; 4] = *b"RZFG";
pub const CONTAINER_VERSION: u32 = 1;
pub const ALIGN: usize = 8;
pub const TAG_JSON: [u8; 4] = *b"JSON";
pub const TAG_BIN: [u8; 4] = *b"BIN ";
pub const TAG_PSTR: [u8; 4] = *b"PSTR";
pub const TAG_FONT: [u8; 4] = *b"FONT";
pub const TAG_WASM: [u8; 4] = *b"WASM";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChunkRef {
pub tag: [u8; 4],
pub offset: usize,
pub len: usize,
}
impl ChunkRef {
#[must_use]
pub fn name(&self) -> String {
tag_name(self.tag)
}
}
#[must_use]
pub fn write(chunks: &[([u8; 4], &[u8])]) -> Vec<u8> {
let total: usize = chunks.iter().map(|(_, p)| p.len() + 8 + ALIGN).sum();
let mut out = Vec::with_capacity(12 + total);
out.extend_from_slice(&MAGIC);
out.extend_from_slice(&CONTAINER_VERSION.to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes()); for (tag, payload) in chunks {
if payload.is_empty() && *tag == TAG_BIN {
continue;
}
let len = u32::try_from(payload.len()).expect("portable chunk exceeds 4 GiB");
out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(tag);
out.extend_from_slice(payload);
while out.len() % ALIGN != 0 {
out.push(0);
}
}
let total = u32::try_from(out.len()).expect("portable figure exceeds 4 GiB");
out[8..12].copy_from_slice(&total.to_le_bytes());
out
}
pub fn directory(bytes: &[u8], limits: &Limits) -> Result<Vec<ChunkRef>, PortableError> {
if bytes.len() > limits.max_total_bytes {
return Err(PortableError::Budget(format!(
"artifact is {} bytes, over the {} byte limit",
bytes.len(),
limits.max_total_bytes
)));
}
let header = bytes
.get(..12)
.ok_or_else(|| malformed("shorter than the 12-byte header"))?;
if header[..4] != MAGIC {
return Err(malformed("bad magic (not an RZFG container)"));
}
let version = u32::from_le_bytes(header[4..8].try_into().expect("4 bytes"));
if version != CONTAINER_VERSION {
return Err(PortableError::Malformed(format!(
"container version {version}; this build reads version {CONTAINER_VERSION}"
)));
}
let declared = u32::from_le_bytes(header[8..12].try_into().expect("4 bytes")) as usize;
if declared != bytes.len() {
return Err(PortableError::Malformed(format!(
"declared length {declared} but got {} bytes",
bytes.len()
)));
}
let mut out: Vec<ChunkRef> = Vec::new();
let mut pos = 12;
while pos < bytes.len() {
if out.len() >= limits.max_chunks {
return Err(PortableError::Budget(format!(
"more than {} chunks",
limits.max_chunks
)));
}
let head = bytes
.get(pos..pos + 8)
.ok_or_else(|| malformed("truncated chunk header"))?;
let len = u32::from_le_bytes(head[..4].try_into().expect("4 bytes")) as usize;
let tag: [u8; 4] = head[4..8].try_into().expect("4 bytes");
let end = pos
.checked_add(8)
.and_then(|s| s.checked_add(len))
.ok_or_else(|| malformed("chunk extent overflows"))?;
if end > bytes.len() {
return Err(malformed("truncated chunk payload"));
}
if out.iter().any(|c| c.tag == tag) {
return Err(PortableError::Malformed(format!(
"duplicate {:?} chunk",
tag_name(tag)
)));
}
out.push(ChunkRef {
tag,
offset: pos + 8,
len,
});
pos = end;
while pos % ALIGN != 0 {
match bytes.get(pos) {
Some(0) => pos += 1,
Some(_) => return Err(malformed("non-zero chunk padding")),
None => break,
}
}
}
if !out.iter().any(|c| c.tag == TAG_JSON) {
return Err(malformed("missing JSON chunk"));
}
Ok(out)
}
#[must_use]
pub fn chunk<'a>(bytes: &'a [u8], dir: &[ChunkRef], tag: [u8; 4]) -> Option<&'a [u8]> {
dir.iter()
.find(|c| c.tag == tag)
.map(|c| &bytes[c.offset..c.offset + c.len])
}
fn malformed(msg: &str) -> PortableError {
PortableError::Malformed(msg.to_string())
}
#[must_use]
pub fn tag_name(tag: [u8; 4]) -> String {
tag.iter()
.map(|&b| {
if b.is_ascii_graphic() || b == b' ' {
char::from(b)
} else {
'?'
}
})
.collect()
}