use std::fmt;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum CafError {
#[error("buffer too short: need {needed} bytes, got {got}")]
BufferTooShort { needed: usize, got: usize },
#[error("invalid CAF magic: expected b\"caff\", got {0:?}")]
InvalidMagic([u8; 4]),
#[error("unsupported CAF version: {0}")]
UnsupportedVersion(u16),
#[error("chunk extends past end of buffer")]
ChunkOverflow,
#[error("required chunk '{0}' not found")]
MissingChunk(String),
#[error("invalid 'desc' chunk size: expected 32, got {0}")]
InvalidDescSize(i64),
#[error("integer overflow in chunk size")]
Overflow,
}
pub const CAF_MAGIC: &[u8; 4] = b"caff";
pub const CAF_VERSION: u16 = 1;
pub const CAF_FILE_HEADER_SIZE: usize = 8;
pub const CAF_CHUNK_HEADER_SIZE: usize = 12;
pub const CAF_ASBD_SIZE: usize = 32;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum CafChunkType {
AudioDescription,
AudioData,
PacketTable,
ChannelLayout,
Information,
MagicCookie,
Marker,
Region,
UniqueId,
Unknown([u8; 4]),
}
impl CafChunkType {
#[must_use]
pub fn from_bytes(b: [u8; 4]) -> Self {
match &b {
b"desc" => Self::AudioDescription,
b"data" => Self::AudioData,
b"pakt" => Self::PacketTable,
b"chan" => Self::ChannelLayout,
b"info" => Self::Information,
b"kuki" => Self::MagicCookie,
b"mark" => Self::Marker,
b"regn" => Self::Region,
b"umid" => Self::UniqueId,
_ => Self::Unknown(b),
}
}
#[must_use]
pub fn to_bytes(self) -> [u8; 4] {
match self {
Self::AudioDescription => *b"desc",
Self::AudioData => *b"data",
Self::PacketTable => *b"pakt",
Self::ChannelLayout => *b"chan",
Self::Information => *b"info",
Self::MagicCookie => *b"kuki",
Self::Marker => *b"mark",
Self::Region => *b"regn",
Self::UniqueId => *b"umid",
Self::Unknown(b) => b,
}
}
}
impl fmt::Display for CafChunkType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let b = self.to_bytes();
let s = std::str::from_utf8(&b).unwrap_or("????");
write!(f, "'{s}'")
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CafFileHeader {
pub version: u16,
pub flags: u16,
}
impl CafFileHeader {
#[must_use]
pub const fn new() -> Self {
Self {
version: CAF_VERSION,
flags: 0,
}
}
}
impl Default for CafFileHeader {
fn default() -> Self {
Self::new()
}
}
pub fn parse_caf_file_header(buf: &[u8]) -> Result<CafFileHeader, CafError> {
if buf.len() < CAF_FILE_HEADER_SIZE {
return Err(CafError::BufferTooShort {
needed: CAF_FILE_HEADER_SIZE,
got: buf.len(),
});
}
let magic: [u8; 4] = buf[0..4].try_into().map_err(|_| CafError::BufferTooShort {
needed: 4,
got: buf.len(),
})?;
if &magic != CAF_MAGIC {
return Err(CafError::InvalidMagic(magic));
}
let version =
u16::from_be_bytes(buf[4..6].try_into().map_err(|_| CafError::BufferTooShort {
needed: 6,
got: buf.len(),
})?);
if version != CAF_VERSION {
return Err(CafError::UnsupportedVersion(version));
}
let flags = u16::from_be_bytes(buf[6..8].try_into().map_err(|_| CafError::BufferTooShort {
needed: 8,
got: buf.len(),
})?);
Ok(CafFileHeader { version, flags })
}
#[must_use]
pub fn serialize_caf_file_header(hdr: &CafFileHeader) -> [u8; 8] {
let mut out = [0u8; 8];
out[0..4].copy_from_slice(CAF_MAGIC);
out[4..6].copy_from_slice(&hdr.version.to_be_bytes());
out[6..8].copy_from_slice(&hdr.flags.to_be_bytes());
out
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CafChunkHeader {
pub chunk_type: CafChunkType,
pub chunk_size: i64,
}
impl CafChunkHeader {
#[must_use]
pub const fn new(chunk_type: CafChunkType, chunk_size: i64) -> Self {
Self {
chunk_type,
chunk_size,
}
}
}
pub fn parse_caf_chunk_header(buf: &[u8]) -> Result<CafChunkHeader, CafError> {
if buf.len() < CAF_CHUNK_HEADER_SIZE {
return Err(CafError::BufferTooShort {
needed: CAF_CHUNK_HEADER_SIZE,
got: buf.len(),
});
}
let type_bytes: [u8; 4] = buf[0..4].try_into().map_err(|_| CafError::BufferTooShort {
needed: 4,
got: buf.len(),
})?;
let chunk_type = CafChunkType::from_bytes(type_bytes);
let chunk_size =
i64::from_be_bytes(
buf[4..12]
.try_into()
.map_err(|_| CafError::BufferTooShort {
needed: 12,
got: buf.len(),
})?,
);
Ok(CafChunkHeader {
chunk_type,
chunk_size,
})
}
#[must_use]
pub fn serialize_caf_chunk_header(hdr: &CafChunkHeader) -> [u8; 12] {
let mut out = [0u8; 12];
out[0..4].copy_from_slice(&hdr.chunk_type.to_bytes());
out[4..12].copy_from_slice(&hdr.chunk_size.to_be_bytes());
out
}
#[derive(Clone, Debug, PartialEq)]
pub struct CafAudioDescription {
pub sample_rate: f64,
pub format_id: [u8; 4],
pub format_flags: u32,
pub bytes_per_packet: u32,
pub frames_per_packet: u32,
pub channels_per_frame: u32,
pub bits_per_channel: u32,
}
impl CafAudioDescription {
#[must_use]
pub fn pcm_f32_mono(sample_rate: f64) -> Self {
Self {
sample_rate,
format_id: *b"lpcm",
format_flags: 0x0C, bytes_per_packet: 4,
frames_per_packet: 1,
channels_per_frame: 1,
bits_per_channel: 32,
}
}
#[must_use]
pub fn pcm_i16_stereo(sample_rate: f64) -> Self {
Self {
sample_rate,
format_id: *b"lpcm",
format_flags: 0x0E, bytes_per_packet: 4,
frames_per_packet: 1,
channels_per_frame: 2,
bits_per_channel: 16,
}
}
}
pub fn parse_audio_description(buf: &[u8]) -> Result<CafAudioDescription, CafError> {
if buf.len() < CAF_ASBD_SIZE {
return Err(CafError::BufferTooShort {
needed: CAF_ASBD_SIZE,
got: buf.len(),
});
}
let rate_bytes: [u8; 8] = buf[0..8].try_into().map_err(|_| CafError::BufferTooShort {
needed: 8,
got: buf.len(),
})?;
let sample_rate = f64::from_be_bytes(rate_bytes);
let format_id: [u8; 4] = buf[8..12]
.try_into()
.map_err(|_| CafError::BufferTooShort {
needed: 12,
got: buf.len(),
})?;
let format_flags =
u32::from_be_bytes(
buf[12..16]
.try_into()
.map_err(|_| CafError::BufferTooShort {
needed: 16,
got: buf.len(),
})?,
);
let bytes_per_packet =
u32::from_be_bytes(
buf[16..20]
.try_into()
.map_err(|_| CafError::BufferTooShort {
needed: 20,
got: buf.len(),
})?,
);
let frames_per_packet =
u32::from_be_bytes(
buf[20..24]
.try_into()
.map_err(|_| CafError::BufferTooShort {
needed: 24,
got: buf.len(),
})?,
);
let channels_per_frame =
u32::from_be_bytes(
buf[24..28]
.try_into()
.map_err(|_| CafError::BufferTooShort {
needed: 28,
got: buf.len(),
})?,
);
let bits_per_channel =
u32::from_be_bytes(
buf[28..32]
.try_into()
.map_err(|_| CafError::BufferTooShort {
needed: 32,
got: buf.len(),
})?,
);
Ok(CafAudioDescription {
sample_rate,
format_id,
format_flags,
bytes_per_packet,
frames_per_packet,
channels_per_frame,
bits_per_channel,
})
}
#[must_use]
pub fn serialize_audio_description(desc: &CafAudioDescription) -> [u8; CAF_ASBD_SIZE] {
let mut out = [0u8; CAF_ASBD_SIZE];
out[0..8].copy_from_slice(&desc.sample_rate.to_be_bytes());
out[8..12].copy_from_slice(&desc.format_id);
out[12..16].copy_from_slice(&desc.format_flags.to_be_bytes());
out[16..20].copy_from_slice(&desc.bytes_per_packet.to_be_bytes());
out[20..24].copy_from_slice(&desc.frames_per_packet.to_be_bytes());
out[24..28].copy_from_slice(&desc.channels_per_frame.to_be_bytes());
out[28..32].copy_from_slice(&desc.bits_per_channel.to_be_bytes());
out
}
#[derive(Clone, Debug)]
pub struct CafChunk {
pub header: CafChunkHeader,
pub body: Vec<u8>,
}
pub struct CafReader<'a> {
buf: &'a [u8],
pos: usize,
file_header_parsed: bool,
}
impl<'a> CafReader<'a> {
#[must_use]
pub fn new(buf: &'a [u8]) -> Self {
Self {
buf,
pos: 0,
file_header_parsed: false,
}
}
pub fn read_file_header(&mut self) -> Result<CafFileHeader, CafError> {
let hdr = parse_caf_file_header(&self.buf[self.pos..])?;
self.pos += CAF_FILE_HEADER_SIZE;
self.file_header_parsed = true;
Ok(hdr)
}
pub fn next_chunk(&mut self) -> Result<Option<CafChunk>, CafError> {
if self.pos >= self.buf.len() {
return Ok(None);
}
let chunk_hdr_buf = &self.buf[self.pos..];
if chunk_hdr_buf.len() < CAF_CHUNK_HEADER_SIZE {
return Ok(None);
}
let header = parse_caf_chunk_header(chunk_hdr_buf)?;
self.pos += CAF_CHUNK_HEADER_SIZE;
let body_size: usize = if header.chunk_size < 0 {
self.buf.len().saturating_sub(self.pos)
} else {
header
.chunk_size
.try_into()
.map_err(|_| CafError::Overflow)?
};
if self.pos + body_size > self.buf.len() {
return Err(CafError::ChunkOverflow);
}
let body = self.buf[self.pos..self.pos + body_size].to_vec();
self.pos += body_size;
Ok(Some(CafChunk { header, body }))
}
#[must_use]
pub fn position(&self) -> usize {
self.pos
}
pub fn collect_chunks(&mut self) -> Result<Vec<CafChunk>, CafError> {
let mut out = Vec::new();
while let Some(chunk) = self.next_chunk()? {
out.push(chunk);
}
Ok(out)
}
}
pub struct CafWriter {
desc: CafAudioDescription,
magic_cookie: Option<Vec<u8>>,
audio_data: Vec<u8>,
extra_chunks: Vec<CafChunk>,
}
impl CafWriter {
#[must_use]
pub fn new(desc: CafAudioDescription) -> Self {
Self {
desc,
magic_cookie: None,
audio_data: Vec::new(),
extra_chunks: Vec::new(),
}
}
pub fn set_magic_cookie(&mut self, cookie: Vec<u8>) {
self.magic_cookie = Some(cookie);
}
pub fn append_audio_data(&mut self, data: &[u8]) {
self.audio_data.extend_from_slice(data);
}
pub fn add_chunk(&mut self, chunk: CafChunk) {
self.extra_chunks.push(chunk);
}
#[must_use]
pub fn finish(self) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&serialize_caf_file_header(&CafFileHeader::new()));
let desc_body = serialize_audio_description(&self.desc);
let desc_hdr = CafChunkHeader::new(CafChunkType::AudioDescription, CAF_ASBD_SIZE as i64);
out.extend_from_slice(&serialize_caf_chunk_header(&desc_hdr));
out.extend_from_slice(&desc_body);
if let Some(cookie) = &self.magic_cookie {
let kuki_hdr = CafChunkHeader::new(CafChunkType::MagicCookie, cookie.len() as i64);
out.extend_from_slice(&serialize_caf_chunk_header(&kuki_hdr));
out.extend_from_slice(cookie);
}
for chunk in &self.extra_chunks {
out.extend_from_slice(&serialize_caf_chunk_header(&chunk.header));
out.extend_from_slice(&chunk.body);
}
let data_chunk_hdr = CafChunkHeader::new(CafChunkType::AudioData, -1i64);
out.extend_from_slice(&serialize_caf_chunk_header(&data_chunk_hdr));
out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&self.audio_data);
out
}
}
pub fn extract_audio_description(chunks: &[CafChunk]) -> Result<CafAudioDescription, CafError> {
let desc_chunk = chunks
.iter()
.find(|c| c.header.chunk_type == CafChunkType::AudioDescription)
.ok_or_else(|| CafError::MissingChunk("desc".into()))?;
if desc_chunk.header.chunk_size != CAF_ASBD_SIZE as i64 {
return Err(CafError::InvalidDescSize(desc_chunk.header.chunk_size));
}
parse_audio_description(&desc_chunk.body)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_header_round_trip() {
let hdr = CafFileHeader::new();
let bytes = serialize_caf_file_header(&hdr);
let parsed = parse_caf_file_header(&bytes).expect("parse failed");
assert_eq!(parsed.version, 1);
assert_eq!(parsed.flags, 0);
}
#[test]
fn test_file_header_invalid_magic() {
let mut bytes = serialize_caf_file_header(&CafFileHeader::new());
bytes[0] = b'X';
assert!(matches!(
parse_caf_file_header(&bytes),
Err(CafError::InvalidMagic(_))
));
}
#[test]
fn test_file_header_unsupported_version() {
let mut bytes = serialize_caf_file_header(&CafFileHeader::new());
bytes[4] = 0;
bytes[5] = 2; assert!(matches!(
parse_caf_file_header(&bytes),
Err(CafError::UnsupportedVersion(2))
));
}
#[test]
fn test_chunk_header_round_trip() {
let hdr = CafChunkHeader::new(CafChunkType::AudioDescription, 32);
let bytes = serialize_caf_chunk_header(&hdr);
let parsed = parse_caf_chunk_header(&bytes).expect("parse failed");
assert_eq!(parsed.chunk_type, CafChunkType::AudioDescription);
assert_eq!(parsed.chunk_size, 32);
}
#[test]
fn test_chunk_type_unknown() {
let hdr = CafChunkHeader::new(CafChunkType::Unknown(*b"xyzw"), 0);
let bytes = serialize_caf_chunk_header(&hdr);
let parsed = parse_caf_chunk_header(&bytes).expect("parse failed");
assert_eq!(parsed.chunk_type, CafChunkType::Unknown(*b"xyzw"));
}
#[test]
fn test_audio_description_round_trip() {
let desc = CafAudioDescription::pcm_f32_mono(44_100.0);
let bytes = serialize_audio_description(&desc);
let parsed = parse_audio_description(&bytes).expect("parse failed");
assert!((parsed.sample_rate - 44_100.0).abs() < f64::EPSILON);
assert_eq!(parsed.format_id, *b"lpcm");
assert_eq!(parsed.channels_per_frame, 1);
assert_eq!(parsed.bits_per_channel, 32);
}
#[test]
fn test_audio_description_stereo() {
let desc = CafAudioDescription::pcm_i16_stereo(48_000.0);
let bytes = serialize_audio_description(&desc);
let parsed = parse_audio_description(&bytes).expect("parse failed");
assert_eq!(parsed.channels_per_frame, 2);
assert_eq!(parsed.bits_per_channel, 16);
assert_eq!(parsed.bytes_per_packet, 4);
}
#[test]
fn test_writer_produces_valid_file() {
let desc = CafAudioDescription::pcm_f32_mono(48_000.0);
let mut writer = CafWriter::new(desc);
writer.append_audio_data(&[0u8; 32]);
let bytes = writer.finish();
assert_eq!(&bytes[..4], b"caff");
assert!(bytes.len() >= 8 + 12 + 32 + 12 + 4 + 32);
}
#[test]
fn test_reader_round_trip() {
let desc = CafAudioDescription::pcm_f32_mono(22_050.0);
let mut writer = CafWriter::new(desc.clone());
writer.append_audio_data(&[0xABu8; 16]);
let bytes = writer.finish();
let mut reader = CafReader::new(&bytes);
let fhdr = reader.read_file_header().expect("file header");
assert_eq!(fhdr.version, 1);
let chunks = reader.collect_chunks().expect("collect chunks");
assert!(!chunks.is_empty());
let parsed_desc = extract_audio_description(&chunks).expect("desc chunk");
assert!((parsed_desc.sample_rate - 22_050.0).abs() < f64::EPSILON);
assert_eq!(parsed_desc.format_id, *b"lpcm");
}
#[test]
fn test_writer_with_magic_cookie() {
let desc = CafAudioDescription::pcm_f32_mono(48_000.0);
let mut writer = CafWriter::new(desc);
writer.set_magic_cookie(vec![0xDE, 0xAD, 0xBE, 0xEF]);
writer.append_audio_data(&[0u8; 8]);
let bytes = writer.finish();
let mut reader = CafReader::new(&bytes);
reader.read_file_header().expect("file header");
let chunks = reader.collect_chunks().expect("collect chunks");
let kuki = chunks
.iter()
.find(|c| c.header.chunk_type == CafChunkType::MagicCookie);
assert!(kuki.is_some());
assert_eq!(kuki.expect("kuki").body, vec![0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn test_chunk_type_display() {
assert_eq!(CafChunkType::AudioDescription.to_string(), "'desc'");
assert_eq!(CafChunkType::AudioData.to_string(), "'data'");
}
#[test]
fn test_missing_desc_chunk_error() {
let chunks: Vec<CafChunk> = vec![];
assert!(matches!(
extract_audio_description(&chunks),
Err(CafError::MissingChunk(_))
));
}
#[test]
fn test_buffer_too_short_file_header() {
assert!(matches!(
parse_caf_file_header(&[0u8; 4]),
Err(CafError::BufferTooShort { .. })
));
}
#[test]
fn test_data_chunk_edit_count() {
let desc = CafAudioDescription::pcm_f32_mono(48_000.0);
let mut writer = CafWriter::new(desc);
writer.append_audio_data(&[1u8, 2, 3, 4]);
let bytes = writer.finish();
let mut reader = CafReader::new(&bytes);
reader.read_file_header().expect("file header");
let chunks = reader.collect_chunks().expect("chunks");
let data_chunk = chunks
.iter()
.find(|c| c.header.chunk_type == CafChunkType::AudioData)
.expect("data chunk");
assert_eq!(&data_chunk.body[..4], &[0, 0, 0, 0]);
assert_eq!(&data_chunk.body[4..], &[1u8, 2, 3, 4]);
}
}