#![forbid(unsafe_code)]
pub mod boxes;
pub mod config;
pub mod fourcc;
use boxes::{bx, find, full_bx, push_u16, push_u32, rd_u16, rd_u32, walk};
pub use config::{Config, Subsampling};
use fourcc::{BRAND, COMPATIBLE_BRANDS, CONFIG_BOX, ITEM_TYPE};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
NotAv2f,
Malformed(&'static str),
Unsupported(String),
Invalid(&'static str),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::NotAv2f => write!(f, "not an AV2F file"),
Error::Malformed(w) => write!(f, "malformed AV2F: {w}"),
Error::Unsupported(w) => write!(f, "unsupported AV2F: {w}"),
Error::Invalid(w) => write!(f, "invalid AV2F: {w}"),
}
}
}
impl std::error::Error for Error {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Params {
pub width: u32,
pub height: u32,
pub config: Config,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Image<'a> {
pub width: u32,
pub height: u32,
pub config: Config,
pub payload: &'a [u8],
}
pub fn probe(data: &[u8]) -> i32 {
if data.len() < 12 || &data[4..8] != b"ftyp" {
return 0;
}
let size = match rd_u32(data, 0) {
Some(s) => (s as usize).clamp(12, data.len()),
None => return 0,
};
if &data[8..12] == BRAND {
return 100;
}
let mut i = 16;
while i + 4 <= size {
if &data[i..i + 4] == BRAND {
return 100;
}
i += 4;
}
match &data[8..12] {
b"mif1" | b"miaf" => 50,
_ => 0,
}
}
pub fn encode(params: &Params, payload: &[u8]) -> Result<Vec<u8>, Error> {
if params.width == 0 || params.height == 0 {
return Err(Error::Invalid("image dimensions are zero"));
}
if payload.is_empty() {
return Err(Error::Invalid("payload is empty"));
}
if !params.config.full_still_picture_header {
return Err(Error::Unsupported(
"compact still-picture header: rusty_av2d does not decode it bit-exactly yet, \
so writing it would produce a file our own decoder refuses. Encode with \
`avmenc --full-still-picture-hdr`."
.into(),
));
}
let mut ipco = Vec::new();
ipco.extend_from_slice(&ispe(params.width, params.height)); ipco.extend_from_slice(¶ms.config.to_box()); ipco.extend_from_slice(&pixi(params.config)); let ipco = bx(b"ipco", &ipco);
let mut iprp = Vec::new();
iprp.extend_from_slice(&ipco);
iprp.extend_from_slice(&ipma());
let iprp = bx(b"iprp", &iprp);
let (iloc, off_field_in_iloc) = build_iloc(payload.len() as u32);
let mut meta_body = Vec::new();
meta_body.extend_from_slice(&hdlr());
meta_body.extend_from_slice(&pitm());
let iloc_start = meta_body.len();
meta_body.extend_from_slice(&iloc);
meta_body.extend_from_slice(&iinf());
meta_body.extend_from_slice(&iprp);
let mut meta = full_bx(b"meta", 0, 0, &meta_body);
let ftyp = ftyp();
let off_field = 12 + iloc_start + off_field_in_iloc;
let mdat_data_offset = ftyp.len() + meta.len() + 8; meta.get_mut(off_field..off_field + 4)
.ok_or(Error::Malformed("iloc offset field out of range"))?
.copy_from_slice(&(mdat_data_offset as u32).to_be_bytes());
let mut file = Vec::with_capacity(ftyp.len() + meta.len() + 8 + payload.len());
file.extend_from_slice(&ftyp);
file.extend_from_slice(&meta);
file.extend_from_slice(&bx(b"mdat", payload));
Ok(file)
}
pub fn decode(data: &[u8]) -> Result<Image<'_>, Error> {
if probe(data) < 100 {
return Err(Error::NotAv2f);
}
let top = walk(data);
let meta = find(&top, b"meta").ok_or(Error::Malformed("no meta box"))?;
let meta_children = walk(meta.body.get(4..).ok_or(Error::Malformed("short meta"))?);
let iinf = find(&meta_children, b"iinf").ok_or(Error::Malformed("no iinf box"))?;
if !iinf
.body
.windows(4)
.any(|w| w == ITEM_TYPE.as_slice())
{
return Err(Error::Unsupported(format!(
"item type is not {}",
String::from_utf8_lossy(ITEM_TYPE)
)));
}
let iprp = find(&meta_children, b"iprp").ok_or(Error::Malformed("no iprp box"))?;
let ipco = find(&walk(iprp.body), b"ipco")
.map(|b| b.body.to_vec())
.ok_or(Error::Malformed("no ipco box"))?;
let props = walk(&ipco);
let ispe = find(&props, b"ispe").ok_or(Error::Malformed("no ispe property"))?;
let width = rd_u32(ispe.body, 4).ok_or(Error::Malformed("short ispe"))?;
let height = rd_u32(ispe.body, 8).ok_or(Error::Malformed("short ispe"))?;
if width == 0 || height == 0 {
return Err(Error::Invalid("image dimensions are zero"));
}
let cfg_box = find(&props, CONFIG_BOX).ok_or(Error::Malformed("no av2C property"))?;
let config = Config::from_body(cfg_box.body).ok_or_else(|| {
Error::Unsupported("av2C record was not written by this crate".into())
})?;
if !config.full_still_picture_header {
return Err(Error::Unsupported(
"payload uses the compact still-picture header, which rusty_av2d does not yet \
decode bit-exactly"
.into(),
));
}
let iloc = find(&meta_children, b"iloc").ok_or(Error::Malformed("no iloc box"))?;
let (off, len) = parse_iloc(iloc.body).ok_or(Error::Malformed("unsupported iloc layout"))?;
let payload = data
.get(off..off.checked_add(len).ok_or(Error::Malformed("iloc extent overflows"))?)
.ok_or(Error::Malformed("iloc extent is outside the file"))?;
if payload.is_empty() {
return Err(Error::Invalid("payload is empty"));
}
Ok(Image {
width,
height,
config,
payload,
})
}
fn ftyp() -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(BRAND);
push_u32(&mut b, 0); for brand in COMPATIBLE_BRANDS {
b.extend_from_slice(brand);
}
bx(b"ftyp", &b)
}
fn hdlr() -> Vec<u8> {
let mut b = Vec::new();
push_u32(&mut b, 0); b.extend_from_slice(b"pict"); push_u32(&mut b, 0);
push_u32(&mut b, 0);
push_u32(&mut b, 0); b.push(0); full_bx(b"hdlr", 0, 0, &b)
}
fn pitm() -> Vec<u8> {
let mut b = Vec::new();
push_u16(&mut b, 1); full_bx(b"pitm", 0, 0, &b)
}
fn iinf() -> Vec<u8> {
let mut infe = Vec::new();
push_u16(&mut infe, 1); push_u16(&mut infe, 0); infe.extend_from_slice(ITEM_TYPE);
infe.push(0); let infe = full_bx(b"infe", 2, 0, &infe);
let mut b = Vec::new();
push_u16(&mut b, 1); b.extend_from_slice(&infe);
full_bx(b"iinf", 0, 0, &b)
}
fn ispe(width: u32, height: u32) -> Vec<u8> {
let mut b = Vec::new();
push_u32(&mut b, width);
push_u32(&mut b, height);
full_bx(b"ispe", 0, 0, &b)
}
fn pixi(cfg: Config) -> Vec<u8> {
let n = cfg.subsampling.channels();
let mut b = Vec::with_capacity(1 + n as usize);
b.push(n);
for _ in 0..n {
b.push(cfg.bit_depth);
}
full_bx(b"pixi", 0, 0, &b)
}
fn ipma() -> Vec<u8> {
let mut b = Vec::new();
push_u32(&mut b, 1); push_u16(&mut b, 1); b.push(3); b.push(1); b.push(0x80 | 2); b.push(3); full_bx(b"ipma", 0, 0, &b)
}
fn build_iloc(length: u32) -> (Vec<u8>, usize) {
let mut body = Vec::new();
body.push((4 << 4) | 4); body.push(0); push_u16(&mut body, 1); push_u16(&mut body, 1); push_u16(&mut body, 0); push_u16(&mut body, 1); let off_field = body.len();
push_u32(&mut body, 0); push_u32(&mut body, length); (full_bx(b"iloc", 0, 0, &body), 12 + off_field)
}
fn parse_iloc(body: &[u8]) -> Option<(usize, usize)> {
let sizes = *body.get(4)?;
if sizes != ((4 << 4) | 4) || *body.get(5)? != 0 {
return None; }
if rd_u16(body, 6)? != 1 {
return None; }
if rd_u16(body, 12)? != 1 {
return None; }
let off = rd_u32(body, 14)? as usize;
let len = rd_u32(body, 18)? as usize;
Some((off, len))
}
#[cfg(test)]
mod tests {
use super::*;
fn params() -> Params {
Params {
width: 432,
height: 240,
config: Config::default(),
}
}
#[test]
fn round_trips_a_payload_exactly() {
let payload: Vec<u8> = (0..5000u32).map(|i| (i % 251) as u8).collect();
let file = encode(¶ms(), &payload).unwrap();
let img = decode(&file).unwrap();
assert_eq!(img.width, 432);
assert_eq!(img.height, 240);
assert_eq!(img.config, Config::default());
assert_eq!(
img.payload, &payload[..],
"the coded payload must survive the container byte-for-byte"
);
}
#[test]
fn probes_its_own_output() {
let file = encode(¶ms(), &[1, 2, 3, 4]).unwrap();
assert_eq!(probe(&file), 100);
assert_eq!(&file[4..8], b"ftyp");
assert_eq!(&file[8..12], BRAND);
}
#[test]
fn every_fourcc_comes_from_the_fourcc_module() {
let file = encode(¶ms(), &[9; 64]).unwrap();
let find_all = |needle: &[u8; 4]| {
file.windows(4).filter(|w| *w == needle.as_slice()).count()
};
assert!(find_all(BRAND) >= 2, "brand appears as major + compatible");
assert_eq!(find_all(ITEM_TYPE), 1, "item type appears once, in infe");
assert_eq!(find_all(CONFIG_BOX), 1, "config box type appears once");
}
#[test]
fn rejects_the_compact_still_picture_header() {
let cfg = Config {
full_still_picture_header: false,
..Config::default()
};
let p = Params {
config: cfg,
..params()
};
assert!(matches!(encode(&p, &[1, 2, 3]), Err(Error::Unsupported(_))));
}
#[test]
fn rejects_zero_dimensions_and_empty_payload() {
let p = Params {
width: 0,
..params()
};
assert!(matches!(encode(&p, &[1]), Err(Error::Invalid(_))));
assert!(matches!(encode(¶ms(), &[]), Err(Error::Invalid(_))));
}
#[test]
fn rejects_foreign_files() {
assert!(matches!(decode(b"not a file at all"), Err(Error::NotAv2f)));
let mut avif = vec![0, 0, 0, 32];
avif.extend_from_slice(b"ftypavif");
avif.extend_from_slice(&[0; 20]);
assert!(matches!(decode(&avif), Err(Error::NotAv2f)));
}
#[test]
fn truncation_never_panics() {
let payload: Vec<u8> = (0..2000u32).map(|i| i as u8).collect();
let file = encode(¶ms(), &payload).unwrap();
for cut in 0..file.len() {
let _ = decode(&file[..cut]);
}
}
#[test]
fn corrupt_bytes_never_panic() {
let payload: Vec<u8> = (0..1500u32).map(|i| i as u8).collect();
let file = encode(¶ms(), &payload).unwrap();
let mut rng = 0x2545F4914F6CDD1Du64;
for _ in 0..4000 {
let mut f = file.clone();
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
let at = (rng >> 33) as usize % f.len();
f[at] ^= ((rng >> 11) & 0xFF) as u8;
let _ = decode(&f);
}
}
#[test]
fn subsampling_and_depth_survive() {
for ss in [
Subsampling::Yuv420,
Subsampling::Yuv422,
Subsampling::Yuv444,
Subsampling::Mono,
] {
for bd in [8u8, 10, 12] {
let cfg = Config {
bit_depth: bd,
subsampling: ss,
full_still_picture_header: true,
};
let p = Params {
config: cfg,
..params()
};
let file = encode(&p, &[7; 32]).unwrap();
assert_eq!(decode(&file).unwrap().config, cfg, "{ss:?} {bd}-bit");
}
}
}
}