Skip to main content

ff_format/
error.rs

1//! Error types for ff-format crate.
2//!
3//! This module defines error types used across the ff-* crate family.
4//! [`FormatError`] is the main error type for format-related operations,
5//! while [`FrameError`] handles frame-specific operations.
6//!
7//! # Examples
8//!
9//! ```
10//! use ff_format::error::FormatError;
11//!
12//! fn validate_format(name: &str) -> Result<(), FormatError> {
13//!     if name.is_empty() {
14//!         return Err(FormatError::InvalidPixelFormat {
15//!             format: name.to_string(),
16//!         });
17//!     }
18//!     Ok(())
19//! }
20//! ```
21
22use std::fmt;
23
24use thiserror::Error;
25
26use crate::{PixelFormat, Rational, SampleFormat};
27
28/// Error type for format-related operations.
29///
30/// This is the main error type for the ff-format crate and is used
31/// for errors related to pixel formats, sample formats, timestamps,
32/// and format conversions.
33///
34/// # Error Variants
35///
36/// - [`InvalidPixelFormat`](FormatError::InvalidPixelFormat): Invalid or unsupported pixel format string
37/// - [`InvalidSampleFormat`](FormatError::InvalidSampleFormat): Invalid or unsupported sample format string
38/// - [`InvalidTimestamp`](FormatError::InvalidTimestamp): Invalid timestamp with PTS and time base
39/// - [`PlaneIndexOutOfBounds`](FormatError::PlaneIndexOutOfBounds): Plane index exceeds available planes
40/// - [`ConversionFailed`](FormatError::ConversionFailed): Pixel format conversion failed
41/// - [`AudioConversionFailed`](FormatError::AudioConversionFailed): Audio sample format conversion failed
42/// - [`InvalidFrameData`](FormatError::InvalidFrameData): General frame data validation error
43///
44/// # Examples
45///
46/// ```
47/// use ff_format::{FormatError, PixelFormat, Rational};
48///
49/// // Create an invalid timestamp error
50/// let error = FormatError::InvalidTimestamp {
51///     pts: -1,
52///     time_base: Rational::new(1, 90000),
53/// };
54/// assert!(error.to_string().contains("pts=-1"));
55///
56/// // Create a plane index out of bounds error
57/// let error = FormatError::PlaneIndexOutOfBounds {
58///     index: 5,
59///     max: 3,
60/// };
61/// assert!(error.to_string().contains("out of bounds"));
62/// ```
63#[derive(Error, Debug, Clone, PartialEq)]
64pub enum FormatError {
65    /// Invalid or unrecognized pixel format string.
66    ///
67    /// This error occurs when parsing a pixel format name that is not
68    /// recognized or supported.
69    #[error("Invalid pixel format: {format}")]
70    InvalidPixelFormat {
71        /// The invalid pixel format string.
72        format: String,
73    },
74
75    /// Invalid or unrecognized sample format string.
76    ///
77    /// This error occurs when parsing a sample format name that is not
78    /// recognized or supported.
79    #[error("Invalid sample format: {format}")]
80    InvalidSampleFormat {
81        /// The invalid sample format string.
82        format: String,
83    },
84
85    /// Invalid timestamp value.
86    ///
87    /// This error occurs when a timestamp has an invalid PTS value
88    /// or incompatible time base.
89    #[error("Invalid timestamp: pts={pts}, time_base={time_base:?}")]
90    InvalidTimestamp {
91        /// The PTS (Presentation Timestamp) value.
92        pts: i64,
93        /// The time base used for the timestamp.
94        time_base: Rational,
95    },
96
97    /// Plane index exceeds the number of available planes.
98    ///
99    /// This error occurs when trying to access a plane that doesn't exist
100    /// in the frame. For example, accessing plane 3 of an RGB image that
101    /// only has plane 0.
102    #[error("Plane index {index} out of bounds (max: {max})")]
103    PlaneIndexOutOfBounds {
104        /// The requested plane index.
105        index: usize,
106        /// The maximum valid plane index.
107        max: usize,
108    },
109
110    /// Pixel format conversion failed.
111    ///
112    /// This error occurs when attempting to convert between two pixel
113    /// formats that is not supported or fails.
114    #[error("Format conversion failed: {from:?} -> {to:?}")]
115    ConversionFailed {
116        /// The source pixel format.
117        from: PixelFormat,
118        /// The target pixel format.
119        to: PixelFormat,
120    },
121
122    /// Audio sample format conversion failed.
123    ///
124    /// This error occurs when attempting to convert between two audio
125    /// sample formats that is not supported or fails.
126    #[error("Audio conversion failed: {from:?} -> {to:?}")]
127    AudioConversionFailed {
128        /// The source sample format.
129        from: SampleFormat,
130        /// The target sample format.
131        to: SampleFormat,
132    },
133
134    /// Invalid or corrupted frame data.
135    ///
136    /// This error occurs when frame data is invalid, corrupted, or
137    /// doesn't match the expected format parameters.
138    #[error("Invalid frame data: {0}")]
139    InvalidFrameData(String),
140}
141
142impl FormatError {
143    /// Creates an `InvalidPixelFormat` error from a format string.
144    ///
145    /// # Examples
146    ///
147    /// ```
148    /// use ff_format::FormatError;
149    ///
150    /// let error = FormatError::invalid_pixel_format("unknown_format");
151    /// assert!(error.to_string().contains("unknown_format"));
152    /// ```
153    #[inline]
154    #[must_use]
155    pub fn invalid_pixel_format(format: impl Into<String>) -> Self {
156        Self::InvalidPixelFormat {
157            format: format.into(),
158        }
159    }
160
161    /// Creates an `InvalidSampleFormat` error from a format string.
162    ///
163    /// # Examples
164    ///
165    /// ```
166    /// use ff_format::FormatError;
167    ///
168    /// let error = FormatError::invalid_sample_format("unknown_format");
169    /// assert!(error.to_string().contains("unknown_format"));
170    /// ```
171    #[inline]
172    #[must_use]
173    pub fn invalid_sample_format(format: impl Into<String>) -> Self {
174        Self::InvalidSampleFormat {
175            format: format.into(),
176        }
177    }
178
179    /// Creates an `InvalidFrameData` error with a description.
180    ///
181    /// # Examples
182    ///
183    /// ```
184    /// use ff_format::FormatError;
185    ///
186    /// let error = FormatError::invalid_frame_data("buffer size mismatch");
187    /// assert!(error.to_string().contains("buffer size"));
188    /// ```
189    #[inline]
190    #[must_use]
191    pub fn invalid_frame_data(reason: impl Into<String>) -> Self {
192        Self::InvalidFrameData(reason.into())
193    }
194
195    /// Creates a `PlaneIndexOutOfBounds` error.
196    ///
197    /// # Examples
198    ///
199    /// ```
200    /// use ff_format::FormatError;
201    ///
202    /// let error = FormatError::plane_out_of_bounds(5, 3);
203    /// assert!(error.to_string().contains("5"));
204    /// assert!(error.to_string().contains("3"));
205    /// ```
206    #[inline]
207    #[must_use]
208    pub fn plane_out_of_bounds(index: usize, max: usize) -> Self {
209        Self::PlaneIndexOutOfBounds { index, max }
210    }
211
212    /// Creates a `ConversionFailed` error for pixel format conversion.
213    ///
214    /// # Examples
215    ///
216    /// ```
217    /// use ff_format::{FormatError, PixelFormat};
218    ///
219    /// let error = FormatError::conversion_failed(PixelFormat::Yuv420p, PixelFormat::Rgba);
220    /// assert!(error.to_string().contains("Yuv420p"));
221    /// assert!(error.to_string().contains("Rgba"));
222    /// ```
223    #[inline]
224    #[must_use]
225    pub fn conversion_failed(from: PixelFormat, to: PixelFormat) -> Self {
226        Self::ConversionFailed { from, to }
227    }
228
229    /// Creates an `AudioConversionFailed` error for sample format conversion.
230    ///
231    /// # Examples
232    ///
233    /// ```
234    /// use ff_format::{FormatError, SampleFormat};
235    ///
236    /// let error = FormatError::audio_conversion_failed(SampleFormat::I16, SampleFormat::F32);
237    /// assert!(error.to_string().contains("I16"));
238    /// assert!(error.to_string().contains("F32"));
239    /// ```
240    #[inline]
241    #[must_use]
242    pub fn audio_conversion_failed(from: SampleFormat, to: SampleFormat) -> Self {
243        Self::AudioConversionFailed { from, to }
244    }
245
246    /// Creates an `InvalidTimestamp` error.
247    ///
248    /// # Examples
249    ///
250    /// ```
251    /// use ff_format::{FormatError, Rational};
252    ///
253    /// let error = FormatError::invalid_timestamp(-1, Rational::new(1, 90000));
254    /// assert!(error.to_string().contains("-1"));
255    /// ```
256    #[inline]
257    #[must_use]
258    pub fn invalid_timestamp(pts: i64, time_base: Rational) -> Self {
259        Self::InvalidTimestamp { pts, time_base }
260    }
261}
262
263/// Error type for frame operations.
264#[derive(Debug, Clone, PartialEq, Eq)]
265pub enum FrameError {
266    /// The number of planes does not match the number of strides.
267    MismatchedPlaneStride {
268        /// Number of planes provided.
269        planes: usize,
270        /// Number of strides provided.
271        strides: usize,
272    },
273    /// Cannot allocate a frame for an unknown pixel format.
274    UnsupportedPixelFormat(PixelFormat),
275    /// Cannot allocate an audio frame for an unknown sample format.
276    UnsupportedSampleFormat(SampleFormat),
277    /// The number of planes does not match the expected count for the format.
278    InvalidPlaneCount {
279        /// Expected number of planes.
280        expected: usize,
281        /// Actual number of planes provided.
282        actual: usize,
283    },
284}
285
286impl fmt::Display for FrameError {
287    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288        match self {
289            Self::MismatchedPlaneStride { planes, strides } => {
290                write!(
291                    f,
292                    "planes and strides length mismatch: {planes} planes, {strides} strides"
293                )
294            }
295            Self::UnsupportedPixelFormat(format) => {
296                write!(
297                    f,
298                    "cannot allocate frame for unsupported pixel format: {format:?}"
299                )
300            }
301            Self::UnsupportedSampleFormat(format) => {
302                write!(
303                    f,
304                    "cannot allocate frame for unsupported sample format: {format:?}"
305                )
306            }
307            Self::InvalidPlaneCount { expected, actual } => {
308                write!(f, "invalid plane count: expected {expected}, got {actual}")
309            }
310        }
311    }
312}
313
314impl std::error::Error for FrameError {}
315
316#[cfg(test)]
317#[allow(clippy::unwrap_used)]
318mod tests {
319    use super::*;
320
321    // === FormatError Tests ===
322
323    #[test]
324    fn test_format_error_invalid_pixel_format() {
325        let err = FormatError::InvalidPixelFormat {
326            format: "unknown_xyz".to_string(),
327        };
328        let msg = format!("{err}");
329        assert!(msg.contains("Invalid pixel format"));
330        assert!(msg.contains("unknown_xyz"));
331
332        // Test helper function
333        let err = FormatError::invalid_pixel_format("bad_format");
334        let msg = format!("{err}");
335        assert!(msg.contains("bad_format"));
336    }
337
338    #[test]
339    fn test_format_error_invalid_sample_format() {
340        let err = FormatError::InvalidSampleFormat {
341            format: "unknown_audio".to_string(),
342        };
343        let msg = format!("{err}");
344        assert!(msg.contains("Invalid sample format"));
345        assert!(msg.contains("unknown_audio"));
346
347        // Test helper function
348        let err = FormatError::invalid_sample_format("bad_audio");
349        let msg = format!("{err}");
350        assert!(msg.contains("bad_audio"));
351    }
352
353    #[test]
354    fn test_format_error_invalid_timestamp() {
355        let time_base = Rational::new(1, 90000);
356        let err = FormatError::InvalidTimestamp {
357            pts: -100,
358            time_base,
359        };
360        let msg = format!("{err}");
361        assert!(msg.contains("Invalid timestamp"));
362        assert!(msg.contains("pts=-100"));
363        assert!(msg.contains("time_base"));
364
365        // Test helper function
366        let err = FormatError::invalid_timestamp(-50, Rational::new(1, 1000));
367        let msg = format!("{err}");
368        assert!(msg.contains("-50"));
369    }
370
371    #[test]
372    fn test_format_error_plane_out_of_bounds() {
373        let err = FormatError::PlaneIndexOutOfBounds { index: 5, max: 3 };
374        let msg = format!("{err}");
375        assert!(msg.contains("Plane index 5"));
376        assert!(msg.contains("out of bounds"));
377        assert!(msg.contains("max: 3"));
378
379        // Test helper function
380        let err = FormatError::plane_out_of_bounds(10, 2);
381        let msg = format!("{err}");
382        assert!(msg.contains("10"));
383        assert!(msg.contains("2"));
384    }
385
386    #[test]
387    fn test_format_error_conversion_failed() {
388        let err = FormatError::ConversionFailed {
389            from: PixelFormat::Yuv420p,
390            to: PixelFormat::Rgba,
391        };
392        let msg = format!("{err}");
393        assert!(msg.contains("Format conversion failed"));
394        assert!(msg.contains("Yuv420p"));
395        assert!(msg.contains("Rgba"));
396
397        // Test helper function
398        let err = FormatError::conversion_failed(PixelFormat::Nv12, PixelFormat::Bgra);
399        let msg = format!("{err}");
400        assert!(msg.contains("Nv12"));
401        assert!(msg.contains("Bgra"));
402    }
403
404    #[test]
405    fn test_format_error_audio_conversion_failed() {
406        let err = FormatError::AudioConversionFailed {
407            from: SampleFormat::I16,
408            to: SampleFormat::F32,
409        };
410        let msg = format!("{err}");
411        assert!(msg.contains("Audio conversion failed"));
412        assert!(msg.contains("I16"));
413        assert!(msg.contains("F32"));
414
415        // Test helper function
416        let err = FormatError::audio_conversion_failed(SampleFormat::U8, SampleFormat::F64);
417        let msg = format!("{err}");
418        assert!(msg.contains("U8"));
419        assert!(msg.contains("F64"));
420    }
421
422    #[test]
423    fn test_format_error_invalid_frame_data() {
424        let err = FormatError::InvalidFrameData("buffer too small".to_string());
425        let msg = format!("{err}");
426        assert!(msg.contains("Invalid frame data"));
427        assert!(msg.contains("buffer too small"));
428
429        // Test helper function
430        let err = FormatError::invalid_frame_data("corrupted header");
431        let msg = format!("{err}");
432        assert!(msg.contains("corrupted header"));
433    }
434
435    #[test]
436    fn test_format_error_is_std_error() {
437        let err: Box<dyn std::error::Error> = Box::new(FormatError::InvalidPixelFormat {
438            format: "test".to_string(),
439        });
440        // Verify it implements std::error::Error
441        assert!(err.to_string().contains("test"));
442    }
443
444    #[test]
445    fn test_format_error_equality() {
446        let err1 = FormatError::InvalidPixelFormat {
447            format: "test".to_string(),
448        };
449        let err2 = FormatError::InvalidPixelFormat {
450            format: "test".to_string(),
451        };
452        let err3 = FormatError::InvalidPixelFormat {
453            format: "other".to_string(),
454        };
455
456        assert_eq!(err1, err2);
457        assert_ne!(err1, err3);
458    }
459
460    #[test]
461    fn test_format_error_clone() {
462        let err1 = FormatError::ConversionFailed {
463            from: PixelFormat::Yuv420p,
464            to: PixelFormat::Rgba,
465        };
466        let err2 = err1.clone();
467        assert_eq!(err1, err2);
468    }
469
470    #[test]
471    fn test_format_error_debug() {
472        let err = FormatError::PlaneIndexOutOfBounds { index: 3, max: 2 };
473        let debug_str = format!("{err:?}");
474        assert!(debug_str.contains("PlaneIndexOutOfBounds"));
475        assert!(debug_str.contains("index"));
476        assert!(debug_str.contains("max"));
477    }
478
479    // === FrameError Tests ===
480
481    #[test]
482    fn test_frame_error_display() {
483        let err = FrameError::MismatchedPlaneStride {
484            planes: 1,
485            strides: 2,
486        };
487        let msg = format!("{err}");
488        assert!(msg.contains("planes"));
489        assert!(msg.contains("strides"));
490        assert!(msg.contains("mismatch"));
491
492        let err = FrameError::UnsupportedPixelFormat(PixelFormat::Other(42));
493        let msg = format!("{err}");
494        assert!(msg.contains("unsupported"));
495        assert!(msg.contains("pixel format"));
496
497        let err = FrameError::UnsupportedSampleFormat(SampleFormat::Other(42));
498        let msg = format!("{err}");
499        assert!(msg.contains("unsupported"));
500        assert!(msg.contains("sample format"));
501
502        let err = FrameError::InvalidPlaneCount {
503            expected: 2,
504            actual: 1,
505        };
506        let msg = format!("{err}");
507        assert!(msg.contains("expected 2"));
508        assert!(msg.contains("got 1"));
509    }
510}