#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum BdError {
#[error("unknown file type: {0}")]
UnknownFileType(String),
#[error("unexpected end of input")]
UnexpectedEof,
#[error("unable to locate BD structure")]
StructureNotFound,
#[error("referenced missing clip file: {0}")]
MissingClipFile(String),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("scan cancelled")]
ScanCancelled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScanStage {
Discovery,
ClipInfo,
Playlist,
StreamFile,
SectorRead,
}
impl ScanStage {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Discovery => "discovery",
Self::ClipInfo => "clipinfo",
Self::Playlist => "playlist",
Self::StreamFile => "stream",
Self::SectorRead => "sector",
}
}
}
#[derive(Debug)]
pub struct ScanError {
pub file: String,
pub stage: ScanStage,
pub reason: BdError,
}
impl std::fmt::Display for ScanError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {}: {}", self.stage.label(), self.file, self.reason)
}
}
#[cfg(test)]
mod tests {
use std::error::Error as _;
use std::io;
use super::{BdError, ScanError, ScanStage};
#[test]
fn display_renders_each_variant() {
assert_eq!(
BdError::UnknownFileType("MPLSXXXX".to_owned()).to_string(),
"unknown file type: MPLSXXXX"
);
assert_eq!(BdError::UnexpectedEof.to_string(), "unexpected end of input");
assert_eq!(BdError::StructureNotFound.to_string(), "unable to locate BD structure");
assert_eq!(
BdError::MissingClipFile("00000.CLPI".to_owned()).to_string(),
"referenced missing clip file: 00000.CLPI"
);
let io = BdError::Io(io::Error::new(io::ErrorKind::PermissionDenied, "denied"));
assert_eq!(io.to_string(), "io error: denied");
assert_eq!(BdError::ScanCancelled.to_string(), "scan cancelled");
}
#[test]
fn io_errors_convert_into_bd_error() {
let io = io::Error::new(io::ErrorKind::NotFound, "nope");
let err: BdError = io.into();
assert!(matches!(err, BdError::Io(ref inner) if inner.kind() == io::ErrorKind::NotFound));
assert_eq!(err.to_string(), "io error: nope");
}
#[test]
fn only_the_io_variant_exposes_an_underlying_cause() {
let io = BdError::Io(io::Error::new(io::ErrorKind::UnexpectedEof, "short"));
assert_eq!(io.source().expect("Io carries a source").to_string(), "short");
for sourceless in [
BdError::UnknownFileType("HDMV9999".to_owned()),
BdError::UnexpectedEof,
BdError::StructureNotFound,
BdError::MissingClipFile("00000.CLPI".to_owned()),
BdError::ScanCancelled,
] {
assert!(sourceless.source().is_none());
}
}
#[test]
fn scan_stage_labels_every_variant() {
assert_eq!(ScanStage::Discovery.label(), "discovery");
assert_eq!(ScanStage::ClipInfo.label(), "clipinfo");
assert_eq!(ScanStage::Playlist.label(), "playlist");
assert_eq!(ScanStage::StreamFile.label(), "stream");
assert_eq!(ScanStage::SectorRead.label(), "sector");
assert_eq!(format!("{:?}", ScanStage::ClipInfo), "ClipInfo");
let copied = ScanStage::Playlist;
assert_eq!(copied, ScanStage::Playlist);
assert_ne!(ScanStage::Discovery, ScanStage::SectorRead);
}
#[test]
fn scan_error_displays_stage_file_and_reason() {
let err = ScanError {
file: "00001.CLPI".to_owned(),
stage: ScanStage::ClipInfo,
reason: BdError::UnexpectedEof,
};
assert_eq!(err.to_string(), "clipinfo 00001.CLPI: unexpected end of input");
assert!(format!("{err:?}").contains("ClipInfo")); }
#[test]
fn is_a_std_error_and_debug() {
let boxed: Box<dyn std::error::Error> =
Box::new(BdError::UnknownFileType("HDMVZZZZ".to_owned()));
assert_eq!(boxed.to_string(), "unknown file type: HDMVZZZZ");
assert_eq!(format!("{:?}", BdError::UnexpectedEof), "UnexpectedEof");
}
}