qubit-mime 0.2.0

MIME type detection utilities for Rust based on filename glob rules and content magic
Documentation
/*******************************************************************************
 *
 *    Copyright (c) 2026 Haixing Hu.
 *
 *    SPDX-License-Identifier: Apache-2.0
 *
 *    Licensed under the Apache License, Version 2.0.
 *
 ******************************************************************************/
//! Error type used by MIME database parsing and detection.
//!

use thiserror::Error;

/// Error type for MIME repository parsing and I/O backed detection.
#[derive(Debug, Error)]
pub enum MimeError {
    /// A glob weight was outside the freedesktop MIME range `0..=100`.
    #[error("invalid MIME glob weight: {weight}")]
    InvalidGlobWeight {
        /// Invalid glob weight.
        weight: u16,
    },

    /// A magic matcher definition is internally inconsistent.
    #[error("invalid MIME magic matcher: {reason}")]
    InvalidMagicMatcher {
        /// Human-readable validation failure.
        reason: String,
    },

    /// An XML attribute is missing or malformed.
    #[error("invalid XML attribute '{attribute}' on <{element}>: '{value}' ({reason})")]
    InvalidXmlAttribute {
        /// Element carrying the invalid attribute.
        element: String,
        /// Invalid attribute name.
        attribute: String,
        /// Invalid attribute value.
        value: String,
        /// Human-readable validation failure.
        reason: String,
    },

    /// An XML element is missing required content or has invalid children.
    #[error("invalid XML element <{element}>: {reason}")]
    InvalidXmlElement {
        /// Invalid element name.
        element: String,
        /// Human-readable validation failure.
        reason: String,
    },

    /// A detector or classifier input cannot be processed.
    #[error("invalid MIME classifier input: {reason}")]
    InvalidClassifierInput {
        /// Human-readable validation failure.
        reason: String,
    },

    /// The XML document could not be parsed.
    #[error("failed to parse MIME XML: {0}")]
    Xml(#[from] roxmltree::Error),

    /// Detection from a path or reader failed due to I/O.
    #[error("I/O error while detecting MIME type: {0}")]
    Io(#[from] std::io::Error),

    /// Detection using an external command failed.
    #[error("command error while detecting MIME type: {0}")]
    Command(#[from] qubit_command::CommandError),

    /// Loading MIME configuration failed.
    #[error("configuration error while loading MIME settings: {0}")]
    Config(#[from] qubit_config::ConfigError),
}

impl MimeError {
    /// Builds an invalid XML attribute error.
    ///
    /// # Parameters
    /// - `element`: Element carrying the attribute.
    /// - `attribute`: Attribute name.
    /// - `value`: Attribute value.
    /// - `reason`: Why the value is invalid.
    ///
    /// # Returns
    /// A [`MimeError::InvalidXmlAttribute`](crate::MimeError::InvalidXmlAttribute) value.
    pub(crate) fn invalid_attr(
        element: &str,
        attribute: &str,
        value: &str,
        reason: impl Into<String>,
    ) -> Self {
        Self::InvalidXmlAttribute {
            element: element.to_owned(),
            attribute: attribute.to_owned(),
            value: value.to_owned(),
            reason: reason.into(),
        }
    }

    /// Builds an invalid XML element error.
    ///
    /// # Parameters
    /// - `element`: Invalid element name.
    /// - `reason`: Why the element is invalid.
    ///
    /// # Returns
    /// A [`MimeError::InvalidXmlElement`](crate::MimeError::InvalidXmlElement) value.
    pub(crate) fn invalid_element(element: &str, reason: impl Into<String>) -> Self {
        Self::InvalidXmlElement {
            element: element.to_owned(),
            reason: reason.into(),
        }
    }

    /// Builds an invalid magic matcher error.
    ///
    /// # Parameters
    /// - `reason`: Why the matcher is invalid.
    ///
    /// # Returns
    /// A [`MimeError::InvalidMagicMatcher`](crate::MimeError::InvalidMagicMatcher) value.
    pub(crate) fn invalid_matcher(reason: impl Into<String>) -> Self {
        Self::InvalidMagicMatcher {
            reason: reason.into(),
        }
    }

    /// Builds an invalid classifier input error.
    ///
    /// # Parameters
    /// - `reason`: Why the input cannot be classified.
    ///
    /// # Returns
    /// A [`MimeError::InvalidClassifierInput`](crate::MimeError::InvalidClassifierInput) value.
    pub(crate) fn invalid_classifier_input(reason: impl Into<String>) -> Self {
        Self::InvalidClassifierInput {
            reason: reason.into(),
        }
    }
}

#[cfg(coverage)]
pub(crate) mod coverage_support {
    //! Coverage helpers for error builder branches.

    use super::MimeError;

    /// Exercises internal error constructors.
    ///
    /// # Returns
    /// Display strings for constructed errors.
    pub(crate) fn exercise_error_builders() -> Vec<String> {
        let command_error = qubit_command::CommandError::SpawnFailed {
            command: "file --mime-type --brief missing".to_owned(),
            source: std::io::Error::new(std::io::ErrorKind::NotFound, "missing"),
        };
        let config_error = qubit_config::ConfigError::Other("bad config".to_owned());
        vec![
            MimeError::invalid_attr("match", "value", "bad", "invalid").to_string(),
            MimeError::invalid_element("magic", "missing match").to_string(),
            MimeError::invalid_matcher("bad matcher").to_string(),
            MimeError::invalid_classifier_input("bad input").to_string(),
            MimeError::Command(command_error).to_string(),
            MimeError::Config(config_error).to_string(),
        ]
    }
}