qubit_mime/mime_error.rs
1// =============================================================================
2// Copyright (c) 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Error type used by MIME database parsing and detection.
9
10use thiserror::Error;
11
12use crate::ProviderRegistryError;
13
14/// Error type for MIME repository parsing and I/O backed detection.
15#[derive(Debug, Error)]
16pub enum MimeError {
17 /// A glob weight was outside the freedesktop MIME range `0..=100`.
18 #[error("invalid MIME glob weight: {weight}")]
19 InvalidGlobWeight {
20 /// Invalid glob weight.
21 weight: u16,
22 },
23
24 /// A magic matcher definition is internally inconsistent.
25 #[error("invalid MIME magic matcher: {reason}")]
26 InvalidMagicMatcher {
27 /// Human-readable validation failure.
28 reason: String,
29 },
30
31 /// An XML attribute is missing or malformed.
32 #[error(
33 "invalid XML attribute '{attribute}' on <{element}>: '{value}' ({reason})"
34 )]
35 InvalidXmlAttribute {
36 /// Element carrying the invalid attribute.
37 element: String,
38 /// Invalid attribute name.
39 attribute: String,
40 /// Invalid attribute value.
41 value: String,
42 /// Human-readable validation failure.
43 reason: String,
44 },
45
46 /// An XML element is missing required content or has invalid children.
47 #[error("invalid XML element <{element}>: {reason}")]
48 InvalidXmlElement {
49 /// Invalid element name.
50 element: String,
51 /// Human-readable validation failure.
52 reason: String,
53 },
54
55 /// A detector or classifier input cannot be processed.
56 #[error("invalid MIME classifier input: {reason}")]
57 InvalidClassifierInput {
58 /// Human-readable validation failure.
59 reason: String,
60 },
61
62 /// A detector read path would allocate a buffer above the configured limit.
63 #[error(
64 "MIME detector buffer allocation exceeds configured limit: requested {requested} bytes, limit {limit} bytes"
65 )]
66 BufferLimitExceeded {
67 /// Requested byte buffer size.
68 requested: usize,
69 /// Configured maximum byte buffer size.
70 limit: usize,
71 },
72
73 /// A detector provider name or alias is already registered.
74 #[error("duplicate MIME detector name or alias: {name}")]
75 DuplicateDetectorName {
76 /// Duplicate provider name or alias.
77 name: String,
78 },
79
80 /// A detector provider name or alias is empty.
81 #[error("MIME detector name must not be empty")]
82 EmptyDetectorName,
83
84 /// A detector provider name or alias is malformed.
85 #[error("invalid MIME detector name '{name}': {reason}")]
86 InvalidDetectorName {
87 /// Invalid provider name.
88 name: String,
89 /// Human-readable validation failure.
90 reason: String,
91 },
92
93 /// A detector provider could not be found.
94 #[error("unknown MIME detector: {name}")]
95 UnknownDetector {
96 /// Requested provider name or alias.
97 name: String,
98 },
99
100 /// A detector provider exists but is not available in this environment.
101 #[error("MIME detector '{name}' is unavailable: {reason}")]
102 DetectorUnavailable {
103 /// Requested provider name or alias.
104 name: String,
105 /// Human-readable unavailability reason.
106 reason: String,
107 },
108
109 /// No configured detector provider could be created.
110 #[error("no available MIME detector: {reason}")]
111 NoAvailableDetector {
112 /// Human-readable failure summary.
113 reason: String,
114 },
115
116 /// A detector backend failed with an implementation-specific error.
117 #[error("MIME detector backend '{backend}' failed: {reason}")]
118 DetectorBackend {
119 /// Backend identifier.
120 backend: String,
121 /// Human-readable failure reason.
122 reason: String,
123 },
124
125 /// A media stream classifier provider name or alias is already registered.
126 #[error("duplicate media stream classifier name or alias: {name}")]
127 DuplicateClassifierName {
128 /// Duplicate provider name or alias.
129 name: String,
130 },
131
132 /// A media stream classifier provider name or alias is empty.
133 #[error("media stream classifier name must not be empty")]
134 EmptyClassifierName,
135
136 /// A media stream classifier provider name or alias is malformed.
137 #[error("invalid media stream classifier name '{name}': {reason}")]
138 InvalidClassifierName {
139 /// Invalid provider name.
140 name: String,
141 /// Human-readable validation failure.
142 reason: String,
143 },
144
145 /// A media stream classifier provider could not be found.
146 #[error("unknown media stream classifier: {name}")]
147 UnknownClassifier {
148 /// Requested provider name or alias.
149 name: String,
150 },
151
152 /// A media stream classifier provider exists but is not available in this
153 /// environment.
154 #[error("media stream classifier '{name}' is unavailable: {reason}")]
155 ClassifierUnavailable {
156 /// Requested provider name or alias.
157 name: String,
158 /// Human-readable unavailability reason.
159 reason: String,
160 },
161
162 /// No configured media stream classifier provider could be created.
163 #[error("no available media stream classifier: {reason}")]
164 NoAvailableClassifier {
165 /// Human-readable failure summary.
166 reason: String,
167 },
168
169 /// A media stream classifier backend failed with an implementation-specific
170 /// error.
171 #[error("media stream classifier backend '{backend}' failed: {reason}")]
172 ClassifierBackend {
173 /// Backend identifier.
174 backend: String,
175 /// Human-readable failure reason.
176 reason: String,
177 },
178
179 /// The XML document could not be parsed.
180 #[error("failed to parse MIME XML: {0}")]
181 Xml(#[from] roxmltree::Error),
182
183 /// Detection from a path or reader failed due to I/O.
184 #[error("I/O error while detecting MIME type: {0}")]
185 Io(#[from] std::io::Error),
186
187 /// Detection using an external command failed.
188 #[error("command error while detecting MIME type: {0}")]
189 Command(#[from] qubit_command::CommandError),
190
191 /// Loading MIME configuration failed.
192 #[error("configuration error while loading MIME settings: {0}")]
193 Config(#[from] qubit_config::ConfigError),
194}
195
196impl From<ProviderRegistryError> for MimeError {
197 /// Converts a generic SPI registry error into a MIME-domain error.
198 fn from(error: ProviderRegistryError) -> Self {
199 match error {
200 ProviderRegistryError::EmptyProviderName => Self::EmptyDetectorName,
201 ProviderRegistryError::InvalidProviderName { name, reason } => {
202 Self::InvalidDetectorName { name, reason }
203 }
204 ProviderRegistryError::DuplicateProviderName { name }
205 | ProviderRegistryError::DuplicateProviderCandidate { name } => {
206 Self::DuplicateDetectorName {
207 name: name.as_str().to_owned(),
208 }
209 }
210 ProviderRegistryError::UnknownProvider { name } => {
211 Self::UnknownDetector {
212 name: name.as_str().to_owned(),
213 }
214 }
215 ProviderRegistryError::ProviderUnavailable { name, source } => {
216 Self::DetectorUnavailable {
217 name: name.as_str().to_owned(),
218 reason: source.reason().to_owned(),
219 }
220 }
221 ProviderRegistryError::ProviderCreate { name, source } => {
222 Self::DetectorBackend {
223 backend: name.as_str().to_owned(),
224 reason: source.reason().to_owned(),
225 }
226 }
227 ProviderRegistryError::NoAvailableProvider { failures } => {
228 Self::NoAvailableDetector {
229 reason: failures
230 .iter()
231 .map(ToString::to_string)
232 .collect::<Vec<_>>()
233 .join("; "),
234 }
235 }
236 ProviderRegistryError::EmptyRegistry => Self::NoAvailableDetector {
237 reason: "detector registry is empty".to_owned(),
238 },
239 }
240 }
241}
242
243impl MimeError {
244 /// Builds an invalid XML attribute error.
245 ///
246 /// # Parameters
247 /// - `element`: Element carrying the attribute.
248 /// - `attribute`: Attribute name.
249 /// - `value`: Attribute value.
250 /// - `reason`: Why the value is invalid.
251 ///
252 /// # Returns
253 /// A [`MimeError::InvalidXmlAttribute`](crate::MimeError::InvalidXmlAttribute) value.
254 pub(crate) fn invalid_attr(
255 element: &str,
256 attribute: &str,
257 value: &str,
258 reason: impl Into<String>,
259 ) -> Self {
260 Self::InvalidXmlAttribute {
261 element: element.to_owned(),
262 attribute: attribute.to_owned(),
263 value: value.to_owned(),
264 reason: reason.into(),
265 }
266 }
267
268 /// Builds an invalid XML element error.
269 ///
270 /// # Parameters
271 /// - `element`: Invalid element name.
272 /// - `reason`: Why the element is invalid.
273 ///
274 /// # Returns
275 /// A [`MimeError::InvalidXmlElement`](crate::MimeError::InvalidXmlElement)
276 /// value.
277 pub(crate) fn invalid_element(
278 element: &str,
279 reason: impl Into<String>,
280 ) -> Self {
281 Self::InvalidXmlElement {
282 element: element.to_owned(),
283 reason: reason.into(),
284 }
285 }
286
287 /// Builds an invalid magic matcher error.
288 ///
289 /// # Parameters
290 /// - `reason`: Why the matcher is invalid.
291 ///
292 /// # Returns
293 /// A [`MimeError::InvalidMagicMatcher`](crate::MimeError::InvalidMagicMatcher) value.
294 pub(crate) fn invalid_matcher(reason: impl Into<String>) -> Self {
295 Self::InvalidMagicMatcher {
296 reason: reason.into(),
297 }
298 }
299
300 /// Builds an invalid classifier input error.
301 ///
302 /// # Parameters
303 /// - `reason`: Why the input cannot be classified.
304 ///
305 /// # Returns
306 /// A [`MimeError::InvalidClassifierInput`](crate::MimeError::InvalidClassifierInput) value.
307 pub(crate) fn invalid_classifier_input(reason: impl Into<String>) -> Self {
308 Self::InvalidClassifierInput {
309 reason: reason.into(),
310 }
311 }
312
313 /// Builds a detector backend error.
314 ///
315 /// # Parameters
316 /// - `backend`: Detector backend identifier.
317 /// - `reason`: Why the backend failed.
318 ///
319 /// # Returns
320 /// A [`MimeError::DetectorBackend`](crate::MimeError::DetectorBackend)
321 /// value.
322 pub fn detector_backend(
323 backend: impl Into<String>,
324 reason: impl Into<String>,
325 ) -> Self {
326 Self::DetectorBackend {
327 backend: backend.into(),
328 reason: reason.into(),
329 }
330 }
331
332 /// Converts a generic SPI registry error into a classifier-domain error.
333 ///
334 /// # Parameters
335 /// - `error`: Provider registry error returned by `qubit-spi`.
336 ///
337 /// # Returns
338 /// Classifier-specific MIME error.
339 pub(crate) fn classifier_registry_error(
340 error: ProviderRegistryError,
341 ) -> Self {
342 match error {
343 ProviderRegistryError::EmptyProviderName => {
344 Self::EmptyClassifierName
345 }
346 ProviderRegistryError::InvalidProviderName { name, reason } => {
347 Self::InvalidClassifierName { name, reason }
348 }
349 ProviderRegistryError::DuplicateProviderName { name }
350 | ProviderRegistryError::DuplicateProviderCandidate { name } => {
351 Self::DuplicateClassifierName {
352 name: name.as_str().to_owned(),
353 }
354 }
355 ProviderRegistryError::UnknownProvider { name } => {
356 Self::UnknownClassifier {
357 name: name.as_str().to_owned(),
358 }
359 }
360 ProviderRegistryError::ProviderUnavailable { name, source } => {
361 Self::ClassifierUnavailable {
362 name: name.as_str().to_owned(),
363 reason: source.reason().to_owned(),
364 }
365 }
366 ProviderRegistryError::ProviderCreate { name, source } => {
367 Self::ClassifierBackend {
368 backend: name.as_str().to_owned(),
369 reason: source.reason().to_owned(),
370 }
371 }
372 ProviderRegistryError::NoAvailableProvider { failures } => {
373 Self::NoAvailableClassifier {
374 reason: failures
375 .iter()
376 .map(ToString::to_string)
377 .collect::<Vec<_>>()
378 .join("; "),
379 }
380 }
381 ProviderRegistryError::EmptyRegistry => {
382 Self::NoAvailableClassifier {
383 reason: "classifier registry is empty".to_owned(),
384 }
385 }
386 }
387 }
388}