use crate::bytes::{Reader, u32_to_usize};
use crate::error::{MalformedDetail, ResourceLimit};
pub(crate) type FourCc = [u8; 4];
pub(crate) const RIFF: FourCc = *b"RIFF";
pub(crate) const HEADER_BYTES: usize = 8;
pub(crate) const BODY_START: usize = HEADER_BYTES + 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Chunk<'a> {
pub(crate) kind: FourCc,
pub(crate) data: &'a [u8],
pub(crate) raw: &'a [u8],
pub(crate) offset: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum WalkError {
Malformed {
detail: MalformedDetail,
offset: usize,
},
Limit(ResourceLimit),
}
impl WalkError {
const fn at(detail: MalformedDetail, offset: usize) -> Self {
Self::Malformed { detail, offset }
}
}
pub(crate) fn read<'a>(
input: &'a [u8],
form: FourCc,
budget: &mut u32,
) -> Result<(Vec<Chunk<'a>>, &'a [u8]), WalkError> {
let mut r = Reader::new(input);
if r.take(RIFF.len()) != Some(RIFF.as_slice()) {
return Err(WalkError::at(MalformedDetail::MissingMarker, 0));
}
let declared = r
.u32_le()
.ok_or(WalkError::at(MalformedDetail::Truncated, 0))?;
let size = u32_to_usize(declared).ok_or(WalkError::at(MalformedDetail::LengthOutOfRange, 4))?;
if size < form.len() || size > r.remaining() {
return Err(WalkError::at(MalformedDetail::LengthOutOfRange, 4));
}
if r.take(form.len()) != Some(form.as_slice()) {
return Err(WalkError::at(MalformedDetail::MissingMarker, HEADER_BYTES));
}
let end = HEADER_BYTES.saturating_add(size);
let chunks = walk(input, BODY_START, end, 0, budget)?;
Ok((chunks, input.get(end..).unwrap_or_default()))
}
pub(crate) fn chunks<'a>(
data: &'a [u8],
base: usize,
budget: &mut u32,
) -> Result<Vec<Chunk<'a>>, WalkError> {
walk(data, 0, data.len(), base, budget)
}
fn walk<'a>(
input: &'a [u8],
start: usize,
end: usize,
base: usize,
budget: &mut u32,
) -> Result<Vec<Chunk<'a>>, WalkError> {
let mut r = Reader::new(input);
r.skip(start)
.ok_or(WalkError::at(MalformedDetail::Truncated, base))?;
let mut out = Vec::new();
while r.position() < end {
let at = r.position();
let offset = base.saturating_add(at);
*budget = budget
.checked_sub(1)
.ok_or(WalkError::Limit(ResourceLimit::ItemCount))?;
let kind: FourCc = r
.take(4)
.and_then(|k| k.try_into().ok())
.ok_or(WalkError::at(MalformedDetail::Truncated, offset))?;
if !kind.iter().all(|b| b.is_ascii_graphic() || *b == b' ') {
return Err(WalkError::at(MalformedDetail::UnexpectedMarker, offset));
}
let declared = r
.u32_le()
.ok_or(WalkError::at(MalformedDetail::Truncated, offset))?;
let length = u32_to_usize(declared)
.ok_or(WalkError::at(MalformedDetail::LengthOutOfRange, offset))?;
let padding = length & 1;
let padded = length
.checked_add(padding)
.ok_or(WalkError::at(MalformedDetail::LengthOutOfRange, offset))?;
if padded > end.saturating_sub(r.position()) {
return Err(WalkError::at(MalformedDetail::LengthOutOfRange, offset));
}
let data = r
.take(length)
.ok_or(WalkError::at(MalformedDetail::LengthOutOfRange, offset))?;
r.skip(padding)
.ok_or(WalkError::at(MalformedDetail::Truncated, offset))?;
out.push(Chunk {
kind,
data,
raw: input.get(at..r.position()).unwrap_or_default(),
offset,
});
}
Ok(out)
}
pub(crate) fn list_form(data: &[u8]) -> Option<(FourCc, &[u8])> {
let form: FourCc = data.get(..4)?.try_into().ok()?;
Some((form, data.get(4..)?))
}
pub(crate) fn write_chunk(
out: &mut Vec<u8>,
kind: FourCc,
payload: &[u8],
) -> Result<(), MalformedDetail> {
let size = u32::try_from(payload.len()).map_err(|_| MalformedDetail::LengthOutOfRange)?;
out.extend_from_slice(&kind);
out.extend_from_slice(&size.to_le_bytes());
out.extend_from_slice(payload);
if payload.len() & 1 == 1 {
out.push(0);
}
Ok(())
}
pub(crate) fn write(form: FourCc, body: &[u8]) -> Result<Vec<u8>, MalformedDetail> {
let size = body
.len()
.checked_add(form.len())
.and_then(|n| u32::try_from(n).ok())
.ok_or(MalformedDetail::LengthOutOfRange)?;
let mut out = Vec::with_capacity(body.len().saturating_add(BODY_START));
out.extend_from_slice(&RIFF);
out.extend_from_slice(&size.to_le_bytes());
out.extend_from_slice(&form);
out.extend_from_slice(body);
Ok(out)
}
pub(crate) fn name_of(kind: &[u8]) -> String {
crate::formats::xmp::name_of(kind).trim_end().to_owned()
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
clippy::indexing_slicing,
clippy::arithmetic_side_effects
)]
use super::*;
fn chunk(kind: FourCc, payload: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
write_chunk(&mut out, kind, payload).unwrap();
out
}
fn file(form: FourCc, parts: &[Vec<u8>]) -> Vec<u8> {
let body: Vec<u8> = parts.concat();
write(form, &body).unwrap()
}
#[test]
fn a_flat_sequence_is_read_in_order() {
let data = file(
*b"WAVE",
&[chunk(*b"fmt ", b"0123456789abcdef"), chunk(*b"data", b"AB")],
);
let mut budget = 64;
let (chunks, trailing) = read(&data, *b"WAVE", &mut budget).unwrap();
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].kind, *b"fmt ");
assert_eq!(chunks[1].data, b"AB");
assert_eq!(chunks[1].offset, BODY_START + 8 + 16);
assert!(trailing.is_empty());
}
#[test]
fn an_odd_payload_carries_a_pad_byte_that_the_raw_slice_includes() {
let data = file(*b"WAVE", &[chunk(*b"data", b"ODD")]);
let mut budget = 64;
let (chunks, _) = read(&data, *b"WAVE", &mut budget).unwrap();
assert_eq!(chunks[0].data, b"ODD");
assert_eq!(chunks[0].raw.len(), HEADER_BYTES + 4);
}
#[test]
fn a_declared_size_past_the_end_is_refused_not_clamped() {
let mut data = file(*b"WAVE", &[chunk(*b"data", b"AB")]);
data[4..8].copy_from_slice(&0x00FF_FFFFu32.to_le_bytes());
let mut budget = 64;
assert_eq!(
read(&data, *b"WAVE", &mut budget),
Err(WalkError::at(MalformedDetail::LengthOutOfRange, 4))
);
}
#[test]
fn a_chunk_overrunning_the_riff_extent_is_refused() {
let mut lying = b"data".to_vec();
lying.extend_from_slice(&0x0010_0000u32.to_le_bytes());
lying.extend_from_slice(b"AB");
let data = file(*b"WAVE", &[lying]);
let mut budget = 64;
assert!(matches!(
read(&data, *b"WAVE", &mut budget),
Err(WalkError::Malformed {
detail: MalformedDetail::LengthOutOfRange,
..
})
));
}
#[test]
fn the_wrong_magic_or_form_type_is_refused() {
let mut budget = 64;
assert_eq!(
read(b"RIFX\x04\x00\x00\x00WAVE", *b"WAVE", &mut budget),
Err(WalkError::at(MalformedDetail::MissingMarker, 0))
);
assert_eq!(
read(b"RIFF\x04\x00\x00\x00WEBP", *b"WAVE", &mut budget),
Err(WalkError::at(MalformedDetail::MissingMarker, HEADER_BYTES))
);
}
#[test]
fn a_code_that_is_not_ascii_is_refused() {
let mut raw = b"\x00\x01\x02\x03".to_vec();
raw.extend_from_slice(&0u32.to_le_bytes());
let data = file(*b"WAVE", &[raw]);
let mut budget = 64;
assert_eq!(
read(&data, *b"WAVE", &mut budget),
Err(WalkError::at(MalformedDetail::UnexpectedMarker, BODY_START))
);
}
#[test]
fn the_item_budget_is_shared_across_the_walk() {
let parts: Vec<Vec<u8>> = (0..5).map(|_| chunk(*b"JUNK", b"xx")).collect();
let data = file(*b"WAVE", &parts);
let mut budget = 3;
assert_eq!(
read(&data, *b"WAVE", &mut budget),
Err(WalkError::Limit(ResourceLimit::ItemCount))
);
}
#[test]
fn bytes_past_the_declared_extent_are_handed_back() {
let mut data = file(*b"WAVE", &[chunk(*b"data", b"AB")]);
data.extend_from_slice(b"APPENDED");
let mut budget = 64;
let (chunks, trailing) = read(&data, *b"WAVE", &mut budget).unwrap();
assert_eq!(chunks.len(), 1);
assert_eq!(trailing, b"APPENDED");
}
#[test]
fn a_list_body_walks_as_a_bare_sequence() {
let mut body = b"INFO".to_vec();
body.extend_from_slice(&chunk(*b"ISFT", b"SYNTHETIC\0"));
let (form, rest) = list_form(&body).unwrap();
assert_eq!(form, *b"INFO");
let mut budget = 64;
let inner = chunks(rest, 100, &mut budget).unwrap();
assert_eq!(inner[0].kind, *b"ISFT");
assert_eq!(inner[0].offset, 100);
}
#[test]
fn what_this_module_writes_it_can_read_back() {
let data = file(
*b"WAVE",
&[chunk(*b"fmt ", &[0u8; 16]), chunk(*b"data", b"ODD")],
);
let mut budget = 64;
let (chunks, trailing) = read(&data, *b"WAVE", &mut budget).unwrap();
assert_eq!(chunks.len(), 2);
assert!(trailing.is_empty());
let rebuilt: Vec<u8> = chunks.iter().flat_map(|c| c.raw.to_vec()).collect();
assert_eq!(write(*b"WAVE", &rebuilt).unwrap(), data);
}
#[test]
fn names_drop_the_padding_space() {
assert_eq!(name_of(b"fmt "), "fmt");
assert_eq!(name_of(b"bext"), "bext");
}
}