use core::fmt;
pub type FourCc = [u8; 4];
pub mod fourcc {
use super::FourCc;
pub const RIFF: FourCc = *b"RIFF";
pub const WEBP: FourCc = *b"WEBP";
pub const VP8: FourCc = *b"VP8 ";
pub const VP8L: FourCc = *b"VP8L";
pub const VP8X: FourCc = *b"VP8X";
pub const ALPH: FourCc = *b"ALPH";
pub const ANIM: FourCc = *b"ANIM";
pub const ANMF: FourCc = *b"ANMF";
pub const ICCP: FourCc = *b"ICCP";
pub const EXIF: FourCc = *b"EXIF";
pub const XMP: FourCc = *b"XMP ";
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContainerError {
TooShortForHeader { got: usize },
NotRiff { got: FourCc },
NotWebp { got: FourCc },
RiffSizeOverflowsBuffer {
declared: u32,
buffer_len: usize,
},
TruncatedChunkHeader {
offset: usize,
},
ChunkPayloadOverflowsRiff {
offset: usize,
declared: u32,
available: usize,
},
MissingPadByte {
offset: usize,
},
}
impl fmt::Display for ContainerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooShortForHeader { got } => write!(
f,
"WebP buffer too short for §2.4 file header (12 bytes), got {got}"
),
Self::NotRiff { got } => write!(
f,
"WebP buffer does not start with §2.4 'RIFF' tag (got {:02x?})",
got
),
Self::NotWebp { got } => write!(
f,
"WebP buffer is RIFF but not 'WEBP' form (got {:02x?})",
got
),
Self::RiffSizeOverflowsBuffer {
declared,
buffer_len,
} => write!(
f,
"§2.4 RIFF File Size {declared} overflows buffer length {buffer_len}"
),
Self::TruncatedChunkHeader { offset } => write!(
f,
"§2.3 chunk header at offset {offset} is truncated (need 8 bytes)"
),
Self::ChunkPayloadOverflowsRiff {
offset,
declared,
available,
} => write!(
f,
"§2.3 chunk at offset {offset} declares Size {declared} \
but only {available} bytes remain in the RIFF payload"
),
Self::MissingPadByte { offset } => write!(
f,
"§2.3 chunk at offset {offset} has odd Size but no trailing pad byte"
),
}
}
}
impl std::error::Error for ContainerError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WebpChunk {
pub fourcc: FourCc,
pub size: u32,
pub payload_start: usize,
pub payload_end: usize,
}
impl WebpChunk {
pub fn payload<'a>(&self, buf: &'a [u8]) -> &'a [u8] {
&buf[self.payload_start..self.payload_end]
}
pub fn is_vp8_lossy(&self) -> bool {
self.fourcc == fourcc::VP8
}
pub fn is_vp8_lossless(&self) -> bool {
self.fourcc == fourcc::VP8L
}
pub fn is_extended(&self) -> bool {
self.fourcc == fourcc::VP8X
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WebpContainer {
pub riff_file_size: u32,
pub chunks: Vec<WebpChunk>,
}
impl WebpContainer {
pub fn chunks_with_fourcc(&self, fourcc: FourCc) -> impl Iterator<Item = &WebpChunk> + '_ {
self.chunks.iter().filter(move |c| c.fourcc == fourcc)
}
pub fn first_chunk_with_fourcc(&self, fourcc: FourCc) -> Option<&WebpChunk> {
self.chunks.iter().find(|c| c.fourcc == fourcc)
}
pub fn is_extended(&self) -> bool {
self.chunks
.first()
.map(|c| c.is_extended())
.unwrap_or(false)
}
}
pub fn parse(buf: &[u8]) -> Result<WebpContainer, ContainerError> {
if buf.len() < 12 {
return Err(ContainerError::TooShortForHeader { got: buf.len() });
}
let riff_tag: FourCc = buf[0..4]
.try_into()
.expect("12-byte slice always has 4 bytes at offset 0");
if riff_tag != fourcc::RIFF {
return Err(ContainerError::NotRiff { got: riff_tag });
}
let riff_file_size = u32::from_le_bytes(
buf[4..8]
.try_into()
.expect("12-byte slice always has 4 bytes at offset 4"),
);
let webp_tag: FourCc = buf[8..12]
.try_into()
.expect("12-byte slice always has 4 bytes at offset 8");
if webp_tag != fourcc::WEBP {
return Err(ContainerError::NotWebp { got: webp_tag });
}
let declared_payload_end = 8usize.saturating_add(riff_file_size as usize);
if declared_payload_end > buf.len() {
return Err(ContainerError::RiffSizeOverflowsBuffer {
declared: riff_file_size,
buffer_len: buf.len(),
});
}
let chunk_stream_end = declared_payload_end;
let mut chunks: Vec<WebpChunk> = Vec::new();
let mut cursor: usize = 12; while cursor < chunk_stream_end {
if chunk_stream_end - cursor < 8 {
return Err(ContainerError::TruncatedChunkHeader { offset: cursor });
}
let fourcc: FourCc = buf[cursor..cursor + 4]
.try_into()
.expect("bounds checked above");
let size = u32::from_le_bytes(
buf[cursor + 4..cursor + 8]
.try_into()
.expect("bounds checked above"),
);
let payload_start = cursor + 8;
let payload_avail = chunk_stream_end - payload_start;
if (size as usize) > payload_avail {
return Err(ContainerError::ChunkPayloadOverflowsRiff {
offset: cursor,
declared: size,
available: payload_avail,
});
}
let payload_end = payload_start + size as usize;
chunks.push(WebpChunk {
fourcc,
size,
payload_start,
payload_end,
});
let needs_pad = (size & 1) == 1;
let total = if needs_pad {
(size as usize).checked_add(1)
} else {
Some(size as usize)
}
.expect("size+1 cannot overflow because size <= payload_avail < usize::MAX");
let after_chunk =
payload_start
.checked_add(total)
.ok_or(ContainerError::ChunkPayloadOverflowsRiff {
offset: cursor,
declared: size,
available: payload_avail,
})?;
if after_chunk > chunk_stream_end {
return Err(ContainerError::MissingPadByte { offset: cursor });
}
cursor = after_chunk;
}
Ok(WebpContainer {
riff_file_size,
chunks,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn chunk(fourcc: &FourCc, payload: &[u8]) -> Vec<u8> {
let mut v = Vec::with_capacity(8 + payload.len() + 1);
v.extend_from_slice(fourcc);
v.extend_from_slice(&(payload.len() as u32).to_le_bytes());
v.extend_from_slice(payload);
if payload.len() % 2 == 1 {
v.push(0);
}
v
}
fn webp(chunks: &[u8]) -> Vec<u8> {
let file_size = 4u32 + chunks.len() as u32;
let mut v = Vec::with_capacity(12 + chunks.len());
v.extend_from_slice(b"RIFF");
v.extend_from_slice(&file_size.to_le_bytes());
v.extend_from_slice(b"WEBP");
v.extend_from_slice(chunks);
v
}
#[test]
fn simple_lossy_walks_to_one_vp8_chunk() {
let body = chunk(&fourcc::VP8, &[0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03]);
let buf = webp(&body);
let c = parse(&buf).expect("simple lossy parses");
assert_eq!(c.riff_file_size, 4 + body.len() as u32);
assert_eq!(c.chunks.len(), 1);
let only = &c.chunks[0];
assert!(only.is_vp8_lossy());
assert_eq!(only.size, 7);
assert_eq!(
only.payload(&buf),
&[0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03]
);
assert!(!c.is_extended());
}
#[test]
fn simple_lossless_walks_to_one_vp8l_chunk() {
let body = chunk(&fourcc::VP8L, &[0x2F, 0x00, 0x00, 0x00]);
let buf = webp(&body);
let c = parse(&buf).expect("simple lossless parses");
assert_eq!(c.chunks.len(), 1);
let only = &c.chunks[0];
assert!(only.is_vp8_lossless());
assert_eq!(only.size, 4);
assert_eq!(only.payload(&buf), &[0x2F, 0x00, 0x00, 0x00]);
}
#[test]
fn extended_layout_walks_all_chunks_in_order() {
let vp8x_payload = vec![
0x10, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x07, 0x00, 0x00, ];
let mut body = Vec::new();
body.extend(chunk(&fourcc::VP8X, &vp8x_payload));
body.extend(chunk(&fourcc::ICCP, &[0xAA; 5])); body.extend(chunk(&fourcc::ANIM, &[0; 6]));
body.extend(chunk(&fourcc::ANMF, &[0; 9])); body.extend(chunk(&fourcc::VP8, &[0; 8]));
body.extend(chunk(&fourcc::EXIF, b"Exif\x00\x00MM*\x00"));
body.extend(chunk(&fourcc::XMP, b"<?xpacket?>"));
let buf = webp(&body);
let c = parse(&buf).expect("extended layout parses");
let order: Vec<FourCc> = c.chunks.iter().map(|c| c.fourcc).collect();
assert_eq!(
order,
vec![
fourcc::VP8X,
fourcc::ICCP,
fourcc::ANIM,
fourcc::ANMF,
fourcc::VP8,
fourcc::EXIF,
fourcc::XMP,
]
);
assert!(c.is_extended());
assert_eq!(c.first_chunk_with_fourcc(fourcc::ICCP).unwrap().size, 5);
assert_eq!(c.chunks_with_fourcc(fourcc::VP8).count(), 1);
let iccp = c.first_chunk_with_fourcc(fourcc::ICCP).unwrap();
assert_eq!(iccp.payload(&buf), &[0xAA, 0xAA, 0xAA, 0xAA, 0xAA]);
}
#[test]
fn rejects_buffer_shorter_than_file_header() {
let buf = b"RIFF\x00\x00\x00\x00WEB";
assert_eq!(
parse(buf),
Err(ContainerError::TooShortForHeader { got: 11 })
);
}
#[test]
fn rejects_wrong_riff_or_form_tag() {
let mut buf = b"riff\x04\x00\x00\x00WEBP".to_vec();
match parse(&buf) {
Err(ContainerError::NotRiff { got }) => assert_eq!(&got, b"riff"),
other => panic!("expected NotRiff, got {other:?}"),
}
buf[0..4].copy_from_slice(b"RIFF");
buf[8..12].copy_from_slice(b"AVI ");
match parse(&buf) {
Err(ContainerError::NotWebp { got }) => assert_eq!(&got, b"AVI "),
other => panic!("expected NotWebp, got {other:?}"),
}
}
#[test]
fn rejects_chunk_whose_size_overflows_riff_payload() {
let mut bad = Vec::new();
bad.extend_from_slice(b"VP8 ");
bad.extend_from_slice(&100u32.to_le_bytes()); let buf = webp(&bad);
match parse(&buf) {
Err(ContainerError::ChunkPayloadOverflowsRiff {
offset,
declared,
available,
}) => {
assert_eq!(offset, 12);
assert_eq!(declared, 100);
assert_eq!(available, 0);
}
other => panic!("expected ChunkPayloadOverflowsRiff, got {other:?}"),
}
}
#[test]
fn rejects_odd_chunk_missing_pad_byte() {
let mut chunk_bytes = Vec::new();
chunk_bytes.extend_from_slice(b"ICCP");
chunk_bytes.extend_from_slice(&3u32.to_le_bytes()); chunk_bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE]);
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&(4u32 + chunk_bytes.len() as u32).to_le_bytes());
buf.extend_from_slice(b"WEBP");
buf.extend_from_slice(&chunk_bytes);
match parse(&buf) {
Err(ContainerError::MissingPadByte { offset }) => assert_eq!(offset, 12),
other => panic!("expected MissingPadByte, got {other:?}"),
}
}
#[test]
fn rejects_riff_size_that_runs_past_buffer() {
let mut buf = b"RIFF".to_vec();
buf.extend_from_slice(&1000u32.to_le_bytes());
buf.extend_from_slice(b"WEBP");
match parse(&buf) {
Err(ContainerError::RiffSizeOverflowsBuffer {
declared,
buffer_len,
}) => {
assert_eq!(declared, 1000);
assert_eq!(buffer_len, 12);
}
other => panic!("expected RiffSizeOverflowsBuffer, got {other:?}"),
}
}
}