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}