rsmediainfo 0.2.0

Rust wrapper for MediaInfo library
//! Error types and the [`Result`] alias used throughout the crate.
//!
//! Every fallible call returns [`Result<T>`](Result), which is just
//! `std::result::Result<T, MediaInfoError>`. Each variant of
//! [`MediaInfoError`] corresponds to one specific failure mode and carries
//! the smallest payload that lets the caller act on it.

use std::path::PathBuf;
use thiserror::Error;

/// How invalid UTF-8 should be handled when decoding XML output from the
/// underlying library.
///
/// The XML payload returned by the parser is supposed to be valid UTF-8, but
/// in practice attribute values can occasionally contain stray bytes
/// (filenames with mixed encodings, embedded metadata fields with corrupt
/// text, and so on). This enum lets the caller pick the policy that best
/// fits their use case.
///
/// # Example
///
/// ```
/// use rsmediainfo::{ParseOptions, EncodingErrorMode};
///
/// let options = ParseOptions::new()
///     .encoding_errors(EncodingErrorMode::Replace);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EncodingErrorMode {
    /// Return [`MediaInfoError::XmlParseError`] as soon as an invalid byte
    /// sequence is encountered. This is the safest mode and the default.
    #[default]
    Strict,

    /// Substitute the Unicode replacement character (`U+FFFD`) for any
    /// invalid byte sequence and continue. The output is always valid
    /// UTF-8 but the original byte values are lost.
    Replace,

    /// Silently drop invalid byte sequences from the output. Use with care
    /// — this can produce subtly wrong results when the discarded bytes
    /// were meaningful.
    Ignore,

    /// Render each unrecognised byte as a `\xNN` two-digit hexadecimal
    /// escape, preserving the original byte value while keeping the
    /// surrounding text intact.
    BackslashReplace,

    /// Render each unrecognised byte as an XML numeric character reference
    /// (`&#NN;`), preserving the original byte value in a form that is
    /// safe to embed in XML payloads.
    XmlCharRefReplace,
}

/// Result alias used by every fallible function in the crate.
pub type Result<T> = std::result::Result<T, MediaInfoError>;

/// Errors that can be produced anywhere in the crate.
///
/// Each variant covers a single, well-defined failure mode. The carried
/// payload is whatever the caller needs to react to it (a path, a list of
/// load attempts, a free-form message, …). The `Display` impl on each
/// variant is part of the public contract — see the variant docs for the
/// exact text.
#[derive(Error, Debug)]
pub enum MediaInfoError {
    /// The shared library could not be loaded from any of the candidate
    /// paths the loader tried.
    ///
    /// The carried `paths` and `errors` fields are comma-separated lists
    /// (one entry per attempted path) so the resulting `Display` text
    /// shows every load attempt and the system error that caused each one
    /// to fail.
    #[error("Failed to load library from {paths} - {errors}")]
    LibraryNotFound {
        /// The library paths that were attempted, joined with `, `.
        paths: String,
        /// The error messages from each load attempt, joined with `, `.
        errors: String,
    },

    /// The requested file does not exist on disk.
    ///
    /// The carried path is the literal value the caller passed in (after
    /// the lossless `Path` → string conversion).
    #[error("{0}")]
    FileNotFound(PathBuf),

    /// The underlying library opened the input but could not parse it.
    ///
    /// This is also returned when the library reports an open failure for
    /// a real file or URL (the `MediaInfo_Open` FFI call returning `0`).
    /// The carried `filename` is the literal path or URL that failed.
    #[error("An error occured while opening {filename} with libmediainfo")]
    ParseError {
        /// The filename or URL that failed to parse.
        filename: String,
    },

    /// An option combination was rejected before the parse started.
    ///
    /// Example: requesting a raw output mode through one of the
    /// structured-only entrypoints. The carried message describes the
    /// specific violation.
    #[error("{0}")]
    InvalidInput(String),

    /// The XML payload returned by the library could not be parsed, or an
    /// invalid byte sequence was encountered under the strict encoding
    /// mode.
    ///
    /// The carried message comes from the underlying XML parser or from
    /// [`EncodingErrorMode::Strict`].
    #[error("{0}")]
    XmlParseError(String),

    /// The library was loaded but reported a version string the crate
    /// could not interpret.
    #[error("Could not determine library version")]
    VersionDetectionFailed,

