pub(crate) mod ancillary;
pub(crate) mod ihdr;
pub(crate) mod write;
use alloc::vec::Vec;
use zenflate::crc32;
use crate::error::PngError;
pub(crate) const PNG_SIGNATURE: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10];
#[derive(Clone, Copy, Debug)]
pub(crate) struct ChunkRef<'a> {
pub chunk_type: [u8; 4],
pub data: &'a [u8],
}
pub(crate) struct ChunkIter<'a> {
data: &'a [u8],
pos: usize,
skip_critical_crc: bool,
pub warnings: Vec<crate::decode::PngWarning>,
}
impl<'a> ChunkIter<'a> {
pub fn new(data: &'a [u8]) -> Self {
Self {
data,
pos: 8, skip_critical_crc: false,
warnings: Vec::new(),
}
}
pub fn new_with_config(data: &'a [u8], skip_critical_crc: bool) -> Self {
Self {
data,
pos: 8,
skip_critical_crc,
warnings: Vec::new(),
}
}
pub fn pos(&self) -> usize {
self.pos
}
}
impl<'a> Iterator for ChunkIter<'a> {
type Item = Result<ChunkRef<'a>, PngError>;
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.pos >= self.data.len() {
return None;
}
if self.pos + 12 > self.data.len() {
return Some(Err(PngError::Decode("truncated chunk header".into())));
}
let length =
u32::from_be_bytes(self.data[self.pos..self.pos + 4].try_into().unwrap()) as usize;
let chunk_type: [u8; 4] = self.data[self.pos + 4..self.pos + 8].try_into().unwrap();
let data_start = self.pos + 8;
let Some(data_end) = data_start.checked_add(length) else {
return Some(Err(PngError::Decode(alloc::format!(
"chunk length overflow at offset {}",
self.pos
))));
};
let Some(crc_end) = data_end.checked_add(4) else {
return Some(Err(PngError::Decode(alloc::format!(
"chunk length overflow at offset {}",
self.pos
))));
};
if crc_end > self.data.len() {
return Some(Err(PngError::Decode(alloc::format!(
"truncated chunk {:?} at offset {}",
core::str::from_utf8(&chunk_type).unwrap_or("????"),
self.pos
))));
}
let chunk_data = &self.data[data_start..data_end];
let stored_crc = u32::from_be_bytes(self.data[data_end..crc_end].try_into().unwrap());
if self.skip_critical_crc {
} else {
let computed_crc = crc32(crc32(0, &chunk_type), chunk_data);
if stored_crc != computed_crc {
let is_ancillary = chunk_type[0] & 0x20 != 0;
if is_ancillary {
self.pos = crc_end;
continue; }
return Some(Err(PngError::Decode(alloc::format!(
"CRC mismatch in {:?} chunk at offset {}",
core::str::from_utf8(&chunk_type).unwrap_or("????"),
self.pos
))));
}
}
self.pos = crc_end;
return Some(Ok(ChunkRef {
chunk_type,
data: chunk_data,
}));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_chunk(chunk_type: &[u8; 4], data: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&(data.len() as u32).to_be_bytes());
buf.extend_from_slice(chunk_type);
buf.extend_from_slice(data);
let crc = crc32(crc32(0, chunk_type), data);
buf.extend_from_slice(&crc.to_be_bytes());
buf
}
fn make_png_with_chunks(chunks: &[Vec<u8>]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&PNG_SIGNATURE);
for chunk in chunks {
buf.extend_from_slice(chunk);
}
buf
}
#[test]
fn iter_empty_data_returns_none() {
let data = PNG_SIGNATURE.to_vec();
let mut iter = ChunkIter::new(&data);
assert!(iter.next().is_none());
}
#[test]
fn iter_truncated_header() {
let mut data = PNG_SIGNATURE.to_vec();
data.extend_from_slice(&[0; 5]);
let mut iter = ChunkIter::new(&data);
let result = iter.next();
assert!(result.is_some());
assert!(result.unwrap().is_err());
}
#[test]
fn iter_ancillary_bad_crc_is_skipped() {
let mut text_chunk = make_chunk(b"tEXt", b"hello");
let len = text_chunk.len();
text_chunk[len - 1] ^= 0xFF;
let iend_chunk = make_chunk(b"IEND", &[]);
let data = make_png_with_chunks(&[text_chunk, iend_chunk]);
let mut iter = ChunkIter::new(&data);
let chunk = iter.next().unwrap().unwrap();
assert_eq!(&chunk.chunk_type, b"IEND");
}
#[test]
fn iter_critical_bad_crc_is_error() {
let ihdr_data = vec![0u8; 13]; let mut ihdr_chunk = make_chunk(b"IHDR", &ihdr_data);
let len = ihdr_chunk.len();
ihdr_chunk[len - 1] ^= 0xFF;
let data = make_png_with_chunks(&[ihdr_chunk]);
let mut iter = ChunkIter::new(&data);
let result = iter.next();
assert!(result.is_some());
let err = result.unwrap().unwrap_err();
assert!(err.to_string().contains("CRC mismatch"));
}
#[test]
fn iter_valid_chunk_returns_data() {
let iend_chunk = make_chunk(b"IEND", &[]);
let data = make_png_with_chunks(&[iend_chunk]);
let mut iter = ChunkIter::new(&data);
let chunk = iter.next().unwrap().unwrap();
assert_eq!(&chunk.chunk_type, b"IEND");
assert_eq!(chunk.data.len(), 0);
assert!(iter.next().is_none());
}
#[test]
fn iter_skip_crc_does_not_check() {
let ihdr_data = vec![0u8; 13];
let mut ihdr_chunk = make_chunk(b"IHDR", &ihdr_data);
let len = ihdr_chunk.len();
ihdr_chunk[len - 1] ^= 0xFF;
let data = make_png_with_chunks(&[ihdr_chunk]);
let mut iter = ChunkIter::new_with_config(&data, true);
let chunk = iter.next().unwrap().unwrap();
assert_eq!(&chunk.chunk_type, b"IHDR");
}
#[test]
fn pos_advances_correctly() {
let chunk1 = make_chunk(b"tEXt", b"hello");
let chunk2 = make_chunk(b"IEND", &[]);
let data = make_png_with_chunks(&[chunk1.clone(), chunk2]);
let mut iter = ChunkIter::new(&data);
assert_eq!(iter.pos(), 8); let _ = iter.next().unwrap().unwrap();
assert_eq!(iter.pos(), 8 + chunk1.len());
}
#[test]
fn many_bad_crc_ancillary_chunks_no_stack_overflow() {
let mut chunks = Vec::new();
for _ in 0..10_000 {
let mut bad_text = make_chunk(b"tEXt", b"x");
let len = bad_text.len();
bad_text[len - 1] ^= 0xFF; chunks.push(bad_text);
}
chunks.push(make_chunk(b"IEND", &[]));
let data = make_png_with_chunks(&chunks);
let mut iter = ChunkIter::new(&data);
let chunk = iter.next().unwrap().unwrap();
assert_eq!(&chunk.chunk_type, b"IEND");
}
}