fsbex 0.3.0

Library for extracting audio from FMOD sound banks
Documentation
use crate::read::ReadError;
use core::ffi::FromBytesUntilNulError;
use std::{
    error::Error,
    fmt::{Display, Formatter, Result as FmtResult},
    str::Utf8Error,
};

#[derive(Debug)]
pub(crate) struct HeaderError {
    kind: HeaderErrorKind,
    source: Option<HeaderErrorSource>,
}

#[derive(Debug)]
#[cfg_attr(test, derive(Clone, Copy, PartialEq, Eq))]
pub(crate) enum HeaderErrorKind {
    Magic,
    Version,
    UnknownVersion { version: u32 },
    StreamCount,
    ZeroStreams,
    StreamHeadersSize,
    NameTableSize,
    TotalStreamSize,
    ZeroTotalStreamSize,
    AudioFormat,
    UnknownAudioFormat { flag: u32 },
    EncodingFlags,
    Metadata,
    StreamHeader,
    ZeroStreamSize { index: u32 },
    WrongHeaderSize { expected: usize, actual: usize },
    NameTable,
}

#[derive(Debug)]
pub(crate) enum HeaderErrorSource {
    Read(ReadError),
    Stream(StreamError),
    NameTable(NameError),
}

impl HeaderError {
    pub(crate) fn new(kind: HeaderErrorKind) -> Self {
        Self { kind, source: None }
    }

    pub(crate) fn new_with_source(kind: HeaderErrorKind, source: ReadError) -> Self {
        Self {
            kind,
            source: Some(HeaderErrorSource::Read(source)),
        }
    }

    pub(crate) fn factory(kind: HeaderErrorKind) -> impl FnOnce(ReadError) -> Self {
        move |source| Self::new_with_source(kind, source)
    }
}

#[cfg(test)]
impl HeaderError {
    pub(crate) fn kind(&self) -> HeaderErrorKind {
        self.kind
    }

    pub(crate) fn is_stream_err_kind(&self, kind: StreamErrorKind) -> bool {
        match &self.source {
            Some(HeaderErrorSource::Stream(e)) => e.kind == kind,
            _ => false,
        }
    }

    pub(crate) fn is_chunk_err_kind(&self, kind: ChunkErrorKind) -> bool {
        match &self.source {
            Some(HeaderErrorSource::Stream(e)) => match &e.source {
                Some(StreamErrorSource::Chunk(e)) => e.kind == kind,
                _ => false,
            },
            _ => false,
        }
    }
}

impl Display for HeaderError {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        #[allow(clippy::enum_glob_use)]
        use HeaderErrorKind::*;

        match self.kind {
            Magic => f.write_str("no file signature found"),
            Version => f.write_str("failed to read file format version"),
            UnknownVersion { version } => {
                f.write_fmt(format_args!("file format version was not recognized (0x{version:08x})"))
            }
            StreamCount => f.write_str("failed to read number of streams"),
            ZeroStreams => f.write_str("number of streams was 0"),
            StreamHeadersSize => f.write_str("failed to read size of stream headers"),
            NameTableSize => f.write_str("failed to read size of name table"),
            TotalStreamSize => f.write_str("failed to read total size of stream data"),
            ZeroTotalStreamSize => f.write_str("total size of stream data was 0 bytes"),
            AudioFormat => f.write_str("failed to read audio format flag"),
            UnknownAudioFormat { flag } => {
                f.write_fmt(format_args!("audio format flag was not recognized (0x{flag:08x})"))
            }
            EncodingFlags => f.write_str("failed to read encoding flags"),
            Metadata => f.write_str("failed to read (unused) metadata bytes"),
            StreamHeader => f.write_str("failed to parse stream header"),
            ZeroStreamSize { index } => f.write_fmt(format_args!("size of data of stream at index {index} was 0 bytes")),
            WrongHeaderSize { expected, actual } => {
                f.write_fmt(format_args!("total size of base header and stream headers ({actual} bytes) was different from expected ({expected} bytes)"))
            }
            NameTable => f.write_str("failed to read stream names")
        }
    }
}

