use super::fourcc::FourCc;
use super::vp8x::Vp8xInfo;
use crate::image::Metadata;
use crate::{Error, Result};
pub struct Chunk<'a> {
pub id: FourCc,
pub data: &'a [u8],
}
pub struct Chunks<'a> {
rest: &'a [u8],
done: bool,
}
impl<'a> Chunks<'a> {
#[must_use]
pub const fn walk(body: &'a [u8]) -> Self {
Self {
rest: body,
done: false,
}
}
}
impl<'a> Iterator for Chunks<'a> {
type Item = Result<Chunk<'a>>;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
match parse_chunk(self.rest) {
Ok(None) => None,
Ok(Some((chunk, advance))) => {
self.rest = self.rest.get(advance..).unwrap_or(&[]);
Some(Ok(chunk))
},
Err(err) => {
self.done = true;
Some(Err(err))
},
}
}
}
fn parse_chunk(data: &[u8]) -> Result<Option<(Chunk<'_>, usize)>> {
if data.is_empty() {
return Ok(None);
}
if data.len() < 8 {
return Err(Error::Truncated);
}
let id = FourCc([data[0], data[1], data[2], data[3]]);
let size = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize;
let body = &data[8..];
let Some(payload) = body.get(..size) else {
return Err(Error::Truncated);
};
let advance = 8 + size + (size & 1);
Ok(Some((Chunk { id, data: payload }, advance)))
}
pub fn read_chunk_at(data: &[u8], offset: usize) -> Result<Option<(Chunk<'_>, usize)>> {
let Some(rest) = data.get(offset..) else {
return Ok(None);
};
match parse_chunk(rest)? {
None => Ok(None),
Some((chunk, advance)) => Ok(Some((chunk, offset + advance))),
}
}
pub fn body_range(input: &[u8]) -> Result<(usize, usize)> {
if input.len() < 12 {
return Err(Error::Truncated);
}
if input[0..4] != FourCc::RIFF.0 || input[8..12] != FourCc::WEBP.0 {
return Err(Error::NotWebp);
}
let riff_size = u32::from_le_bytes([input[4], input[5], input[6], input[7]]) as usize;
let end = 8usize.saturating_add(riff_size).min(input.len());
Ok((12.min(end), end))
}
pub fn chunks(input: &[u8]) -> Result<Chunks<'_>> {
let (start, end) = body_range(input)?;
Ok(Chunks::walk(&input[start..end]))
}
#[derive(Debug)]
pub struct ParsedWebp<'a> {
pub vp8l: &'a [u8],
pub metadata: Metadata,
pub vp8x: Option<Vp8xInfo>,
}
pub fn parse_container(input: &[u8], read_metadata: bool) -> Result<ParsedWebp<'_>> {
let mut image: Option<&[u8]> = None;
let mut extended: Option<Vp8xInfo> = None;
let mut metadata = Metadata::none();
for chunk in chunks(input)? {
let chunk = chunk?;
match chunk.id {
FourCc::VP8X => {
let info = Vp8xInfo::parse(chunk.data)?;
if info.flags.is_animated() {
return Err(Error::UnsupportedFeature);
}
extended = Some(info);
},
FourCc::VP8L if image.is_none() => image = Some(chunk.data),
FourCc::ICCP if read_metadata => metadata.icc_profile = Some(chunk.data.to_vec()),
FourCc::EXIF if read_metadata => metadata.exif = Some(chunk.data.to_vec()),
FourCc::XMP if read_metadata => metadata.xmp = Some(chunk.data.to_vec()),
FourCc::ANIM | FourCc::ANMF | FourCc::VP8 => return Err(Error::UnsupportedFeature),
_ => {},
}
}
let vp8l = image.ok_or(Error::MissingImage)?;
Ok(ParsedWebp {
vp8l,
metadata,
vp8x: extended,
})
}
#[must_use]
pub fn extract_metadata(input: &[u8]) -> Metadata {
let mut metadata = Metadata::none();
let Ok(walk) = chunks(input) else {
return metadata;
};
for chunk in walk {
let Ok(chunk) = chunk else { break };
match chunk.id {
FourCc::ICCP => metadata.icc_profile = Some(chunk.data.to_vec()),
FourCc::EXIF => metadata.exif = Some(chunk.data.to_vec()),
FourCc::XMP => metadata.xmp = Some(chunk.data.to_vec()),
_ => {},
}
}
metadata
}
#[derive(Debug, PartialEq, Eq)]
pub enum ImageChunk<'a> {
Lossless(&'a [u8]),
Lossy(&'a [u8]),
}
#[derive(Debug)]
pub struct LocatedImage<'a> {
pub image: ImageChunk<'a>,
pub alpha: Option<&'a [u8]>,
pub vp8x: Option<Vp8xInfo>,
}
pub fn locate_image_with_alpha(input: &[u8]) -> Result<LocatedImage<'_>> {
let mut image: Option<ImageChunk<'_>> = None;
let mut alpha: Option<&[u8]> = None;
let mut vp8x: Option<Vp8xInfo> = None;
for chunk in chunks(input)? {
let chunk = chunk?;
match chunk.id {
FourCc::VP8X => {
let info = Vp8xInfo::parse(chunk.data)?;
if info.flags.is_animated() {
return Err(Error::UnsupportedFeature);
}
vp8x = Some(info);
},
FourCc::VP8L if image.is_none() => image = Some(ImageChunk::Lossless(chunk.data)),
FourCc::VP8 if image.is_none() => image = Some(ImageChunk::Lossy(chunk.data)),
FourCc::ALPH => alpha = Some(chunk.data),
FourCc::ANIM | FourCc::ANMF => return Err(Error::UnsupportedFeature),
_ => {},
}
}
let image = image.ok_or(Error::MissingImage)?;
Ok(LocatedImage { image, alpha, vp8x })
}
#[derive(Debug)]
pub struct ContainerContents<'a> {
pub image: Option<ImageChunk<'a>>,
pub alpha: Option<&'a [u8]>,
pub vp8x: Option<Vp8xInfo>,
pub metadata: Metadata,
pub animated: bool,
}
pub fn read_container(input: &[u8], read_metadata: bool) -> Result<ContainerContents<'_>> {
let mut image: Option<ImageChunk<'_>> = None;
let mut alpha: Option<&[u8]> = None;
let mut vp8x: Option<Vp8xInfo> = None;
let mut metadata = Metadata::none();
let mut animated = false;
for chunk in chunks(input)? {
let chunk = chunk?;
match chunk.id {
FourCc::VP8X => {
let info = Vp8xInfo::parse(chunk.data)?;
animated |= info.flags.is_animated();
vp8x = Some(info);
},
FourCc::VP8L if image.is_none() => image = Some(ImageChunk::Lossless(chunk.data)),
FourCc::VP8 if image.is_none() => image = Some(ImageChunk::Lossy(chunk.data)),
FourCc::ALPH => alpha = Some(chunk.data),
FourCc::ANIM | FourCc::ANMF => animated = true,
FourCc::ICCP if read_metadata => metadata.icc_profile = Some(chunk.data.to_vec()),
FourCc::EXIF if read_metadata => metadata.exif = Some(chunk.data.to_vec()),
FourCc::XMP if read_metadata => metadata.xmp = Some(chunk.data.to_vec()),
_ => {},
}
}
Ok(ContainerContents {
image,
alpha,
vp8x,
metadata,
animated,
})
}
pub fn locate_image(input: &[u8]) -> Result<ImageChunk<'_>> {
locate_image_with_alpha(input).map(|located| located.image)
}
pub fn is_animated(input: &[u8]) -> Result<bool> {
for chunk in chunks(input)? {
let chunk = chunk?;
match chunk.id {
FourCc::VP8X => return Ok(Vp8xInfo::parse(chunk.data)?.flags.is_animated()),
FourCc::ANIM | FourCc::ANMF => return Ok(true),
_ => {},
}
}
Ok(false)
}
#[cfg(test)]
mod tests {
use super::{
ImageChunk, chunks, is_animated, locate_image, locate_image_with_alpha, parse_container,
};
use crate::Error;
use crate::container::fourcc::FourCc;
use crate::container::vp8x::{Vp8xFlags, Vp8xInfo};
use crate::image::Dimensions;
fn vp8l_of(file: &[u8]) -> crate::Result<&[u8]> {
parse_container(file, false).map(|p| p.vp8l)
}
fn chunk(id: [u8; 4], data: &[u8]) -> Vec<u8> {
let mut v = Vec::new();
v.extend_from_slice(&id);
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
}
fn webp(body: &[u8]) -> Vec<u8> {
let mut v = Vec::new();
v.extend_from_slice(b"RIFF");
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
}
#[test]
fn finds_vp8l_in_a_simple_file() {
let file = webp(&chunk(*b"VP8L", &[1, 2, 3, 4]));
assert_eq!(vp8l_of(&file).unwrap(), &[1, 2, 3, 4]);
}
#[test]
fn handles_odd_size_padding_between_chunks() {
let mut body = chunk(*b"VP8L", &[1, 2, 3]); body.extend_from_slice(&chunk(*b"EXIF", &[9]));
let file = webp(&body);
assert_eq!(vp8l_of(&file).unwrap(), &[1, 2, 3]);
let count = chunks(&file).unwrap().filter_map(Result::ok).count();
assert_eq!(count, 2);
}
#[test]
fn rejects_non_webp_magic() {
let mut file = webp(&chunk(*b"VP8L", &[0]));
file[0] = b'X'; assert_eq!(vp8l_of(&file), Err(Error::NotWebp));
}
#[test]
fn reports_truncated_chunk() {
let mut body = Vec::new();
body.extend_from_slice(b"VP8L");
body.extend_from_slice(&100u32.to_le_bytes());
body.extend_from_slice(&[1, 2, 3]);
let file = webp(&body);
assert_eq!(vp8l_of(&file), Err(Error::Truncated));
}
#[test]
fn fourcc_constants_match_ascii() {
assert_eq!(FourCc::VP8L, FourCc(*b"VP8L"));
assert_eq!(FourCc::RIFF, FourCc(*b"RIFF"));
}
#[test]
fn parse_container_bare_vp8l_has_no_metadata() {
let file = webp(&chunk(*b"VP8L", &[1, 2, 3, 4]));
let parsed = parse_container(&file, true).unwrap();
assert_eq!(parsed.vp8l, &[1, 2, 3, 4]);
assert!(parsed.metadata.is_empty());
assert!(parsed.vp8x.is_none());
}
#[test]
fn parse_container_extracts_vp8x_metadata() {
let flags = Vp8xFlags(0x20 | 0x08 | 0x04); let vp8x = Vp8xInfo::build(flags, Dimensions::new(4, 4).unwrap());
let mut body = chunk(*b"VP8X", &vp8x);
body.extend_from_slice(&chunk(*b"ICCP", &[0xAA, 0xBB]));
body.extend_from_slice(&chunk(*b"VP8L", &[0x2f]));
body.extend_from_slice(&chunk(*b"EXIF", &[1, 2, 3]));
body.extend_from_slice(&chunk(*b"XMP ", &[9]));
let file = webp(&body);
let parsed = parse_container(&file, true).unwrap();
assert_eq!(parsed.vp8l, &[0x2f]);
assert_eq!(
parsed.metadata.icc_profile.as_deref(),
Some(&[0xAA, 0xBB][..])
);
assert_eq!(parsed.metadata.exif.as_deref(), Some(&[1, 2, 3][..]));
assert_eq!(parsed.metadata.xmp.as_deref(), Some(&[9][..]));
assert!(parsed.vp8x.unwrap().flags.has_icc());
}
#[test]
fn parse_container_skips_metadata_when_disabled() {
let mut body = chunk(*b"ICCP", &[1]);
body.extend_from_slice(&chunk(*b"VP8L", &[0x2f]));
let file = webp(&body);
let parsed = parse_container(&file, false).unwrap();
assert!(parsed.metadata.is_empty());
}
#[test]
fn parse_container_rejects_animation_and_lossy() {
let anim = webp(&chunk(*b"ANIM", &[0u8; 6]));
assert_eq!(
parse_container(&anim, true).unwrap_err(),
Error::UnsupportedFeature
);
let lossy = webp(&chunk(*b"VP8 ", &[0, 1, 2, 3]));
assert_eq!(
parse_container(&lossy, true).unwrap_err(),
Error::UnsupportedFeature
);
}
#[test]
fn read_chunk_at_returns_chunk_and_next_offset() {
use super::read_chunk_at;
let mut body = chunk(*b"VP8L", &[1, 2]); body.extend_from_slice(&chunk(*b"EXIF", &[9, 9]));
let (c, next) = read_chunk_at(&body, 10).unwrap().unwrap();
assert_eq!(c.id, FourCc::EXIF);
assert_eq!(c.data, &[9, 9]);
assert_eq!(next, body.len());
assert!(read_chunk_at(&body, body.len()).unwrap().is_none());
}
#[test]
fn body_range_boundary_at_twelve_bytes() {
use super::body_range;
assert_eq!(body_range(&[0u8; 11]).unwrap_err(), Error::Truncated);
let empty = webp(&[]);
assert_eq!(empty.len(), 12);
assert_eq!(body_range(&empty).unwrap(), (12, 12));
}
#[test]
fn parse_container_keeps_first_vp8l_and_honors_read_metadata() {
let mut body = chunk(*b"VP8L", &[0x2f, 1]);
body.extend_from_slice(&chunk(*b"EXIF", &[7]));
body.extend_from_slice(&chunk(*b"XMP ", &[8]));
body.extend_from_slice(&chunk(*b"VP8L", &[0x2f, 2]));
let file = webp(&body);
let parsed = parse_container(&file, true).unwrap();
assert_eq!(parsed.vp8l, &[0x2f, 1]);
assert_eq!(parsed.metadata.exif.as_deref(), Some(&[7][..]));
assert_eq!(parsed.metadata.xmp.as_deref(), Some(&[8][..]));
let parsed = parse_container(&file, false).unwrap();
assert!(parsed.metadata.exif.is_none());
assert!(parsed.metadata.xmp.is_none());
}
#[test]
fn extract_metadata_collects_every_sidecar() {
let mut body = chunk(*b"ICCP", &[1, 1]);
body.extend_from_slice(&chunk(*b"VP8L", &[0x2f]));
body.extend_from_slice(&chunk(*b"EXIF", &[2, 2]));
body.extend_from_slice(&chunk(*b"XMP ", &[3, 3]));
let md = super::extract_metadata(&webp(&body));
assert_eq!(md.icc_profile.as_deref(), Some(&[1, 1][..]));
assert_eq!(md.exif.as_deref(), Some(&[2, 2][..]));
assert_eq!(md.xmp.as_deref(), Some(&[3, 3][..]));
}
#[test]
fn locate_image_with_alpha_keeps_the_first_image() {
let mut body = chunk(*b"VP8L", &[0x2f, 1]);
body.extend_from_slice(&chunk(*b"VP8L", &[0x2f, 2]));
body.extend_from_slice(&chunk(*b"VP8 ", &[3]));
let file = webp(&body);
let located = locate_image_with_alpha(&file).unwrap();
assert_eq!(located.image, ImageChunk::Lossless(&[0x2f, 1][..]));
}
#[test]
fn is_animated_detects_anmf_without_vp8x() {
assert!(is_animated(&webp(&chunk(*b"ANMF", &[0u8; 8]))).unwrap());
assert!(!is_animated(&webp(&chunk(*b"VP8L", &[0x2f]))).unwrap());
}
#[test]
fn parse_container_rejects_vp8x_animation_flag() {
let vp8x = Vp8xInfo::build(Vp8xFlags(0x02), Dimensions::new(2, 2).unwrap());
let mut body = chunk(*b"VP8X", &vp8x);
body.extend_from_slice(&chunk(*b"VP8L", &[0x2f]));
assert_eq!(
parse_container(&webp(&body), true).unwrap_err(),
Error::UnsupportedFeature
);
}
#[test]
fn parse_container_missing_image() {
let file = webp(&chunk(*b"ICCP", &[1, 2]));
assert_eq!(
parse_container(&file, true).unwrap_err(),
Error::MissingImage
);
}
#[test]
fn locate_image_distinguishes_lossless_and_lossy() {
let lossless = webp(&chunk(*b"VP8L", &[0x2f, 1, 2, 3]));
assert_eq!(
locate_image(&lossless).unwrap(),
ImageChunk::Lossless(&[0x2f, 1, 2, 3])
);
let lossy = webp(&chunk(*b"VP8 ", &[9, 8, 7, 6]));
assert_eq!(
locate_image(&lossy).unwrap(),
ImageChunk::Lossy(&[9, 8, 7, 6])
);
}
#[test]
fn locate_image_rejects_animation_but_reports_missing_and_lossy() {
let anim = webp(&chunk(*b"ANIM", &[0u8; 6]));
assert_eq!(locate_image(&anim).unwrap_err(), Error::UnsupportedFeature);
let none = webp(&chunk(*b"ICCP", &[1, 2]));
assert_eq!(locate_image(&none).unwrap_err(), Error::MissingImage);
}
#[test]
fn locate_image_reads_lossy_from_an_extended_file() {
let vp8x = Vp8xInfo::build(Vp8xFlags(0x10), Dimensions::new(4, 4).unwrap());
let mut body = chunk(*b"VP8X", &vp8x);
body.extend_from_slice(&chunk(*b"VP8 ", &[0xAA, 0xBB]));
let file = webp(&body);
assert_eq!(
locate_image(&file).unwrap(),
ImageChunk::Lossy(&[0xAA, 0xBB])
);
}
#[test]
fn locate_with_alpha_captures_alph_before_vp8() {
let vp8x = Vp8xInfo::build(Vp8xFlags(0x10), Dimensions::new(4, 4).unwrap());
let mut body = chunk(*b"VP8X", &vp8x);
body.extend_from_slice(&chunk(*b"ALPH", &[0x01, 0xAA, 0xBB])); body.extend_from_slice(&chunk(*b"VP8 ", &[9, 8, 7]));
let file = webp(&body);
let located = locate_image_with_alpha(&file).unwrap();
assert_eq!(located.image, ImageChunk::Lossy(&[9, 8, 7]));
assert_eq!(located.alpha, Some(&[0x01, 0xAA, 0xBB][..]));
assert!(located.vp8x.unwrap().flags.has_alpha());
}
#[test]
fn locate_with_alpha_captures_alph_after_vp8() {
let mut body = chunk(*b"VP8 ", &[1, 2, 3, 4]);
body.extend_from_slice(&chunk(*b"ALPH", &[0x00, 0x55]));
let file = webp(&body);
let located = locate_image_with_alpha(&file).unwrap();
assert_eq!(located.image, ImageChunk::Lossy(&[1, 2, 3, 4]));
assert_eq!(located.alpha, Some(&[0x00, 0x55][..]));
assert!(located.vp8x.is_none());
}
#[test]
fn locate_with_alpha_none_when_absent() {
let file = webp(&chunk(*b"VP8L", &[0x2f, 1, 2, 3]));
let located = locate_image_with_alpha(&file).unwrap();
assert_eq!(located.image, ImageChunk::Lossless(&[0x2f, 1, 2, 3]));
assert!(located.alpha.is_none());
assert!(located.vp8x.is_none());
}
#[test]
fn locate_with_alpha_still_rejects_animation() {
let anim = webp(&chunk(*b"ANIM", &[0u8; 6]));
assert_eq!(
locate_image_with_alpha(&anim).unwrap_err(),
Error::UnsupportedFeature
);
}
#[test]
fn is_animated_true_for_vp8x_animation_flag() {
let vp8x = Vp8xInfo::build(Vp8xFlags(0x02), Dimensions::new(4, 4).unwrap());
let file = webp(&chunk(*b"VP8X", &vp8x));
assert!(is_animated(&file).unwrap());
}
#[test]
fn is_animated_false_for_still_vp8x() {
let vp8x = Vp8xInfo::build(Vp8xFlags(0x10), Dimensions::new(4, 4).unwrap()); let mut body = chunk(*b"VP8X", &vp8x);
body.extend_from_slice(&chunk(*b"VP8L", &[0x2f]));
let file = webp(&body);
assert!(!is_animated(&file).unwrap());
}
#[test]
fn is_animated_false_for_bare_vp8l() {
let file = webp(&chunk(*b"VP8L", &[0x2f, 1, 2, 3]));
assert!(!is_animated(&file).unwrap());
}
#[test]
fn is_animated_errors_on_truncated() {
let mut body = Vec::new();
body.extend_from_slice(b"VP8L");
body.extend_from_slice(&100u32.to_le_bytes());
body.extend_from_slice(&[1, 2, 3]);
let file = webp(&body);
assert_eq!(is_animated(&file).unwrap_err(), Error::Truncated);
}
#[test]
fn read_container_bare_vp8l_is_a_still_lossless_image() {
use super::read_container;
let file = webp(&chunk(*b"VP8L", &[0x2f, 1, 2, 3]));
let c = read_container(&file, true).unwrap();
assert_eq!(c.image, Some(ImageChunk::Lossless(&[0x2f, 1, 2, 3][..])));
assert!(c.alpha.is_none());
assert!(c.vp8x.is_none());
assert!(c.metadata.is_empty());
assert!(!c.animated);
}
#[test]
fn read_container_collects_extended_lossy_alpha_and_metadata() {
use super::read_container;
let vp8x = Vp8xInfo::build(Vp8xFlags(0x10 | 0x08), Dimensions::new(4, 4).unwrap());
let mut body = chunk(*b"VP8X", &vp8x);
body.extend_from_slice(&chunk(*b"ALPH", &[0x01, 0xAA]));
body.extend_from_slice(&chunk(*b"VP8 ", &[9, 8, 7]));
body.extend_from_slice(&chunk(*b"EXIF", &[1, 2]));
let file = webp(&body);
let c = read_container(&file, true).unwrap();
assert_eq!(c.image, Some(ImageChunk::Lossy(&[9, 8, 7][..])));
assert_eq!(c.alpha, Some(&[0x01, 0xAA][..]));
assert!(c.vp8x.unwrap().flags.has_alpha());
assert_eq!(c.metadata.exif.as_deref(), Some(&[1, 2][..]));
assert!(!c.animated);
let c = read_container(&file, false).unwrap();
assert!(c.metadata.is_empty());
assert_eq!(c.alpha, Some(&[0x01, 0xAA][..]));
}
#[test]
fn read_container_marks_animation_without_erroring() {
use super::read_container;
let anmf = webp(&chunk(*b"ANMF", &[0u8; 8]));
let c = read_container(&anmf, true).unwrap();
assert!(c.animated);
assert!(c.image.is_none());
let vp8x = Vp8xInfo::build(Vp8xFlags(0x02), Dimensions::new(4, 4).unwrap());
let anim_vp8x = webp(&chunk(*b"VP8X", &vp8x));
let c = read_container(&anim_vp8x, true).unwrap();
assert!(c.animated);
}
#[test]
fn chunks_reject_oversized_chunk_without_overflow() {
let mut body = Vec::new();
body.extend_from_slice(b"VP8L");
body.extend_from_slice(&u32::MAX.to_le_bytes());
body.extend_from_slice(&[1, 2, 3]);
let file = webp(&body);
assert_eq!(parse_container(&file, false).unwrap_err(), Error::Truncated);
}
}