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/// Error type for subtitle parsing operations.
317#[derive(Debug, Error)]
318pub enum SubtitleError {
319    /// I/O error reading a subtitle file.
320    #[error("io error: {0}")]
321    Io(#[from] std::io::Error),
322
323    /// File extension is not a recognized subtitle format.
324    #[error("unsupported subtitle format: {extension}")]
325    UnsupportedFormat {
326        /// The unrecognized file extension.
327        extension: String,
328    },
329
330    /// A structural parse error prevents processing the file.
331    #[error("parse error at line {line}: {reason}")]
332    ParseError {
333        /// 1-based line number where the error was detected.
334        line: usize,
335        /// Human-readable description of the problem.
336        reason: String,
337    },
338
339    /// The input contained no valid subtitle events.
340    #[error("no valid subtitle events found")]
341    NoEvents,
342}
343
344#[cfg(test)]
345#[allow(clippy::unwrap_used)]
346mod tests {
347    use super::*;
348
349    // === FormatError Tests ===
350
351    #[test]
352    fn test_format_error_invalid_pixel_format() {
353        let err = FormatError::InvalidPixelFormat {
354            format: "unknown_xyz".to_string(),
355        };
356        let msg = format!("{err}");
357        assert!(msg.contains("Invalid pixel format"));
358        assert!(msg.contains("unknown_xyz"));
359
360        // Test helper function
361        let err = FormatError::invalid_pixel_format("bad_format");
362        let msg = format!("{err}");
363        assert!(msg.contains("bad_format"));
364    }
365
366    #[test]
367    fn test_format_error_invalid_sample_format() {
368        let err = FormatError::InvalidSampleFormat {
369            format: "unknown_audio".to_string(),
370        };
371        let msg = format!("{err}");
372        assert!(msg.contains("Invalid sample format"));
373        assert!(msg.contains("unknown_audio"));
374
375        // Test helper function
376        let err = FormatError::invalid_sample_format("bad_audio");
377        let msg = format!("{err}");
378        assert!(msg.contains("bad_audio"));
379    }
380
381    #[test]
382    fn test_format_error_invalid_timestamp() {
383        let time_base = Rational::new(1, 90000);
384        let err = FormatError::InvalidTimestamp {
385            pts: -100,
386            time_base,
387        };
388        let msg = format!("{err}");
389        assert!(msg.contains("Invalid timestamp"));
390        assert!(msg.contains("pts=-100"));
391        assert!(msg.contains("time_base"));
392
393        // Test helper function
394        let err = FormatError::invalid_timestamp(-50, Rational::new(1, 1000));
395        let msg = format!("{err}");
396        assert!(msg.contains("-50"));
397    }
398
399    #[test]
400    fn test_format_error_plane_out_of_bounds() {
401        let err = FormatError::PlaneIndexOutOfBounds { index: 5, max: 3 };
402        let msg = format!("{err}");
403        assert!(msg.contains("Plane index 5"));
404        assert!(msg.contains("out of bounds"));
405        assert!(msg.contains("max: 3"));
406
407        // Test helper function
408        let err = FormatError::plane_out_of_bounds(10, 2);
409        let msg = format!("{err}");
410        assert!(msg.contains("10"));
411        assert!(msg.contains("2"));
412    }
413
414    #[test]
415    fn test_format_error_conversion_failed() {
416        let err = FormatError::ConversionFailed {
417            from: PixelFormat::Yuv420p,
418            to: PixelFormat::Rgba,
419        };
420        let msg = format!("{err}");
421        assert!(msg.contains("Format conversion failed"));
422        assert!(msg.contains("Yuv420p"));
423        assert!(msg.contains("Rgba"));
424
425        // Test helper function
426        let err = FormatError::conversion_failed(PixelFormat::Nv12, PixelFormat::Bgra);
427        let msg = format!("{err}");
428        assert!(msg.contains("Nv12"));
429        assert!(msg.contains("Bgra"));
430    }
431
432    #[test]
433    fn test_format_error_audio_conversion_failed() {
434        let err = FormatError::AudioConversionFailed {
435            from: SampleFormat::I16,
436            to: SampleFormat::F32,
437        };
438        let msg = format!("{err}");
439        assert!(msg.contains("Audio conversion failed"));
440        assert!(msg.contains("I16"));
441        assert!(msg.contains("F32"));
442
443        // Test helper function
444        let err = FormatError::audio_conversion_failed(SampleFormat::U8, SampleFormat::F64);
445        let msg = format!("{err}");
446        assert!(msg.contains("U8"));
447        assert!(msg.contains("F64"));
448    }
449
450    #[test]
451    fn test_format_error_invalid_frame_data() {
452        let err = FormatError::InvalidFrameData("buffer too small".to_string());
453        let msg = format!("{err}");
454        assert!(msg.contains("Invalid frame data"));
455        assert!(msg.contains("buffer too small"));
456
457        // Test helper function
458        let err = FormatError::invalid_frame_data("corrupted header");
459        let msg = format!("{err}");
460        assert!(msg.contains("corrupted header"));
461    }
462
463    #[test]
464    fn test_format_error_is_std_error() {
465        let err: Box<dyn std::error::Error> = Box::new(FormatError::InvalidPixelFormat {
466            format: "test".to_string(),
467        });
468        // Verify it implements std::error::Error
469        assert!(err.to_string().contains("test"));
470    }
471
472    #[test]
473    fn test_format_error_equality() {
474        let err1 = FormatError::InvalidPixelFormat {
475            format: "test".to_string(),
476        };
477        let err2 = FormatError::InvalidPixelFormat {
478            format: "test".to_string(),
479        };
480        let err3 = FormatError::InvalidPixelFormat {
481            format: "other".to_string(),
482        };
483
484        assert_eq!(err1, err2);
485        assert_ne!(err1, err3);
486    }
487
488    #[test]
489    fn test_format_error_clone() {
490        let err1 = FormatError::ConversionFailed {
491            from: PixelFormat::Yuv420p,
492            to: PixelFormat::Rgba,
493        };
494        let err2 = err1.clone();
495        assert_eq!(err1, err2);
496    }
497
498    #[test]
499    fn test_format_error_debug() {
500        let err = FormatError::PlaneIndexOutOfBounds { index: 3, max: 2 };
501        let debug_str = format!("{err:?}");
502        assert!(debug_str.contains("PlaneIndexOutOfBounds"));
503        assert!(debug_str.contains("index"));
504        assert!(debug_str.contains("max"));
505    }
506
507    // === FrameError Tests ===
508
509    #[test]
510    fn test_frame_error_display() {
511        let err = FrameError::MismatchedPlaneStride {
512            planes: 1,
513            strides: 2,
514        };
515        let msg = format!("{err}");
516        assert!(msg.contains("planes"));
517        assert!(msg.contains("strides"));
518        assert!(msg.contains("mismatch"));
519
520        let err = FrameError::UnsupportedPixelFormat(PixelFormat::Other(42));
521        let msg = format!("{err}");
522        assert!(msg.contains("unsupported"));
523        assert!(msg.contains("pixel format"));
524
525        let err = FrameError::UnsupportedSampleFormat(SampleFormat::Other(42));
526        let msg = format!("{err}");
527        assert!(msg.contains("unsupported"));
528        assert!(msg.contains("sample format"));
529
530        let err = FrameError::InvalidPlaneCount {
531            expected: 2,
532            actual: 1,
533        };
534        let msg = format!("{err}");
535        assert!(msg.contains("expected 2"));
536        assert!(msg.contains("got 1"));
537    }
538}