impl Error for HeaderError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match &self.source {
            Some(source) => match source {
                HeaderErrorSource::Read(e) => Some(e),
                HeaderErrorSource::Stream(e) => Some(e),
                HeaderErrorSource::NameTable(e) => Some(e),
            },
            None => None,
        }
    }
}

#[derive(Debug)]
pub(crate) struct StreamError {
    index: u32,
    kind: StreamErrorKind,
    source: Option<StreamErrorSource>,
}

#[derive(Debug)]
#[cfg_attr(test, derive(Clone, Copy, PartialEq, Eq))]
pub(crate) enum StreamErrorKind {
    StreamInfo,
    UnknownSampleRate { flag: u8 },
    ZeroSamples,
    Chunk,
}

#[derive(Debug)]
enum StreamErrorSource {
    Read(ReadError),
    Chunk(ChunkError),
}

impl StreamError {
    pub(crate) fn new(index: u32, kind: StreamErrorKind) -> Self {
        Self {
            index,
            kind,
            source: None,
        }
    }

    pub(crate) fn new_with_source(index: u32, kind: StreamErrorKind, source: ReadError) -> Self {
        Self {
            index,
            kind,
            source: Some(StreamErrorSource::Read(source)),
        }
    }
}

#[cfg(test)]
impl StreamError {
    pub(crate) fn kind(&self) -> StreamErrorKind {
        self.kind
    }
}

impl From<StreamError> for HeaderError {
    fn from(value: StreamError) -> Self {
        Self {
            kind: HeaderErrorKind::StreamHeader,
            source: Some(HeaderErrorSource::Stream(value)),
        }
    }
}

impl Display for StreamError {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        #[allow(clippy::enum_glob_use)]
        use StreamErrorKind::*;

        match self.kind {
            StreamInfo => f.write_str("failed to read stream metadata"),
            UnknownSampleRate { flag } => {
                f.write_fmt(format_args!("sample rate flag was not recognized (0x{flag:02x})"))
            }
            ZeroSamples => f.write_str("number of samples was 0"),
            Chunk => f.write_str("failed to parse stream header chunk"),
        }?;

        f.write_fmt(format_args!(" - stream header at index {}", self.index))
    }
}

impl Error for StreamError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match &self.source {
            Some(source) => match source {
                StreamErrorSource::Read(e) => Some(e),
                StreamErrorSource::Chunk(e) => Some(e),
            },
            None => None,
        }
    }
}

#[derive(Debug)]
pub(crate) struct ChunkError {
    index: u32,
    kind: ChunkErrorKind,
    source: Option<ReadError>,
}

#[derive(Debug)]
#[cfg_attr(test, derive(Clone, Copy, PartialEq, Eq))]
pub(crate) enum ChunkErrorKind {
    Flag,
    UnknownType { flag: u8 },
    ChannelCount,
    ZeroChannels,
    SampleRate,
    ZeroSampleRate,
    LoopStart,
    LoopEnd,
    ZeroLengthLoop,
    DspCoefficients,
    VorbisCrc32,
    VorbisLayerCount,
    TooManyVorbisLayers { layers: u32 },
    ZeroVorbisLayers,
    WrongChunkSize { expected: u32, actual: usize },
}

impl ChunkError {
    pub(crate) fn new(index: u32, kind: ChunkErrorKind) -> Self {
        Self {
            index,
            kind,
            source: None,
        }
    }

    pub(crate) fn new_with_source(index: u32, kind: ChunkErrorKind, source: ReadError) -> Self {
        Self {
            index,
            kind,
            source: Some(source),
        }
    }

    pub(crate) fn factory(index: u32, kind: ChunkErrorKind) -> impl FnOnce(ReadError) -> Self {
        move |source| Self::new_with_source(index, kind, source)
    }

    pub(crate) fn into_stream_err(self, stream_index: u32) -> StreamError {
        StreamError {
            index: stream_index,
            kind: StreamErrorKind::Chunk,
            source: Some(StreamErrorSource::Chunk(self)),
        }
    }
}

