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}