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