//! Error types and shared utilities (timestamps, I/O error helpers) for the core crate. //! //! [`CoreError`] is the single error enum used across database, VFS, store, and //! tag operations. This module also provides small helpers for constructing I/O //! errors with path context and for reading the current UNIX timestamp. use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH}; use thiserror::Error; use crate::id_types::{CollectionId, NodeId, VfsId}; /// Unified error type for all audiofiles-core operations. #[derive(Error, Debug)] pub enum CoreError { /// SQLite / rusqlite error. #[error("database error: {0}")] Db(#[from] rusqlite::Error), /// Filesystem I/O error with the originating path. #[error("I/O error at {path}: {source}")] Io { /// The path where the I/O error occurred. path: PathBuf, /// The underlying I/O error. source: std::io::Error, }, /// Referenced sample hash does not exist in the store. #[error("sample not found: {0}")] SampleNotFound(String), /// Referenced VFS ID does not exist. #[error("VFS not found: {0}")] VfsNotFound(VfsId), /// Referenced node ID does not exist. #[error("node not found: {0}")] NodeNotFound(NodeId), /// Referenced collection ID does not exist. #[error("collection not found: {0}")] CollectionNotFound(CollectionId), /// A node with the same name already exists in the target directory. #[error("name conflict: {0}")] NameConflict(String), /// Tag string failed validation. #[error("invalid tag: {0}")] TagInvalid(String), /// Rename pattern parse error. #[error("invalid rename pattern: {0}")] RenameInvalid(String), /// Hash string failed validation (not valid hex or wrong length). #[error("invalid hash: {0}")] HashInvalid(String), /// Audio analysis error (decode failure, unsupported format, etc.). #[error("analysis error: {0}")] Analysis(#[from] AnalysisError), /// Export pipeline error (conversion, encoding, write failure). #[error("export error: {0}")] Export(String), /// JSON serialization/deserialization failure. #[error("serialization error: {0}")] Serialization(String), /// VFS node name contains invalid characters or is a reserved name. #[error("invalid node name: {0}")] InvalidNodeName(String), /// Internal logic error (e.g. invalid arguments to an internal function). #[error("internal error: {0}")] Internal(String), } /// Typed errors for the audio analysis/decode pipeline. #[derive(Error, Debug)] pub enum AnalysisError { /// Symphonia failed to identify the audio format. #[error("probe failed: {0}")] ProbeFailed(String), /// The file contains no audio tracks. #[error("no audio track found")] NoAudioTrack, /// Symphonia could not create a decoder for the codec. #[error("decoder init failed: {0}")] DecoderFailed(String), /// Error reading a packet from the demuxer. #[error("packet read error: {0}")] PacketError(String), /// Error decoding an audio packet. #[error("decode error: {0}")] DecodeError(String), /// The file was opened and decoded but produced zero samples. #[error("no audio data decoded")] NoAudioData, } impl From for CoreError { fn from(e: crate::db::DbError) -> Self { match e { crate::db::DbError::Sqlite(e) => CoreError::Db(e), } } } /// Convenience alias for `std::result::Result`. pub type Result = std::result::Result; /// Construct a `CoreError::Io` with path context. pub(crate) fn io_err(path: impl Into, source: std::io::Error) -> CoreError { CoreError::Io { path: path.into(), source, } } /// Return the current time as seconds since the UNIX epoch. /// /// Returns 0 if the system clock is before the UNIX epoch (should never /// happen in practice, but avoids a panic in CLAP/VST3 plugin contexts /// where unwinding would crash the host DAW). pub(crate) fn unix_now() -> i64 { SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() as i64 } #[cfg(test)] mod tests { use super::*; #[test] fn display_db_error() { let err = CoreError::Db(rusqlite::Error::InvalidQuery); let msg = format!("{err}"); assert!(!msg.is_empty()); assert!(msg.contains("database error")); } #[test] fn display_io_error() { let err = CoreError::Io { path: PathBuf::from("/tmp/test.wav"), source: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"), }; let msg = format!("{err}"); assert!(msg.contains("/tmp/test.wav")); assert!(msg.contains("not found")); } #[test] fn display_all_variants_non_empty() { let variants: Vec = vec![ CoreError::Db(rusqlite::Error::InvalidQuery), CoreError::Io { path: PathBuf::from("/x"), source: std::io::Error::other("err"), }, CoreError::SampleNotFound("hash123".into()), CoreError::VfsNotFound(VfsId::from(42)), CoreError::NodeNotFound(NodeId::from(7)), CoreError::CollectionNotFound(CollectionId::from(1)), CoreError::NameConflict("dup".into()), CoreError::TagInvalid("bad tag".into()), CoreError::RenameInvalid("unclosed brace".into()), CoreError::HashInvalid("bad hash".into()), CoreError::Analysis(AnalysisError::NoAudioData), CoreError::InvalidNodeName("contains /".into()), CoreError::Export("test export error".into()), CoreError::Serialization("test serialization error".into()), CoreError::Internal("test internal error".into()), ]; for err in &variants { let msg = format!("{err}"); assert!(!msg.is_empty(), "Display for {err:?} was empty"); } } #[test] fn io_err_helper_builds_correct_variant() { let source = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"); let err = io_err("/some/path", source); match err { CoreError::Io { path, source } => { assert_eq!(path, PathBuf::from("/some/path")); assert_eq!(source.kind(), std::io::ErrorKind::PermissionDenied); } other => panic!("expected CoreError::Io, got {other:?}"), } } #[test] fn unix_now_returns_reasonable_timestamp() { let ts = unix_now(); // Should be after 2024-01-01 (1704067200) and before 2100-01-01 (4102444800) assert!(ts > 1_704_067_200, "timestamp {ts} is too old"); assert!(ts < 4_102_444_800, "timestamp {ts} is too far in the future"); } }