use super::fourcc::FourCc;
#[must_use]
pub fn declared_len(bytes: &[u8]) -> Option<usize> {
let s = bytes.get(4..8)?;
Some(8usize.saturating_add(u32::from_le_bytes([s[0], s[1], s[2], s[3]]) as usize))
}
#[must_use]
pub fn is_complete(bytes: &[u8]) -> bool {
declared_len(bytes).is_some_and(|total| total >= 12 && bytes.len() >= total)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PartialChunk {
pub id: FourCc,
pub payload_start: usize,
pub payload_end: usize,
pub next: usize,
}
pub fn scan_chunks(bytes: &[u8]) -> impl Iterator<Item = PartialChunk> + '_ {
let mut cursor = 12usize;
core::iter::from_fn(move || {
if cursor > bytes.len().saturating_sub(8) {
return None;
}
let id = FourCc([
bytes[cursor],
bytes[cursor + 1],
bytes[cursor + 2],
bytes[cursor + 3],
]);
let size = u32::from_le_bytes([
bytes[cursor + 4],
bytes[cursor + 5],
bytes[cursor + 6],
bytes[cursor + 7],
]) as usize;
let payload_start = cursor + 8; let payload_end = payload_start.saturating_add(size);
let next = payload_end.saturating_add(size & 1);
cursor = next;
Some(PartialChunk {
id,
payload_start,
payload_end,
next,
})
})
}
#[cfg(test)]
mod tests {
use super::{PartialChunk, declared_len, is_complete, scan_chunks};
use crate::container::fourcc::FourCc;
fn webp(body: &[u8]) -> Vec<u8> {
let mut v = b"RIFF".to_vec();
v.extend_from_slice(&u32::try_from(4 + body.len()).unwrap().to_le_bytes());
v.extend_from_slice(b"WEBP");
v.extend_from_slice(body);
v
}
fn chunk(id: [u8; 4], data: &[u8]) -> Vec<u8> {
let mut v = id.to_vec();
v.extend_from_slice(&u32::try_from(data.len()).unwrap().to_le_bytes());
v.extend_from_slice(data);
if data.len() % 2 == 1 {
v.push(0);
}
v
}
#[test]
fn declared_len_needs_eight_bytes_and_adds_the_header() {
assert_eq!(declared_len(&[0u8; 7]), None);
assert_eq!(declared_len(&webp(&[])), Some(12));
}
#[test]
fn is_complete_gates_on_the_declared_size() {
let file = webp(&chunk(*b"VP8L", &[0x2f, 1, 2, 3]));
assert!(is_complete(&file));
assert!(!is_complete(&file[..file.len() - 1]));
assert!(!is_complete(&[0u8; 4]));
}
#[test]
fn scan_chunks_yields_headers_before_payloads_arrive() {
let mut body = chunk(*b"VP8X", &[0u8; 10]);
body.extend_from_slice(&chunk(*b"VP8 ", &[9, 8, 7]));
let file = webp(&body);
let ids: Vec<FourCc> = scan_chunks(&file).map(|c| c.id).collect();
assert_eq!(ids, vec![FourCc::VP8X, FourCc::VP8]);
let vp8_header_end = file.len() - 3; let partial = &file[..vp8_header_end];
let last = scan_chunks(partial).last().unwrap();
assert_eq!(last.id, FourCc::VP8);
assert!(
last.payload_end > partial.len(),
"declared end past the buffer"
);
}
#[test]
fn scan_chunks_is_overflow_safe_on_a_hostile_size() {
let mut body = b"VP8 ".to_vec();
body.extend_from_slice(&u32::MAX.to_le_bytes());
body.extend_from_slice(&[1, 2, 3]);
let file = webp(&body);
let chunks: Vec<PartialChunk> = scan_chunks(&file).collect();
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].id, FourCc::VP8);
assert!(chunks[0].payload_end >= file.len());
}
}