use std::{
fmt,
io::{Cursor, Read, Seek},
};
use anyhow::{Context, Result, bail};
use binrw::{BinRead, NullString, binread};
#[binread]
#[derive(Debug)]
#[br(little)]
pub struct RiffChunkU32 {
#[br(temp)]
data_size: u32,
#[br(try_calc = usize::try_from(data_size / 4), temp)]
data_length: usize,
#[br(count = data_length)]
pub data: Vec<u32>,
}
#[binread]
#[derive(Debug)]
#[br(little)]
pub struct RiffChunkU8 {
#[br(temp)]
data_size: u32,
#[br(count = data_size, pad_after = data_size % 2)]
pub data: Vec<u8>,
}
#[derive(Debug, Default, PartialEq, BinRead)]
#[br(little)]
#[br(repr = u32)]
enum AniFlags {
#[default]
Unsequenced = 1,
Sequenced = 3,
}
#[binread]
#[derive(Debug, Default, PartialEq)]
#[br(little)]
pub struct AniHeader {
#[br(temp)]
anih_size: u32,
#[br(assert(anih_size == header_size && header_size == 36), temp)]
header_size: u32,
pub num_frames: u32,
#[br(pad_after = 16)]
pub num_steps: u32,
pub jiffy_rate: u32,
flags: AniFlags,
}
#[derive(Default)]
pub struct AniFile {
pub header: AniHeader,
pub title: Option<NullString>,
pub author: Option<NullString>,
pub rate: Option<RiffChunkU32>,
pub sequence: Option<RiffChunkU32>,
pub ico_frames: Vec<RiffChunkU8>,
}
impl fmt::Debug for AniFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AniFile")
.field("header", &self.header)
.field("title", &self.title)
.field("author", &self.author)
.field("rate", &self.rate)
.field("sequence", &self.sequence)
.finish_non_exhaustive()
}
}
impl AniFile {
const MAX_CHUNK_SIZE: usize = 2_097_152;
pub fn from_blob(ani_blob: &[u8]) -> Result<Self> {
if ani_blob.len() > Self::MAX_CHUNK_SIZE {
bail!(
"ani_blob.len()={} unreasonably large (2MB+)",
ani_blob.len()
)
}
let ani_blob_len_u64 = u64::try_from(ani_blob.len())?;
let mut ani = Self::default();
let mut cursor = Cursor::new(ani_blob);
let mut buf = [0u8; 4];
cursor.read_exact(&mut buf)?;
if buf != *b"RIFF" {
bail!("expected 'RIFF' chunk, instead got {buf:?}");
}
cursor.read_exact(&mut buf)?;
let riff_size = u32::from_le_bytes(buf);
if u64::from(riff_size) > ani_blob_len_u64 {
bail!("riff_size={riff_size} extends beyond blob")
}
cursor.read_exact(&mut buf)?;
if buf != *b"ACON" {
bail!("expected 'ACON' as 'RIFF' subtype, instead got {buf:?}");
}
while cursor.position() < ani_blob.len().try_into()? {
cursor.read_exact(&mut buf)?;
match &buf {
b"LIST" => Self::parse_list(&mut cursor, &mut ani)?,
b"anih" => {
if ani.header != AniHeader::default() {
bail!("duplicate 'anih' chunk");
}
ani.header =
AniHeader::read_le(&mut cursor).context("failed to read 'anih' chunk")?;
}
b"rate" => {
if ani.rate.is_some() {
bail!("duplicate 'rate' chunk");
}
ani.rate = Some(
RiffChunkU32::read_le(&mut cursor)
.context("failed to read 'rate' chunk")?,
);
}
b"seq " => {
if ani.sequence.is_some() {
bail!("duplicate 'seq ' chunk");
}
ani.sequence = Some(
RiffChunkU32::read_le(&mut cursor)
.context("failed to read 'seq ' chunk")?,
);
}
_ => bail!("unexpected fourcc(?) buf={buf:?}"),
}
}
Self::check_invariants(&ani)?;
Ok(ani)
}
fn parse_list(cursor: &mut Cursor<&[u8]>, ani: &mut Self) -> Result<()> {
let ani_blob_size = cursor.get_ref().len();
let mut buf = [0u8; 4];
let mut list_id = [0u8; 4];
cursor.read_exact(&mut buf)?; cursor.read_exact(&mut list_id)?;
let list_size = u32::from_le_bytes(buf);
let list_data_size = list_size
.checked_sub(4)
.with_context(|| format!("underflow on list_size={list_size} - 4"))?;
if usize::try_from(list_data_size)? > Self::MAX_CHUNK_SIZE {
bail!("list_data_size={list_data_size} unreasonably large (2MB+)");
}
let end = cursor
.position()
.checked_add(u64::from(list_data_size))
.with_context(|| {
format!(
"overflow on cursor.position={} + list_data_size={list_data_size}",
cursor.position()
)
})?;
if end > ani_blob_size.try_into()? {
bail!("list_data_size={list_data_size} extends beyond blob");
}
match &list_id {
b"INFO" => {
while cursor.position() < end {
cursor.read_exact(&mut buf)?;
if buf == *b"INAM" {
if ani.title.is_some() {
bail!("duplicate 'INAM' subchunk in 'INFO'");
}
cursor.read_exact(&mut buf)?; ani.title = Some(NullString::read_le(cursor)?);
if !u32::from_le_bytes(buf).is_multiple_of(2) {
cursor.seek_relative(1)?;
}
} else if buf == *b"IART" {
if ani.author.is_some() {
bail!("duplicate 'IART' subchunk in 'INFO'");
}
cursor.read_exact(&mut buf)?; ani.author = Some(NullString::read_le(cursor)?);
if !u32::from_le_bytes(buf).is_multiple_of(2) {
cursor.seek_relative(1)?;
}
} else {
bail!("expected 'INAM' or 'IART' subchunk in 'INFO', instead got {buf:?}");
}
}
}
b"fram" => {
if !ani.ico_frames.is_empty() {
bail!("duplicate 'fram' chunk");
}
let mut chunks = Vec::with_capacity(usize::try_from(ani.header.num_frames)?);
while cursor.position() < end {
cursor.read_exact(&mut buf)?;
if buf != *b"icon" {
bail!("expected 'icon' subchunk, instead got {buf:?}");
}
let chunk = RiffChunkU8::read_le(cursor)
.context("failed to read 'icon' subchunk of 'fram'")?;
chunks.push(chunk);
}
if chunks.is_empty() {
bail!("failed to parse any frames from 'fram' chunk");
}
ani.ico_frames = chunks;
}
_ => bail!("unexpected list_id={list_id:?}"),
}
if list_data_size % 2 != 0 {
cursor.seek_relative(1)?;
}
Ok(())
}
fn check_invariants(ani: &Self) -> Result<()> {
use AniFlags::*;
let hdr = &ani.header;
let num_frames = usize::try_from(hdr.num_frames)?;
let num_steps = usize::try_from(hdr.num_steps)?;
if num_frames != ani.ico_frames.len() {
bail!(
"expected num_frames={num_frames}, instead got ico_frames.len()={}",
ani.ico_frames.len()
);
}
if let Some(rate) = &ani.rate
&& rate.data.len() != num_steps
{
bail!(
"expected num_steps={num_steps}, instead got rate.len()={}",
rate.data.len(),
)
}
if hdr.jiffy_rate == 0 && ani.rate.is_none() && ani.ico_frames.len() > 1 {
bail!("no frame timings (>1 frames): jiffy_rate=0, ani.rate=None");
}
if let Some(seq) = &ani.sequence
&& seq.data.iter().max() >= Some(&hdr.num_frames)
{
bail!("frame indices of 'seq ' chunk go out of bounds");
}
if hdr.flags == Sequenced && ani.sequence.is_none() {
eprintln!(
"[warning] expected 'seq ' chunk from flags={:?}, found None. \
the order in which frames were stored will be used instead",
hdr.flags
);
}
if let Some(seq) = &ani.sequence
&& hdr.flags == Unsequenced
&& seq.data != (0..hdr.num_steps).collect::<Vec<_>>()
{
eprintln!(
"[warning] expected 'seq ' chunk to be None from flags={:?}, found \
non-linear sequence={:?}. note that this sequence will still be used",
hdr.flags, ani.sequence
);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::fmt::Write;
use super::*;
use crate::from_root;
#[test]
fn good_ani() {
const ANI_FRAMES: &str = include_str!(from_root!("/testing/fixtures/neuro_alt_frames"));
const ANI_BLOB: &[u8] = include_bytes!(from_root!("/testing/fixtures/neuro/Neuro alt.ani"));
const {
assert!(
size_of::<AniFile>() == 136,
"AniFile fields have changed, update tests and this number accordingly"
);
}
let ani = AniFile::from_blob(ANI_BLOB).unwrap();
let hdr = &ani.header;
assert_eq!(hdr.num_frames, 10);
assert_eq!(hdr.num_steps, 21);
assert_eq!(hdr.jiffy_rate, 6);
assert_eq!(hdr.flags, AniFlags::Sequenced);
assert!(ani.rate.is_none());
assert_eq!(
ani.sequence.as_ref().unwrap().data,
&[
0, 1, 2, 2, 3, 3, 3, 3, 4, 5, 6, 7, 3, 3, 3, 2, 2, 2, 3, 8, 9
]
);
assert_eq!(
usize::try_from(hdr.num_frames).unwrap(),
ani.ico_frames.len()
);
assert_eq!(
usize::try_from(hdr.num_steps).unwrap(),
ani.sequence.as_ref().unwrap().data.len()
);
let mut ani_frames = String::new();
for frame in ani.ico_frames {
writeln!(&mut ani_frames, "{:?}", frame.data).unwrap();
}
assert_eq!(ani_frames, ANI_FRAMES);
}
}