use core::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Vp8xError {
BadPayloadLength {
got: usize,
},
CanvasTooLarge {
canvas_width: u32,
canvas_height: u32,
},
}
impl fmt::Display for Vp8xError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BadPayloadLength { got } => write!(
f,
"VP8X payload must be 10 bytes per §2.7.1 Figure 7, got {got}"
),
Self::CanvasTooLarge {
canvas_width,
canvas_height,
} => write!(
f,
"§2.7.1 canvas size {canvas_width}x{canvas_height} \
exceeds the 2^32 - 1 product cap"
),
}
}
}
impl std::error::Error for Vp8xError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Vp8xHeader {
pub canvas_width: u32,
pub canvas_height: u32,
pub has_iccp: bool,
pub has_animation: bool,
pub has_exif: bool,
pub has_xmp: bool,
pub has_alpha: bool,
pub has_unknown: bool,
}
impl Vp8xHeader {
pub fn parse(payload: &[u8]) -> Result<Self, Vp8xError> {
if payload.len() != 10 {
return Err(Vp8xError::BadPayloadLength { got: payload.len() });
}
let flags = payload[0];
let reserved_lo = payload[1];
let reserved_mid = payload[2];
let reserved_hi = payload[3];
let has_iccp = (flags & 0b0010_0000) != 0;
let has_alpha = (flags & 0b0001_0000) != 0;
let has_exif = (flags & 0b0000_1000) != 0;
let has_xmp = (flags & 0b0000_0100) != 0;
let has_animation = (flags & 0b0000_0010) != 0;
let reserved_flag_bits = flags & 0b1100_0001;
let has_unknown =
reserved_flag_bits != 0 || reserved_lo != 0 || reserved_mid != 0 || reserved_hi != 0;
let cwm1 =
u32::from(payload[4]) | (u32::from(payload[5]) << 8) | (u32::from(payload[6]) << 16);
let chm1 =
u32::from(payload[7]) | (u32::from(payload[8]) << 8) | (u32::from(payload[9]) << 16);
let canvas_width = cwm1 + 1;
let canvas_height = chm1 + 1;
if (canvas_width as u64) * (canvas_height as u64) > u64::from(u32::MAX) {
return Err(Vp8xError::CanvasTooLarge {
canvas_width,
canvas_height,
});
}
Ok(Self {
canvas_width,
canvas_height,
has_iccp,
has_animation,
has_exif,
has_xmp,
has_alpha,
has_unknown,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn vp8x(flags: u8, cwm1: u32, chm1: u32, reserved24: u32) -> Vec<u8> {
vec![
flags,
(reserved24 & 0xFF) as u8,
((reserved24 >> 8) & 0xFF) as u8,
((reserved24 >> 16) & 0xFF) as u8,
(cwm1 & 0xFF) as u8,
((cwm1 >> 8) & 0xFF) as u8,
((cwm1 >> 16) & 0xFF) as u8,
(chm1 & 0xFF) as u8,
((chm1 >> 8) & 0xFF) as u8,
((chm1 >> 16) & 0xFF) as u8,
]
}
#[test]
fn smallest_canvas_one_by_one_with_no_flags() {
let h = Vp8xHeader::parse(&vp8x(0x00, 0, 0, 0)).unwrap();
assert_eq!(h.canvas_width, 1);
assert_eq!(h.canvas_height, 1);
assert!(!h.has_iccp);
assert!(!h.has_alpha);
assert!(!h.has_exif);
assert!(!h.has_xmp);
assert!(!h.has_animation);
assert!(!h.has_unknown);
}
#[test]
fn flag_bit_assignments_match_2_7_1_letters() {
let i = Vp8xHeader::parse(&vp8x(0b0010_0000, 0, 0, 0)).unwrap();
assert!(i.has_iccp);
assert!(!i.has_alpha && !i.has_exif && !i.has_xmp && !i.has_animation);
assert!(!i.has_unknown);
let l = Vp8xHeader::parse(&vp8x(0b0001_0000, 0, 0, 0)).unwrap();
assert!(l.has_alpha);
assert!(!l.has_iccp && !l.has_exif && !l.has_xmp && !l.has_animation);
let e = Vp8xHeader::parse(&vp8x(0b0000_1000, 0, 0, 0)).unwrap();
assert!(e.has_exif);
assert!(!e.has_iccp && !e.has_alpha && !e.has_xmp && !e.has_animation);
let x = Vp8xHeader::parse(&vp8x(0b0000_0100, 0, 0, 0)).unwrap();
assert!(x.has_xmp);
assert!(!x.has_iccp && !x.has_alpha && !x.has_exif && !x.has_animation);
let a = Vp8xHeader::parse(&vp8x(0b0000_0010, 0, 0, 0)).unwrap();
assert!(a.has_animation);
assert!(!a.has_iccp && !a.has_alpha && !a.has_exif && !a.has_xmp);
}
#[test]
fn multiple_feature_flags_combine_independently() {
let h = Vp8xHeader::parse(&vp8x(0b0001_0010, 63, 63, 0)).unwrap();
assert_eq!(h.canvas_width, 64);
assert_eq!(h.canvas_height, 64);
assert!(h.has_alpha);
assert!(h.has_animation);
assert!(!h.has_iccp);
assert!(!h.has_exif);
assert!(!h.has_xmp);
assert!(!h.has_unknown);
}
#[test]
fn canvas_dims_are_one_based_24bit_little_endian() {
let h = Vp8xHeader::parse(&vp8x(0x00, 0x00ABCC, 0x000123, 0)).unwrap();
assert_eq!(h.canvas_width, 0x00ABCD);
assert_eq!(h.canvas_height, 0x000124);
}
#[test]
fn maximum_24bit_canvas_dim_decodes_then_trips_product_cap() {
let err = Vp8xHeader::parse(&vp8x(0x00, 0x00FF_FFFF, 0x00FF_FFFF, 0)).unwrap_err();
assert_eq!(
err,
Vp8xError::CanvasTooLarge {
canvas_width: 0x0100_0000,
canvas_height: 0x0100_0000,
}
);
let h = Vp8xHeader::parse(&vp8x(0x00, 0x00FF_FFFF, 0, 0)).unwrap();
assert_eq!(h.canvas_width, 0x0100_0000);
assert_eq!(h.canvas_height, 1);
}
#[test]
fn canvas_product_above_2_32_minus_1_is_rejected() {
let err = Vp8xHeader::parse(&vp8x(0x00, 65_535, 65_535, 0)).unwrap_err();
assert_eq!(
err,
Vp8xError::CanvasTooLarge {
canvas_width: 65_536,
canvas_height: 65_536,
}
);
}
#[test]
fn canvas_product_at_2_32_minus_1_is_accepted() {
let h = Vp8xHeader::parse(&vp8x(0x00, 65_535, 65_534, 0)).unwrap();
assert_eq!(h.canvas_width, 65_536);
assert_eq!(h.canvas_height, 65_535);
}
#[test]
fn reserved_bits_in_flag_octet_set_has_unknown_but_do_not_reject() {
for flag in [0b1000_0000, 0b0100_0000, 0b1100_0000, 0b0000_0001] {
let h = Vp8xHeader::parse(&vp8x(flag, 0, 0, 0)).unwrap();
assert!(h.has_unknown, "flag=0x{flag:02x}");
assert!(!h.has_iccp);
assert!(!h.has_alpha);
assert!(!h.has_exif);
assert!(!h.has_xmp);
assert!(!h.has_animation);
}
}
#[test]
fn reserved_24bit_field_set_sets_has_unknown_but_does_not_reject() {
let h = Vp8xHeader::parse(&vp8x(0x00, 7, 7, 0x12_3456)).unwrap();
assert!(h.has_unknown);
assert_eq!(h.canvas_width, 8);
assert_eq!(h.canvas_height, 8);
}
#[test]
fn payload_must_be_exactly_ten_bytes() {
assert_eq!(
Vp8xHeader::parse(&[]),
Err(Vp8xError::BadPayloadLength { got: 0 })
);
assert_eq!(
Vp8xHeader::parse(&[0u8; 9]),
Err(Vp8xError::BadPayloadLength { got: 9 })
);
assert_eq!(
Vp8xHeader::parse(&[0u8; 11]),
Err(Vp8xError::BadPayloadLength { got: 11 })
);
}
fn parse_at_byte(flags: u8, cwm1: u32, chm1: u32) -> Vp8xHeader {
Vp8xHeader::parse(&vp8x(flags, cwm1, chm1, 0)).unwrap()
}
#[test]
fn fixture_extended_with_exif_decode_matches_trace() {
let h = parse_at_byte(0x08, 127, 127);
assert_eq!(h.canvas_width, 128);
assert_eq!(h.canvas_height, 128);
assert!(h.has_exif);
assert!(!h.has_iccp);
assert!(!h.has_xmp);
assert!(!h.has_alpha);
assert!(!h.has_animation);
}
#[test]
fn fixture_extended_with_icc_profile_decode_matches_trace() {
let h = parse_at_byte(0x20, 127, 127);
assert!(h.has_iccp);
assert!(!h.has_exif && !h.has_xmp && !h.has_alpha && !h.has_animation);
}
#[test]
fn fixture_extended_with_xmp_decode_matches_trace() {
let h = parse_at_byte(0x04, 127, 127);
assert!(h.has_xmp);
assert!(!h.has_iccp && !h.has_exif && !h.has_alpha && !h.has_animation);
}
#[test]
fn fixture_lossy_with_alpha_128x128_decode_matches_trace() {
let h = parse_at_byte(0x10, 127, 127);
assert!(h.has_alpha);
assert!(!h.has_iccp && !h.has_exif && !h.has_xmp && !h.has_animation);
}
#[test]
fn fixture_animated_with_alpha_decode_matches_trace() {
let h = parse_at_byte(0x12, 63, 63);
assert_eq!(h.canvas_width, 64);
assert_eq!(h.canvas_height, 64);
assert!(h.has_alpha);
assert!(h.has_animation);
assert!(!h.has_iccp && !h.has_exif && !h.has_xmp);
}
}