pub const PNG_SIGNATURE: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10];
pub const MAX_IDAT_SIZE: usize = 50 * 1024 * 1024;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct PngInfo {
pub width: u32,
pub height: u32,
pub bit_depth: u8,
pub color_type: u8,
pub channels: u8,
pub idat_data: Vec<u8>,
}
pub fn is_png(data: &[u8]) -> bool {
data.len() >= 8 && data[..8] == PNG_SIGNATURE
}
pub fn parse_png(data: &[u8]) -> Option<PngInfo> {
parse_png_with_limit(data, MAX_IDAT_SIZE)
}
fn parse_png_with_limit(data: &[u8], max_idat: usize) -> Option<PngInfo> {
if !is_png(data) {
return None;
}
let mut pos = 8; let mut width = 0u32;
let mut height = 0u32;
let mut bit_depth = 0u8;
let mut color_type = 0u8;
let mut ihdr_found = false;
let mut idat_data = Vec::new();
while pos + 8 <= data.len() {
let chunk_len = read_u32_be(data, pos) as usize;
let chunk_type = &data[pos + 4..pos + 8];
if pos + 12 + chunk_len > data.len() {
break;
}
let chunk_data_start = pos + 8;
match chunk_type {
b"IHDR" => {
if chunk_len < 13 {
return None;
}
width = read_u32_be(data, chunk_data_start);
height = read_u32_be(data, chunk_data_start + 4);
bit_depth = data[chunk_data_start + 8];
color_type = data[chunk_data_start + 9];
ihdr_found = true;
}
b"IDAT" => {
if idat_data.len() + chunk_len > max_idat {
return None; }
idat_data.extend_from_slice(&data[chunk_data_start..chunk_data_start + chunk_len]);
}
b"IEND" => {
break;
}
_ => {}
}
pos += 12 + chunk_len;
}
if !ihdr_found || idat_data.is_empty() || width == 0 || height == 0 {
return None;
}
let channels = match color_type {
0 => 1, 2 => 3, 4 => 2, 6 => 4, _ => return None, };
Some(PngInfo {
width,
height,
bit_depth,
color_type,
channels,
idat_data,
})
}
fn read_u32_be(data: &[u8], offset: usize) -> u32 {
((data[offset] as u32) << 24)
| ((data[offset + 1] as u32) << 16)
| ((data[offset + 2] as u32) << 8)
| (data[offset + 3] as u32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn png_signature_detection() {
assert!(is_png(&PNG_SIGNATURE));
assert!(is_png(&[137, 80, 78, 71, 13, 10, 26, 10, 0, 0]));
assert!(!is_png(&[0, 0, 0, 0, 0, 0, 0, 0]));
assert!(!is_png(&[0xFF, 0xD8, 0xFF])); assert!(!is_png(&[]));
assert!(!is_png(&[137, 80])); }
#[test]
fn parse_invalid_data() {
assert!(parse_png(&[]).is_none());
assert!(parse_png(&[0, 1, 2, 3]).is_none());
assert!(parse_png(&PNG_SIGNATURE).is_none());
}
#[test]
fn parse_minimal_valid_png() {
let png = build_test_png(
1,
1,
8,
2,
&[
0x78, 0x01, 0x62, 0x60, 0x60, 0x60, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01,
],
);
let info = parse_png(&png).unwrap();
assert_eq!(info.width, 1);
assert_eq!(info.height, 1);
assert_eq!(info.bit_depth, 8);
assert_eq!(info.color_type, 2);
assert_eq!(info.channels, 3);
assert!(!info.idat_data.is_empty());
}
#[test]
fn parse_grayscale_png() {
let png = build_test_png(2, 2, 8, 0, &[0x78, 0x01, 0x01, 0x00, 0x00]);
let info = parse_png(&png).unwrap();
assert_eq!(info.color_type, 0);
assert_eq!(info.channels, 1);
}
#[test]
fn parse_rgba_png() {
let png = build_test_png(2, 2, 8, 6, &[0x78, 0x01, 0x01, 0x00, 0x00]);
let info = parse_png(&png).unwrap();
assert_eq!(info.color_type, 6);
assert_eq!(info.channels, 4);
}
#[test]
fn parse_gray_alpha_png() {
let png = build_test_png(2, 2, 8, 4, &[0x78, 0x01, 0x01, 0x00, 0x00]);
let info = parse_png(&png).unwrap();
assert_eq!(info.color_type, 4);
assert_eq!(info.channels, 2);
}
#[test]
fn parse_unsupported_color_type() {
let png = build_test_png(1, 1, 8, 3, &[0x78, 0x01, 0x01, 0x00, 0x00]);
assert!(parse_png(&png).is_none());
}
#[test]
fn parse_multiple_idat_chunks() {
let mut png = Vec::new();
png.extend_from_slice(&PNG_SIGNATURE);
let ihdr_data = build_ihdr(4, 4, 8, 2);
append_chunk(&mut png, b"IHDR", &ihdr_data);
let idat1 = [0x78, 0x01, 0x62];
append_chunk(&mut png, b"IDAT", &idat1);
let idat2 = [0x60, 0x60, 0x00];
append_chunk(&mut png, b"IDAT", &idat2);
append_chunk(&mut png, b"IEND", &[]);
let info = parse_png(&png).unwrap();
assert_eq!(info.width, 4);
assert_eq!(info.height, 4);
assert_eq!(info.idat_data, [0x78, 0x01, 0x62, 0x60, 0x60, 0x00]);
}
#[test]
fn parse_ihdr_too_short() {
let mut png = Vec::new();
png.extend_from_slice(&PNG_SIGNATURE);
append_chunk(&mut png, b"IHDR", &[0; 5]);
append_chunk(&mut png, b"IEND", &[]);
assert!(parse_png(&png).is_none());
}
fn build_test_png(
width: u32,
height: u32,
bit_depth: u8,
color_type: u8,
idat_data: &[u8],
) -> Vec<u8> {
let mut png = Vec::new();
png.extend_from_slice(&PNG_SIGNATURE);
let ihdr_data = build_ihdr(width, height, bit_depth, color_type);
append_chunk(&mut png, b"IHDR", &ihdr_data);
append_chunk(&mut png, b"IDAT", idat_data);
append_chunk(&mut png, b"IEND", &[]);
png
}
fn build_ihdr(width: u32, height: u32, bit_depth: u8, color_type: u8) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&width.to_be_bytes());
data.extend_from_slice(&height.to_be_bytes());
data.push(bit_depth);
data.push(color_type);
data.push(0); data.push(0); data.push(0); data
}
fn append_chunk(buf: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) {
buf.extend_from_slice(&(data.len() as u32).to_be_bytes());
buf.extend_from_slice(chunk_type);
buf.extend_from_slice(data);
buf.extend_from_slice(&[0, 0, 0, 0]);
}
#[test]
fn parse_png_idat_within_limit() {
let idat_payload = vec![0x78; 32]; let mut png = Vec::new();
png.extend_from_slice(&PNG_SIGNATURE);
let ihdr_data = build_ihdr(1, 1, 8, 2);
append_chunk(&mut png, b"IHDR", &ihdr_data);
append_chunk(&mut png, b"IDAT", &idat_payload);
append_chunk(&mut png, b"IEND", &[]);
let info = parse_png_with_limit(&png, 64).unwrap();
assert_eq!(info.idat_data.len(), 32);
}
#[test]
fn parse_png_idat_exceeds_limit() {
let mut png = Vec::new();
png.extend_from_slice(&PNG_SIGNATURE);
let ihdr_data = build_ihdr(1, 1, 8, 2);
append_chunk(&mut png, b"IHDR", &ihdr_data);
let chunk = vec![0xAA; 20];
append_chunk(&mut png, b"IDAT", &chunk); append_chunk(&mut png, b"IDAT", &chunk); append_chunk(&mut png, b"IDAT", &chunk); append_chunk(&mut png, b"IEND", &[]);
assert!(parse_png_with_limit(&png, 50).is_none());
}
}