use crate::check::Check;
use crate::container::CHUNK_FLAG_ENCRYPTED;
use crate::error::{Error, Result};
pub const PNG_SIGNATURE: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
pub const PREVIEW_ROLE_MASK: u32 = 0x0F;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PreviewRole {
#[default]
Unspecified = 0,
Large = 1,
Small = 2,
Icon = 3,
}
impl PreviewRole {
pub fn from_flags(flags: u32) -> Result<PreviewRole> {
let reserved = !(PREVIEW_ROLE_MASK | CHUNK_FLAG_ENCRYPTED);
if flags & reserved != 0 {
return Err(Error::new(
Check::PrevFlags,
format!("preview flags {flags:#010x} set a reserved bit"),
));
}
match flags & PREVIEW_ROLE_MASK {
0 => Ok(PreviewRole::Unspecified),
1 => Ok(PreviewRole::Large),
2 => Ok(PreviewRole::Small),
3 => Ok(PreviewRole::Icon),
role => Err(Error::new(
Check::PrevFlags,
format!("preview role {role} is reserved"),
)),
}
}
pub fn to_flags(self) -> u32 {
self as u32
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Preview {
pub role: PreviewRole,
pub png: Vec<u8>,
}
pub fn is_png(payload: &[u8]) -> bool {
payload.len() >= 8 && payload[..8] == PNG_SIGNATURE
}
pub fn check_png(payload: &[u8]) -> Result<()> {
if payload.len() < 25 || !is_png(payload) {
return Err(Error::new(
Check::PrevPngSignature,
format!(
"a {} byte payload does not begin with the PNG signature",
payload.len()
),
));
}
let declared = u32::from_be_bytes([payload[8], payload[9], payload[10], payload[11]]);
if declared != 13 {
return Err(Error::new(
Check::PrevPngSignature,
format!("the first PNG chunk declares {declared} bytes; IHDR is 13"),
));
}
if &payload[12..16] != b"IHDR" {
return Err(Error::new(
Check::PrevPngSignature,
"the first PNG chunk is not IHDR",
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn minimal_png() -> Vec<u8> {
let mut png = Vec::from(PNG_SIGNATURE);
png.extend_from_slice(&13u32.to_be_bytes());
png.extend_from_slice(b"IHDR");
png.extend_from_slice(&1u32.to_be_bytes()); png.extend_from_slice(&1u32.to_be_bytes()); png.extend_from_slice(&[8, 6, 0, 0, 0]); png
}
#[test]
fn check_png_accepts_a_minimal_png_and_rejects_other_bytes() {
assert!(check_png(&minimal_png()).is_ok());
assert_eq!(
check_png(b"this is not a PNG at all, not even close")
.unwrap_err()
.check(),
Check::PrevPngSignature
);
let mut wrong_type = minimal_png();
wrong_type[12..16].copy_from_slice(b"IDAT");
assert_eq!(
check_png(&wrong_type).unwrap_err().check(),
Check::PrevPngSignature
);
let mut wrong_len = minimal_png();
wrong_len[8..12].copy_from_slice(&14u32.to_be_bytes());
assert_eq!(
check_png(&wrong_len).unwrap_err().check(),
Check::PrevPngSignature
);
}
#[test]
fn preview_roles_reject_reserved_values_but_allow_sealing() {
assert_eq!(PreviewRole::from_flags(1).unwrap(), PreviewRole::Large);
assert_eq!(
PreviewRole::from_flags(4).unwrap_err().check(),
Check::PrevFlags
);
assert_eq!(
PreviewRole::from_flags(15).unwrap_err().check(),
Check::PrevFlags
);
assert_eq!(
PreviewRole::from_flags(1 | (1 << 5)).unwrap_err().check(),
Check::PrevFlags
);
let flags = PreviewRole::Icon.to_flags() | CHUNK_FLAG_ENCRYPTED;
assert_eq!(PreviewRole::from_flags(flags).unwrap(), PreviewRole::Icon);
}
}