Skip to main content

native/exmpeg_native/src/errors.rs

//! Categorized errors for the NIF boundary.
//!
//! Every entry point returns `Result<T, NativeError>` and `NativeError` is
//! encoded as `{:error, %{type, message, details}}` on the Elixir side.
//! `type` carries the category (`"invalid_request"`, `"io_error"`,
//! `"decode_error"`, `"encode_error"`, `"unsupported"`, `"runtime_error"`,
//! `"nif_panic"`), and Elixir maps it back to a `Exmpeg.Error.reason` atom.

use std::collections::HashMap;

use rustler::NifMap;

/// Structured error returned to Elixir as a `NifMap`.
#[derive(Debug, NifMap)]
pub(crate) struct NativeError {
    /// Error category. Stable, matches `Exmpeg.Error` reason strings.
    pub(crate) r#type: String,
    /// Human-readable message. Safe to surface to end users.
    pub(crate) message: String,
    /// Free-form key/value details (paths, codec names, offsets, etc.).
    pub(crate) details: HashMap<String, String>,
}

impl NativeError {
    pub(crate) fn new(type_name: &str, message: impl Into<String>) -> Self {
        Self {
            r#type: type_name.to_owned(),
            message: message.into(),
            details: HashMap::new(),
        }
    }

    pub(crate) fn with_detail(mut self, key: &str, value: impl Into<String>) -> Self {
        self.details.insert(key.to_owned(), value.into());
        self
    }
}

/// Convenience: maps an `rsmpeg::error::RsmpegError` to a categorized
/// `NativeError`. The mapping is best-effort: file-not-found / format
/// problems become `io_error`, codec/decoder problems become
/// `decode_error`, and everything else falls through to `runtime_error`.
impl From<rsmpeg::error::RsmpegError> for NativeError {
    fn from(err: rsmpeg::error::RsmpegError) -> Self {
        // RsmpegError is a flat enum; classify by the variant's debug name
        // so we stay forward-compatible across minor rsmpeg releases that
        // add or rename variant payloads. The Display impl carries the
        // human-readable message.
        let kind = classify_rsmpeg_error(&err);
        NativeError::new(kind, format!("{err}"))
    }
}

fn classify_rsmpeg_error(err: &rsmpeg::error::RsmpegError) -> &'static str {
    let dbg = format!("{err:?}");
    if dbg.starts_with("OpenInputError")
        || dbg.starts_with("FindStreamInfoError")
        || dbg.starts_with("AVError")
    {
        "io_error"
    } else if dbg.starts_with("SendPacket")
        || dbg.starts_with("ReceiveFrame")
        || dbg.starts_with("Decoder")
    {
        "decode_error"
    } else if dbg.starts_with("SendFrame")
        || dbg.starts_with("ReceivePacket")
        || dbg.starts_with("Encoder")
    {
        "encode_error"
    } else {
        // BufferSink, Bitstream, AVFrame*, TryFromIntError, Unknown all
        // surface as runtime faults rather than user-attributable errors.
        "runtime_error"
    }
}