use std::fmt;
use std::io::Cursor;
use binrw::meta::{ReadEndian, WriteEndian};
use binrw::{BinRead, BinWrite, binrw};
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct ChunkType(pub u16);
impl ChunkType {
pub const NULL: Self = Self(0x0000);
pub const STRING_POOL: Self = Self(0x0001);
pub const TABLE: Self = Self(0x0002);
pub const XML: Self = Self(0x0003);
pub const XML_START_NAMESPACE: Self = Self(0x0100);
pub const XML_END_NAMESPACE: Self = Self(0x0101);
pub const XML_START_ELEMENT: Self = Self(0x0102);
pub const XML_END_ELEMENT: Self = Self(0x0103);
pub const XML_CDATA: Self = Self(0x0104);
pub const XML_RESOURCE_MAP: Self = Self(0x0180);
pub const TABLE_PACKAGE: Self = Self(0x0200);
pub const TABLE_TYPE: Self = Self(0x0201);
pub const TABLE_TYPE_SPEC: Self = Self(0x0202);
pub const TABLE_LIBRARY: Self = Self(0x0203);
fn name(self) -> Option<&'static str> {
Some(match self {
Self::NULL => "null",
Self::STRING_POOL => "string pool",
Self::TABLE => "resource table",
Self::XML => "XML document",
Self::XML_START_NAMESPACE => "XML namespace start",
Self::XML_END_NAMESPACE => "XML namespace end",
Self::XML_START_ELEMENT => "XML element start",
Self::XML_END_ELEMENT => "XML element end",
Self::XML_CDATA => "XML text",
Self::XML_RESOURCE_MAP => "XML resource map",
Self::TABLE_PACKAGE => "package",
Self::TABLE_TYPE => "type",
Self::TABLE_TYPE_SPEC => "type spec",
Self::TABLE_LIBRARY => "library",
_ => return None,
})
}
}
impl fmt::Display for ChunkType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.name() {
Some(name) => write!(f, "{name} ({:#06x})", self.0),
None => write!(f, "unknown ({:#06x})", self.0),
}
}
}
impl fmt::Debug for ChunkType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ChunkType({:#06x})", self.0)
}
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum ChunkError {
#[error("expected a chunk but found no data")]
Missing,
#[error("chunk header at offset {offset:#x} is truncated")]
TruncatedHeader { offset: usize },
#[error("{ty} chunk at offset {offset:#x} declares header size {header_size} and total size {size}")]
InvalidSizes { ty: ChunkType, offset: usize, header_size: u16, size: u32 },
#[error("{ty} chunk at offset {offset:#x} needs {size} bytes but only {available} remain")]
Overflow { ty: ChunkType, offset: usize, size: u32, available: usize },
#[error("expected a {expected} chunk, found {found}")]
UnexpectedType { expected: ChunkType, found: ChunkType },
#[error("{len} bytes of trailing data follow the root chunk")]
TrailingData { len: usize },
#[error("{ty} chunk header is {size} bytes; the format needs at least {min}")]
HeaderTooShort { ty: ChunkType, size: usize, min: usize },
#[error("chunk exceeds the format's 4 GiB size limit")]
TooLarge,
}
#[binrw]
#[brw(little)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct ChunkHeader {
pub ty: u16,
pub header_size: u16,
pub size: u32,
}
impl ChunkHeader {
pub const SIZE: usize = 8;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct Chunk<'a> {
pub ty: ChunkType,
pub bytes: &'a [u8],
pub header: &'a [u8],
pub body: &'a [u8],
}
impl<'a> Chunk<'a> {
pub fn parse_root(bytes: &'a [u8], expected: ChunkType) -> Result<Self, ChunkError> {
let chunk = Chunks::new(bytes).next().ok_or(ChunkError::Missing)??;
if chunk.ty != expected {
return Err(ChunkError::UnexpectedType { expected, found: chunk.ty });
}
match bytes.len() - chunk.bytes.len() {
0 => Ok(chunk),
len => Err(ChunkError::TrailingData { len }),
}
}
pub fn header<T>(&self) -> Result<T, ChunkError>
where
T: Header + BinRead + ReadEndian,
for<'b> T::Args<'b>: Default,
{
if self.header.len() < T::MIN_SIZE {
let base = ChunkHeader::SIZE;
return Err(ChunkError::HeaderTooShort {
ty: self.ty,
size: base + self.header.len(),
min: base + T::MIN_SIZE,
});
}
Ok(read_padded(self.header))
}
pub fn children(&self) -> Chunks<'a> {
Chunks::new(self.body)
}
pub fn expect(&self, expected: ChunkType) -> Result<(), ChunkError> {
if self.ty == expected { Ok(()) } else { Err(ChunkError::UnexpectedType { expected, found: self.ty }) }
}
}
pub(crate) trait Header {
const MIN_SIZE: usize;
}
pub(crate) fn read_padded<T>(bytes: &[u8]) -> T
where
T: BinRead + ReadEndian,
for<'b> T::Args<'b>: Default,
{
const MAX_RECORD: usize = 512;
let mut padded = [0u8; MAX_RECORD];
let len = bytes.len().min(MAX_RECORD);
padded[..len].copy_from_slice(&bytes[..len]);
T::read(&mut Cursor::new(&padded[..])).expect("fixed-size record fits in a zero-extended buffer")
}
pub(crate) struct Chunks<'a> {
data: &'a [u8],
offset: usize,
}
impl<'a> Chunks<'a> {
pub fn new(data: &'a [u8]) -> Self {
Self { data, offset: 0 }
}
fn next_chunk(&mut self) -> Result<Chunk<'a>, ChunkError> {
let offset = self.offset;
if self.data.len() < ChunkHeader::SIZE {
return Err(ChunkError::TruncatedHeader { offset });
}
let header: ChunkHeader = read_padded(self.data);
let ty = ChunkType(header.ty);
let (header_size, size) = (usize::from(header.header_size), header.size as usize);
if header_size < ChunkHeader::SIZE || size < header_size {
return Err(ChunkError::InvalidSizes { ty, offset, header_size: header.header_size, size: header.size });
}
if size > self.data.len() {
return Err(ChunkError::Overflow { ty, offset, size: header.size, available: self.data.len() });
}
let bytes = &self.data[..size];
self.data = &self.data[size..];
self.offset += size;
Ok(Chunk { ty, bytes, header: &bytes[ChunkHeader::SIZE..header_size], body: &bytes[header_size..] })
}
}
impl<'a> Iterator for Chunks<'a> {
type Item = Result<Chunk<'a>, ChunkError>;
fn next(&mut self) -> Option<Self::Item> {
if self.data.is_empty() {
return None;
}
let chunk = self.next_chunk();
if chunk.is_err() {
self.data = &[];
}
Some(chunk)
}
}
pub(crate) trait Put {
fn put_u16(&mut self, value: u16);
fn put_u32(&mut self, value: u32);
fn put<T>(&mut self, value: &T)
where
T: BinWrite + WriteEndian,
for<'b> T::Args<'b>: Default;
fn align4(&mut self);
}
impl Put for Vec<u8> {
fn put_u16(&mut self, value: u16) {
self.extend_from_slice(&value.to_le_bytes());
}
fn put_u32(&mut self, value: u32) {
self.extend_from_slice(&value.to_le_bytes());
}
fn put<T>(&mut self, value: &T)
where
T: BinWrite + WriteEndian,
for<'b> T::Args<'b>: Default,
{
let mut cursor = Cursor::new(self);
cursor.set_position(cursor.get_ref().len() as u64);
value.write(&mut cursor).expect("plain records always serialize into a Vec");
}
fn align4(&mut self) {
self.resize(self.len().next_multiple_of(4), 0);
}
}
pub(crate) fn write_chunk<H, E>(
out: &mut Vec<u8>,
ty: ChunkType,
header: &H,
body: impl FnOnce(&mut Vec<u8>) -> Result<(), E>,
) -> Result<(), E>
where
H: BinWrite + WriteEndian,
for<'b> H::Args<'b>: Default,
E: From<ChunkError>,
{
let start = out.len();
out.put(&ChunkHeader { ty: ty.0, header_size: 0, size: 0 });
out.put(header);
let header_size = u16::try_from(out.len() - start).map_err(|_| ChunkError::TooLarge)?;
body(out)?;
let size = u32::try_from(out.len() - start).map_err(|_| ChunkError::TooLarge)?;
out[start + 2..start + 4].copy_from_slice(&header_size.to_le_bytes());
out[start + 4..start + 8].copy_from_slice(&size.to_le_bytes());
Ok(())
}
#[binrw]
#[brw(little)]
pub(crate) struct NoHeader;
impl Header for NoHeader {
const MIN_SIZE: usize = 0;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_overlapping_or_truncated_chunks() {
let too_small = [0x02, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x00, 0x00];
assert_eq!(
Chunks::new(&too_small).next(),
Some(Err(ChunkError::InvalidSizes { ty: ChunkType::TABLE, offset: 0, header_size: 12, size: 8 }))
);
let too_big = [0x02, 0x00, 0x08, 0x00, 0xff, 0x00, 0x00, 0x00];
assert_eq!(
Chunks::new(&too_big).next(),
Some(Err(ChunkError::Overflow { ty: ChunkType::TABLE, offset: 0, size: 255, available: 8 }))
);
assert_eq!(Chunks::new(&[0x02, 0x00]).next(), Some(Err(ChunkError::TruncatedHeader { offset: 0 })));
}
#[test]
fn writes_nested_chunks_with_patched_sizes() {
let mut out = Vec::new();
write_chunk::<_, ChunkError>(&mut out, ChunkType::XML, &NoHeader, |out| {
write_chunk(out, ChunkType::XML_RESOURCE_MAP, &NoHeader, |out| {
out.put_u32(0x0101_0003);
Ok::<_, ChunkError>(())
})
})
.unwrap();
let root = Chunk::parse_root(&out, ChunkType::XML).unwrap();
let child = root.children().next().unwrap().unwrap();
assert_eq!(child.ty, ChunkType::XML_RESOURCE_MAP);
assert_eq!(child.body, 0x0101_0003u32.to_le_bytes());
assert_eq!(
Chunk::parse_root(&out, ChunkType::TABLE).unwrap_err(),
ChunkError::UnexpectedType { expected: ChunkType::TABLE, found: ChunkType::XML }
);
}
}