impl Display for ChunkError {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        #[allow(clippy::enum_glob_use)]
        use ChunkErrorKind::*;

        match self.kind {
            Flag => f.write_str("failed to read chunk flag"),
            UnknownType { flag } => {
                f.write_fmt(format_args!("chunk type flag was not recognized (0x{flag:02x})"))
            }
            ChannelCount => f.write_str("failed to read number of channels"),
            ZeroChannels => f.write_str("number of channels was 0"),
            SampleRate => f.write_str("failed to read sample rate"),
            ZeroSampleRate => f.write_str("sample rate was 0"),
            LoopStart => f.write_str("failed to read starting position of loop in stream"),
            LoopEnd => f.write_str("failed to read ending position of loop in stream"),
            ZeroLengthLoop => f.write_str("length of loop in stream was 0"),
            DspCoefficients => f.write_str("failed to read DSP coefficients of stream"),
            VorbisCrc32 => f.write_str("failed to read CRC32 of Vorbis setup header"),
            VorbisLayerCount => {
                f.write_str("failed to read number of layers per channel in Vorbis stream")
            }
            TooManyVorbisLayers { layers } => f.write_fmt(format_args!(
                "number of layers in Vorbis stream was greater than 255 ({layers} layers)"
            )),
            ZeroVorbisLayers => f.write_str("number of layers in Vorbis stream was 0"),
            WrongChunkSize { expected, actual } => {
                f.write_fmt(format_args!("size of stream header chunk ({actual} bytes) was different from expected ({expected} bytes)"))
            }
        }?;

        f.write_fmt(format_args!(" - stream header chunk at index {}", self.index))
    }
}

impl Error for ChunkError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match &self.source {
            Some(source) => Some(source),
            None => None,
        }
    }
}

#[derive(Debug)]
pub(crate) struct NameError {
    index: u32,
    kind: NameErrorKind,
    source: NameErrorSource,
}

#[derive(Debug)]
pub(crate) enum NameErrorKind {
    NameOffset,
    Name,
    Utf8,
}

#[derive(Debug)]
enum NameErrorSource {
    Read(ReadError),
    CStr(FromBytesUntilNulError),
    Utf8(Utf8Error),
}

impl NameError {
    fn new(index: u32, kind: NameErrorKind, source: NameErrorSource) -> Self {
        Self {
            index,
            kind,
            source,
        }
    }

    pub(crate) fn read_factory(index: u32, kind: NameErrorKind) -> impl FnOnce(ReadError) -> Self {
        move |source| Self::new(index, kind, NameErrorSource::Read(source))
    }

    pub(crate) fn cstr_factory(index: u32) -> impl FnOnce(FromBytesUntilNulError) -> Self {
        move |source| Self::new(index, NameErrorKind::Name, NameErrorSource::CStr(source))
    }

    pub(crate) fn utf8_factory(index: u32) -> impl FnOnce(Utf8Error) -> Self {
        move |source| Self::new(index, NameErrorKind::Utf8, NameErrorSource::Utf8(source))
    }
}

impl From<NameError> for HeaderError {
    fn from(value: NameError) -> Self {
        Self {
            kind: HeaderErrorKind::NameTable,
            source: Some(HeaderErrorSource::NameTable(value)),
        }
    }
}

impl Display for NameError {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        #[allow(clippy::enum_glob_use)]
        use NameErrorKind::*;

        match self.kind {
            NameOffset => f.write_str("failed to read offset of stream name"),
            Name => f.write_str("failed to read stream name"),
            Utf8 => f.write_str("stream name was not valid UTF-8"),
        }?;

        f.write_fmt(format_args!(" - stream name at index {}", self.index))
    }
}

impl Error for NameError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match &self.source {
            NameErrorSource::Read(e) => Some(e),
            NameErrorSource::CStr(e) => Some(e),
            NameErrorSource::Utf8(e) => Some(e),
        }
    }
}