use crate::error::{MalformedKind, ParsingError, ParsingErrorState};
use crate::parser::ParsingState;
const WEBP_HEADER_LEN: usize = 12; const CHUNK_HEADER_LEN: usize = 8;
pub(crate) fn extract_exif(
state: Option<ParsingState>,
buf: &[u8],
) -> Result<(Option<&[u8]>, Option<ParsingState>), ParsingErrorState> {
let past_header = matches!(state, Some(ParsingState::WebpPastHeader(_)));
let (mut cursor, mut stream_left) = match state {
Some(ParsingState::WebpPastHeader(left)) => (0usize, left),
_ => {
if buf.len() < WEBP_HEADER_LEN {
return Err(ParsingErrorState::new(
ParsingError::Need(WEBP_HEADER_LEN - buf.len()),
None,
));
}
if &buf[0..4] != b"RIFF" || &buf[8..12] != b"WEBP" {
return Err(ParsingErrorState::new(
ParsingError::Failed {
kind: MalformedKind::WebpChunk,
message: "WebP: bad RIFF/WEBP header".into(),
},
None,
));
}
let riff_size = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]) as usize;
(WEBP_HEADER_LEN, riff_size.saturating_sub(4))
}
};
let preserve = |left: usize| past_header.then_some(ParsingState::WebpPastHeader(left));
loop {
if stream_left < CHUNK_HEADER_LEN {
return Ok((None, Some(ParsingState::WebpPastHeader(stream_left))));
}
let in_buf = buf.len() - cursor;
if in_buf < CHUNK_HEADER_LEN {
return Err(ParsingErrorState::new(
ParsingError::Need(CHUNK_HEADER_LEN - in_buf),
preserve(stream_left),
));
}
let fourcc = &buf[cursor..cursor + 4];
let size = u32::from_le_bytes([
buf[cursor + 4],
buf[cursor + 5],
buf[cursor + 6],
buf[cursor + 7],
]);
let pad = (size & 1) as usize;
let total = match (size as usize).checked_add(CHUNK_HEADER_LEN + pad) {
Some(t) => t,
None => {
return Err(ParsingErrorState::new(
ParsingError::Failed {
kind: MalformedKind::WebpChunk,
message: "WebP: chunk size overflows addressable size".into(),
},
preserve(stream_left),
));
}
};
if total > stream_left {
return Ok((None, Some(ParsingState::WebpPastHeader(0))));
}
if fourcc == b"EXIF" {
if total > in_buf {
return Err(ParsingErrorState::new(
ParsingError::Need(total - in_buf),
preserve(stream_left),
));
}
let data_start = cursor + CHUNK_HEADER_LEN;
let data_end = data_start + size as usize;
let mut payload = &buf[data_start..data_end];
if payload.starts_with(b"Exif\0\0") {
payload = &payload[6..];
}
if payload.is_empty() {
return Ok((None, preserve(stream_left)));
}
return Ok((Some(payload), preserve(stream_left)));
}
if total > in_buf {
return Err(ParsingErrorState::new(
ParsingError::ClearAndSkip(cursor + total),
Some(ParsingState::WebpPastHeader(stream_left - total)),
));
}
cursor += total;
stream_left -= total;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn minimal_tiff_le() -> Vec<u8> {
let mut t = Vec::new();
t.extend_from_slice(b"II");
t.extend_from_slice(&[0x2a, 0x00]);
t.extend_from_slice(&[0x08, 0, 0, 0]);
t.extend_from_slice(&[0, 0]);
t.extend_from_slice(&[0, 0, 0, 0]);
t
}
fn chunk(fourcc: &[u8; 4], data: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(fourcc);
out.extend_from_slice(&(data.len() as u32).to_le_bytes());
out.extend_from_slice(data);
if data.len() % 2 == 1 {
out.push(0); }
out
}
fn webp(chunks: &[Vec<u8>]) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(b"WEBP");
for c in chunks {
body.extend_from_slice(c);
}
let mut out = Vec::new();
out.extend_from_slice(b"RIFF");
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&body);
out
}
#[test]
fn no_exif_returns_none() {
let buf = webp(&[chunk(b"VP8X", &[0u8; 10])]);
let (exif, _) = extract_exif(None, &buf).unwrap();
assert!(exif.is_none());
}
#[test]
fn extracts_exif_payload() {
let tiff = minimal_tiff_le();
let buf = webp(&[chunk(b"VP8X", &[0u8; 10]), chunk(b"EXIF", &tiff)]);
let (exif, _) = extract_exif(None, &buf).unwrap();
assert_eq!(exif.unwrap(), tiff.as_slice());
}
#[test]
fn strips_exif_prefix() {
let tiff = minimal_tiff_le();
let mut payload = Vec::new();
payload.extend_from_slice(b"Exif\0\0");
payload.extend_from_slice(&tiff);
let buf = webp(&[chunk(b"EXIF", &payload)]);
let (exif, _) = extract_exif(None, &buf).unwrap();
assert_eq!(exif.unwrap(), tiff.as_slice());
}
#[test]
fn odd_length_chunk_padding_accounted() {
let tiff = minimal_tiff_le();
let buf = webp(&[chunk(b"ICCP", &[1, 2, 3]), chunk(b"EXIF", &tiff)]);
let (exif, _) = extract_exif(None, &buf).unwrap();
assert_eq!(exif.unwrap(), tiff.as_slice());
}
#[test]
fn bad_header_fails() {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&16u32.to_le_bytes());
buf.extend_from_slice(b"WAVE"); buf.extend_from_slice(&[0u8; 8]);
let err = extract_exif(None, &buf).unwrap_err();
assert!(matches!(
err.err,
ParsingError::Failed {
kind: MalformedKind::WebpChunk,
..
}
));
}
#[test]
fn truncated_header_needs_more() {
let buf = b"RIFF\x00\x00".to_vec();
let err = extract_exif(None, &buf).unwrap_err();
assert!(matches!(err.err, ParsingError::Need(_)));
}
#[test]
fn large_leading_chunk_clear_and_skips() {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&1_000_000u32.to_le_bytes());
buf.extend_from_slice(b"WEBP");
buf.extend_from_slice(b"VP8L");
buf.extend_from_slice(&50_000u32.to_le_bytes());
let err = extract_exif(None, &buf).unwrap_err();
match err.err {
ParsingError::ClearAndSkip(n) => assert_eq!(n, 12 + 8 + 50_000),
other => panic!("expected ClearAndSkip, got {other:?}"),
}
assert!(matches!(err.state, Some(ParsingState::WebpPastHeader(_))));
}
#[test]
fn resumes_past_header_finds_exif() {
let tiff = minimal_tiff_le();
let exif_chunk = chunk(b"EXIF", &tiff);
let stream_left = exif_chunk.len();
let (exif, _) =
extract_exif(Some(ParsingState::WebpPastHeader(stream_left)), &exif_chunk).unwrap();
assert_eq!(exif.unwrap(), tiff.as_slice());
}
#[test]
fn size_max_does_not_panic() {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&u32::MAX.to_le_bytes());
buf.extend_from_slice(b"WEBP");
buf.extend_from_slice(b"VP8L");
buf.extend_from_slice(&u32::MAX.to_le_bytes());
let _ = extract_exif(None, &buf);
}
#[test]
fn header_only_need_then_resume_finds_exif() {
let tiff = minimal_tiff_le();
let full = webp(&[chunk(b"EXIF", &tiff)]);
let header_only = &full[..WEBP_HEADER_LEN];
let err = extract_exif(None, header_only).unwrap_err();
assert!(matches!(err.err, ParsingError::Need(_)));
assert!(
err.state.is_none(),
"initial-path Need must not carry WebpPastHeader"
);
let (exif, _) = extract_exif(err.state, &full).unwrap();
assert_eq!(exif.unwrap(), tiff.as_slice());
}
#[test]
fn non_exif_chunk_exceeding_riff_bound_returns_none() {
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&16u32.to_le_bytes()); buf.extend_from_slice(b"WEBP");
buf.extend_from_slice(b"VP8L");
buf.extend_from_slice(&1000u32.to_le_bytes());
let (exif, _) = extract_exif(None, &buf).unwrap();
assert!(exif.is_none());
}
#[test]
fn exif_chunk_only_prefix_returns_none() {
let buf = webp(&[chunk(b"EXIF", b"Exif\0\0")]);
let (exif, _) = extract_exif(None, &buf).unwrap();
assert!(exif.is_none());
}
}