use std::{
fmt,
io::{Cursor, Read, Seek, SeekFrom},
range::Range,
};
use anyhow::{Result, anyhow, bail};
use binrw::{BinRead, binread};
use crate::warn;
#[binread]
#[derive(Debug)]
#[br(little)]
#[br(stream = s)]
pub struct RiffChunk {
id: [u8; 4],
size: u32,
#[br(temp, try_calc = s.stream_position())]
start: u64,
#[br(temp, try_calc = start.checked_add(u64::from(size)).ok_or_else(|| {
anyhow!("overflow when calculating RiffChunk end for id={id:?}")
}))]
end: u64,
#[br(calc = (start..end).into())]
data: Range<u64>,
#[br(try_calc = end.checked_add(u64::from(size) & 1).ok_or_else(|| {
anyhow!("overflow when calculating RiffChunk next for id={id:?}")
}))]
next: u64,
}
#[derive(Debug, PartialEq, BinRead)]
#[br(little)]
#[br(repr = u32)]
enum AniFlags {
Unsequenced = 1,
Sequenced = 3,
}
#[binread]
#[derive(Debug, PartialEq)]
#[br(little)]
pub struct AniHeader {
#[br(assert(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,
}
pub struct AniFile {
pub header: AniHeader,
pub title: Option<String>,
pub author: Option<String>,
pub rate: Option<Vec<u32>>,
pub sequence: Option<Vec<u32>>,
pub ico_frames: Vec<Vec<u8>>,
}
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()
}
}
#[derive(Default)]
struct AniParserState {
header: Option<Range<u64>>,
title: Option<Range<u64>>,
author: Option<Range<u64>>,
rate: Option<Range<u64>>,
sequence: Option<Range<u64>>,
ico_frames: Option<Range<u64>>,
}
fn slice_blob(blob: &[u8], range: Range<u64>) -> Result<&[u8]> {
let start = usize::try_from(range.start)?;
let end = usize::try_from(range.end)?;
blob.get(start..end)
.ok_or_else(|| anyhow!("range {start}..{end} outside of blob (len={})", blob.len()))
}
fn process_ranges(blob: &[u8], state: &AniParserState) -> Result<AniFile> {
let bytes_to_string = |r: Range<u64>| {
let string = slice_blob(blob, r)?;
let string = &string[..(string.iter().position(|&b| b == 0).unwrap_or_else(|| {
warn!("'INFO' string is not null-terminated");
string.len()
}))];
anyhow::Ok(String::from_utf8_lossy(string).to_string())
};
let to_u32_vec = |r: Range<u64>| {
let bytes = slice_blob(blob, r)?;
let (bytes, rem) = bytes.as_chunks::<4>();
if !rem.is_empty() {
bail!("u32 data not divisible by 4")
}
anyhow::Ok(
bytes
.iter()
.map(|&b| u32::from_le_bytes(b))
.collect::<Vec<_>>(),
)
};
let Some(header) = state.header else {
bail!("'anih' chunk is required but is missing")
};
let Some(ico_frames) = state.ico_frames else {
bail!("'fram' chunk is required but is missing")
};
let header = AniHeader::read(&mut Cursor::new(slice_blob(blob, header)?))?;
let fram = slice_blob(blob, ico_frames)?;
let mut cursor = Cursor::new(fram);
let mut ico_frames = Vec::new();
while cursor.position() < u64::try_from(fram.len())? {
let icon = RiffChunk::read(&mut cursor)?;
if icon.id != *b"icon" {
bail!("expected 'icon' subchunks, instead got {:?}", icon.id);
}
let bytes = slice_blob(fram, icon.data)?;
ico_frames.push(bytes.to_vec());
cursor.seek(SeekFrom::Start(icon.next))?;
}
let title = state.title.map(bytes_to_string).transpose()?;
let author = state.author.map(bytes_to_string).transpose()?;
let rate = state.rate.map(to_u32_vec).transpose()?;
let sequence = state.sequence.map(to_u32_vec).transpose()?;
Ok(AniFile {
header,
title,
author,
rate,
sequence,
ico_frames,
})
}
impl AniFile {
pub fn from_blob(ani_blob: &[u8]) -> Result<Self> {
let ani_blob_len_u64 = u64::try_from(ani_blob.len())?;
let mut state = AniParserState::default();
let mut cursor = Cursor::new(ani_blob);
let riff = RiffChunk::read(&mut cursor)?;
if riff.id != *b"RIFF" {
bail!("expected 'RIFF' chunk, instead got {:?}", riff.id);
}
if u64::from(riff.size) > ani_blob_len_u64 {
bail!("riff_size={} extends beyond blob", riff.size)
}
let mut list_type = [0_u8; 4];
cursor.read_exact(&mut list_type)?;
if list_type != *b"ACON" {
bail!("expected 'ACON' as 'RIFF' subtype, instead got {list_type:?}");
}
while cursor.position() < ani_blob.len().try_into()? {
let chunk = RiffChunk::read(&mut cursor)?;
match &chunk.id {
b"LIST" => Self::parse_list(&mut cursor, &mut state, &chunk)?,
b"anih" => {
if state.header.is_some() {
bail!("read duplicate 'anih' chunk at {}", cursor.position());
}
if chunk.size != 36 {
bail!(
"expected 'anih' chunk size to be 36, instead got {}",
chunk.size
)
}
state.header = Some(chunk.data);
}
b"rate" => {
if state.rate.is_some() {
bail!("read duplicate 'rate' chunk at {}", cursor.position());
}
state.rate = Some(chunk.data);
}
b"seq " => {
if state.sequence.is_some() {
bail!("read duplicate 'seq ' chunk at {}", cursor.position());
}
state.sequence = Some(chunk.data);
}
_ => (),
}
cursor.seek(SeekFrom::Start(chunk.next))?;
}
let ani = process_ranges(ani_blob, &state)?;
Self::check_invariants(&ani)?;
Ok(ani)
}
fn parse_list(
cursor: &mut Cursor<&[u8]>,
state: &mut AniParserState,
list_chunk: &RiffChunk,
) -> Result<()> {
if list_chunk.size < 4 {
bail!(
"expected 'LIST' chunk to have size four or greater, instead got {}",
list_chunk.size
);
}
let end = cursor
.position()
.checked_add(u64::from(list_chunk.size))
.ok_or_else(|| anyhow!("overflow when calculating end of list chunk"))?;
let mut list_type = [0_u8; 4];
cursor.read_exact(&mut list_type)?;
match &list_type {
b"INFO" => {
while cursor.position() < end {
let subchunk = RiffChunk::read(cursor)?;
if subchunk.id == *b"INAM" {
state.title = Some(subchunk.data);
} else if subchunk.id == *b"IART" {
state.author = Some(subchunk.data);
}
cursor.seek(SeekFrom::Start(subchunk.next))?;
}
}
b"fram" => {
if state.ico_frames.is_some() {
bail!("read duplicate 'fram' chunk at {}", cursor.position());
}
state.ico_frames = Some((cursor.position()..end).into());
}
_ => (),
}
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 == 0 || num_steps == 0 {
bail!("ANI must contain at least one frame and one animation step");
}
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.len() != num_steps
{
bail!(
"expected num_steps={num_steps}, instead got rate.len()={}",
rate.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.iter().max() >= Some(&hdr.num_frames)
{
bail!("frame indices of 'seq ' chunk go out of bounds");
}
let observed_steps = ani.sequence.as_ref().map_or(num_frames, Vec::len);
if observed_steps != num_steps {
bail!(
"num_steps={num_steps} does not match length of \
sequence table (observed_steps={observed_steps})"
);
}
if hdr.flags == Sequenced && ani.sequence.is_none() {
warn!(
"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.iter().copied().eq(0..hdr.num_steps)
{
warn!(
"expected 'seq ' chunk to be None from flags={:?}, found the 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"));
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(),
&[
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().len()
);
let mut ani_frames = String::new();
for frame in ani.ico_frames {
writeln!(&mut ani_frames, "{frame:?}").unwrap();
}
assert_eq!(ani_frames, ANI_FRAMES);
}
}