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}