    /// An I/O failure occurred while reading from a user-supplied reader
    /// in the buffer-protocol path. Wraps the underlying [`std::io::Error`].
    #[error("I/O error: {0}")]
    IoError(#[from] std::io::Error),

    /// A parse call on a [`MediaInfoContext`](crate::MediaInfoContext)
    /// was given [`ParseOptions`](crate::ParseOptions) whose
    /// `library_file` or `library_search_dir` would require a different
    /// shared library than the one the context already loaded.
    ///
    /// A context is bound to a single library for its lifetime — if the
    /// caller needs a different library, they should build a separate
    /// context via
    /// [`MediaInfoContext::with_library_file`](crate::MediaInfoContext::with_library_file)
    /// or
    /// [`MediaInfoContext::with_library_search_dir`](crate::MediaInfoContext::with_library_search_dir).
    #[error("requested library '{requested}' does not match context library '{context}'")]
    LibraryMismatch {
        /// The library path the context was constructed with, or an
        /// empty path when the context used the default search order.
        context: PathBuf,
        /// The conflicting library path the parse call requested.
        requested: PathBuf,
    },
}

impl MediaInfoError {
    /// Builds a [`MediaInfoError::LibraryNotFound`] from a list of attempted
    /// paths and the error message produced for each one.
    ///
    /// The two slices must be the same length and use the same ordering;
    /// the entries at index `i` are paired together in the rendered error
    /// message.
    ///
    /// # Example
    ///
    /// ```
    /// use rsmediainfo::MediaInfoError;
    ///
    /// let err = MediaInfoError::library_not_found(
    ///     &["libmediainfo.so.0".to_string()],
    ///     &["No such file or directory".to_string()],
    /// );
    /// assert!(err.to_string().contains("libmediainfo.so.0"));
    /// ```
    pub fn library_not_found(paths: &[String], errors: &[String]) -> Self {
        MediaInfoError::LibraryNotFound {
            paths: paths.join(", "),
            errors: errors.join(", "),
        }
    }

    /// Builds a [`MediaInfoError::FileNotFound`] from any path-like value.
    ///
    /// # Example
    ///
    /// ```
    /// use rsmediainfo::MediaInfoError;
    ///
    /// let err = MediaInfoError::file_not_found("/missing/file.mp4");
    /// assert_eq!(err.to_string(), "/missing/file.mp4");
    /// ```
    pub fn file_not_found<P: Into<PathBuf>>(path: P) -> Self {
        MediaInfoError::FileNotFound(path.into())
    }

    /// Builds a [`MediaInfoError::ParseError`] for a given filename or URL.
    pub fn parse_error<S: Into<String>>(filename: S) -> Self {
        MediaInfoError::ParseError {
            filename: filename.into(),
        }
    }

    /// Builds a [`MediaInfoError::InvalidInput`] from a free-form message.
    pub fn invalid_input<S: Into<String>>(message: S) -> Self {
        MediaInfoError::InvalidInput(message.into())
    }

    /// Builds a [`MediaInfoError::XmlParseError`] from a free-form message.
    pub fn xml_parse_error<S: Into<String>>(message: S) -> Self {
        MediaInfoError::XmlParseError(message.into())
    }

    /// Builds a [`MediaInfoError::LibraryMismatch`] from the context
    /// library path and the conflicting requested library path.
    ///
    /// # Example
    ///
    /// ```
    /// use rsmediainfo::MediaInfoError;
    ///
    /// let err = MediaInfoError::library_mismatch(
    ///     "/usr/lib/libmediainfo.so",
    ///     "/opt/custom/libmediainfo.so",
    /// );
    /// assert!(err.to_string().contains("/opt/custom/libmediainfo.so"));
    /// ```
    pub fn library_mismatch<C, R>(context: C, requested: R) -> Self
    where
        C: Into<PathBuf>,
        R: Into<PathBuf>,
    {
        MediaInfoError::LibraryMismatch {
            context: context.into(),
            requested: requested.into(),
        }
    }
}

/// Routes XML parser errors into [`MediaInfoError::XmlParseError`] so the
/// internal parser body can use `?` directly.
impl From<quick_xml::Error> for MediaInfoError {
    fn from(err: quick_xml::Error) -> Self {
        MediaInfoError::XmlParseError(err.to_string())
    }
}

/// Routes XML attribute iteration errors into
/// [`MediaInfoError::XmlParseError`] so the internal parser body can use `?`
/// when walking attribute lists.
impl From<quick_xml::events::attributes::AttrError> for MediaInfoError {
    fn from(err: quick_xml::events::attributes::AttrError) -> Self {
        MediaInfoError::XmlParseError(err.to_string())
    }
}

/// Routes UTF-8 slice decode failures into [`MediaInfoError::XmlParseError`].
impl From<std::str::Utf8Error> for MediaInfoError {
    fn from(err: std::str::Utf8Error) -> Self {
        MediaInfoError::XmlParseError(format!("UTF-8 decoding error: {}", err))
    }
}

/// Routes owned-string UTF-8 decode failures into
/// [`MediaInfoError::XmlParseError`].
impl From<std::string::FromUtf8Error> for MediaInfoError {
    fn from(err: std::string::FromUtf8Error) -> Self {
        MediaInfoError::XmlParseError(format!("UTF-8 decoding error: {}", err))
    }
}