Skip to main content

max / audiofiles

6.8 KB · 214 lines History Blame Raw
1 //! Error types and shared utilities (timestamps, I/O error helpers) for the core crate.
2 //!
3 //! [`CoreError`] is the single error enum used across database, VFS, store, and
4 //! tag operations. This module also provides small helpers for constructing I/O
5 //! errors with path context and for reading the current UNIX timestamp.
6
7 use std::path::PathBuf;
8 use std::time::{SystemTime, UNIX_EPOCH};
9
10 use thiserror::Error;
11
12 use crate::id_types::{CollectionId, NodeId, VfsId};
13
14 /// Unified error type for all audiofiles-core operations.
15 #[derive(Error, Debug)]
16 pub enum CoreError {
17 /// SQLite / rusqlite error.
18 #[error("database error: {0}")]
19 Db(#[from] rusqlite::Error),
20
21 /// Filesystem I/O error with the originating path.
22 #[error("I/O error at {path}: {source}")]
23 Io {
24 /// The path where the I/O error occurred.
25 path: PathBuf,
26 /// The underlying I/O error.
27 source: std::io::Error,
28 },
29
30 /// Referenced sample hash does not exist in the store.
31 #[error("sample not found: {0}")]
32 SampleNotFound(String),
33
34 /// Referenced VFS ID does not exist.
35 #[error("VFS not found: {0}")]
36 VfsNotFound(VfsId),
37
38 /// Referenced node ID does not exist.
39 #[error("node not found: {0}")]
40 NodeNotFound(NodeId),
41
42 /// Referenced collection ID does not exist.
43 #[error("collection not found: {0}")]
44 CollectionNotFound(CollectionId),
45
46 /// A node with the same name already exists in the target directory.
47 #[error("name conflict: {0}")]
48 NameConflict(String),
49
50 /// Tag string failed validation.
51 #[error("invalid tag: {0}")]
52 TagInvalid(String),
53
54 /// Rename pattern parse error.
55 #[error("invalid rename pattern: {0}")]
56 RenameInvalid(String),
57
58 /// Hash string failed validation (not valid hex or wrong length).
59 #[error("invalid hash: {0}")]
60 HashInvalid(String),
61
62 /// Audio analysis error (decode failure, unsupported format, etc.).
63 #[error("analysis error: {0}")]
64 Analysis(#[from] AnalysisError),
65
66 /// Export pipeline error (conversion, encoding, write failure).
67 #[error("export error: {0}")]
68 Export(String),
69
70 /// JSON serialization/deserialization failure.
71 #[error("serialization error: {0}")]
72 Serialization(String),
73
74 /// VFS node name contains invalid characters or is a reserved name.
75 #[error("invalid node name: {0}")]
76 InvalidNodeName(String),
77
78 /// Internal logic error (e.g. invalid arguments to an internal function).
79 #[error("internal error: {0}")]
80 Internal(String),
81 }
82
83 /// Typed errors for the audio analysis/decode pipeline.
84 #[derive(Error, Debug)]
85 pub enum AnalysisError {
86 /// Symphonia failed to identify the audio format.
87 #[error("probe failed: {0}")]
88 ProbeFailed(String),
89
90 /// The file contains no audio tracks.
91 #[error("no audio track found")]
92 NoAudioTrack,
93
94 /// Symphonia could not create a decoder for the codec.
95 #[error("decoder init failed: {0}")]
96 DecoderFailed(String),
97
98 /// Error reading a packet from the demuxer.
99 #[error("packet read error: {0}")]
100 PacketError(String),
101
102 /// Error decoding an audio packet.
103 #[error("decode error: {0}")]
104 DecodeError(String),
105
106 /// The file was opened and decoded but produced zero samples.
107 #[error("no audio data decoded")]
108 NoAudioData,
109 }
110
111 impl From<crate::db::DbError> for CoreError {
112 fn from(e: crate::db::DbError) -> Self {
113 match e {
114 crate::db::DbError::Sqlite(e) => CoreError::Db(e),
115 }
116 }
117 }
118
119 /// Convenience alias for `std::result::Result<T, CoreError>`.
120 pub type Result<T> = std::result::Result<T, CoreError>;
121
122 /// Construct a `CoreError::Io` with path context.
123 pub(crate) fn io_err(path: impl Into<PathBuf>, source: std::io::Error) -> CoreError {
124 CoreError::Io {
125 path: path.into(),
126 source,
127 }
128 }
129
130 /// Return the current time as seconds since the UNIX epoch.
131 ///
132 /// Returns 0 if the system clock is before the UNIX epoch (should never
133 /// happen in practice, but avoids a panic in CLAP/VST3 plugin contexts
134 /// where unwinding would crash the host DAW).
135 pub(crate) fn unix_now() -> i64 {
136 SystemTime::now()
137 .duration_since(UNIX_EPOCH)
138 .unwrap_or_default()
139 .as_secs() as i64
140 }
141
142 #[cfg(test)]
143 mod tests {
144 use super::*;
145
146 #[test]
147 fn display_db_error() {
148 let err = CoreError::Db(rusqlite::Error::InvalidQuery);
149 let msg = format!("{err}");
150 assert!(!msg.is_empty());
151 assert!(msg.contains("database error"));
152 }
153
154 #[test]
155 fn display_io_error() {
156 let err = CoreError::Io {
157 path: PathBuf::from("/tmp/test.wav"),
158 source: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
159 };
160 let msg = format!("{err}");
161 assert!(msg.contains("/tmp/test.wav"));
162 assert!(msg.contains("not found"));
163 }
164
165 #[test]
166 fn display_all_variants_non_empty() {
167 let variants: Vec<CoreError> = vec![
168 CoreError::Db(rusqlite::Error::InvalidQuery),
169 CoreError::Io {
170 path: PathBuf::from("/x"),
171 source: std::io::Error::other("err"),
172 },
173 CoreError::SampleNotFound("hash123".into()),
174 CoreError::VfsNotFound(VfsId::from(42)),
175 CoreError::NodeNotFound(NodeId::from(7)),
176 CoreError::CollectionNotFound(CollectionId::from(1)),
177 CoreError::NameConflict("dup".into()),
178 CoreError::TagInvalid("bad tag".into()),
179 CoreError::RenameInvalid("unclosed brace".into()),
180 CoreError::HashInvalid("bad hash".into()),
181 CoreError::Analysis(AnalysisError::NoAudioData),
182 CoreError::InvalidNodeName("contains /".into()),
183 CoreError::Export("test export error".into()),
184 CoreError::Serialization("test serialization error".into()),
185 CoreError::Internal("test internal error".into()),
186 ];
187 for err in &variants {
188 let msg = format!("{err}");
189 assert!(!msg.is_empty(), "Display for {err:?} was empty");
190 }
191 }
192
193 #[test]
194 fn io_err_helper_builds_correct_variant() {
195 let source = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
196 let err = io_err("/some/path", source);
197 match err {
198 CoreError::Io { path, source } => {
199 assert_eq!(path, PathBuf::from("/some/path"));
200 assert_eq!(source.kind(), std::io::ErrorKind::PermissionDenied);
201 }
202 other => panic!("expected CoreError::Io, got {other:?}"),
203 }
204 }
205
206 #[test]
207 fn unix_now_returns_reasonable_timestamp() {
208 let ts = unix_now();
209 // Should be after 2024-01-01 (1704067200) and before 2100-01-01 (4102444800)
210 assert!(ts > 1_704_067_200, "timestamp {ts} is too old");
211 assert!(ts < 4_102_444_800, "timestamp {ts} is too far in the future");
212 }
213 }
214