1use std::path::Path;
4use thiserror::Error;
5
6#[derive(Debug, Clone, Error)]
8pub enum ValidationError {
9 #[error("Input file does not exist: {0}")]
11 InputNotFound(String),
12
13 #[error("Input file is not readable: {0}")]
15 InputNotReadable(String),
16
17 #[error("Input file has no video stream")]
19 NoVideoStream,
20
21 #[error("Input file has no audio stream")]
23 NoAudioStream,
24
25 #[error("Invalid input format: {0}")]
27 InvalidInputFormat(String),
28
29 #[error("Invalid output path: {0}")]
31 InvalidOutputPath(String),
32
33 #[error("Output directory is not writable: {0}")]
35 OutputNotWritable(String),
36
37 #[error("Output file already exists: {0}")]
39 OutputExists(String),
40
41 #[error("Invalid codec: {0}")]
43 InvalidCodec(String),
44
45 #[error("Invalid resolution: {0}")]
47 InvalidResolution(String),
48
49 #[error("Invalid bitrate: {0}")]
51 InvalidBitrate(String),
52
53 #[error("Invalid frame rate: {0}")]
55 InvalidFrameRate(String),
56
57 #[error("Unsupported operation: {0}")]
59 Unsupported(String),
60}
61
62pub struct InputValidator;
64
65impl InputValidator {
66 pub fn validate_path(path: &str) -> Result<(), ValidationError> {
72 let path_obj = Path::new(path);
73
74 if !path_obj.exists() {
75 return Err(ValidationError::InputNotFound(path.to_string()));
76 }
77
78 if !path_obj.is_file() {
79 return Err(ValidationError::InvalidInputFormat(
80 "Path is not a file".to_string(),
81 ));
82 }
83
84 match std::fs::metadata(path_obj) {
86 Ok(metadata) => {
87 if metadata.len() == 0 {
88 return Err(ValidationError::InvalidInputFormat(
89 "File is empty".to_string(),
90 ));
91 }
92 }
93 Err(_) => {
94 return Err(ValidationError::InputNotReadable(path.to_string()));
95 }
96 }
97
98 Ok(())
99 }
100
101 pub fn validate_format(path: &str) -> Result<String, ValidationError> {
107 let path_obj = Path::new(path);
108 let extension = path_obj
109 .extension()
110 .and_then(|e| e.to_str())
111 .ok_or_else(|| ValidationError::InvalidInputFormat("No file extension".to_string()))?;
112
113 let ext_lower = extension.to_lowercase();
114
115 match ext_lower.as_str() {
117 "mp4" | "mkv" | "webm" | "avi" | "mov" | "flv" | "wmv" | "m4v" | "mpg" | "mpeg"
118 | "ts" | "mts" | "m2ts" | "ogv" | "3gp" => Ok(ext_lower),
119 _ => Err(ValidationError::InvalidInputFormat(format!(
120 "Unsupported format: {extension}"
121 ))),
122 }
123 }
124
125 pub fn validate_streams(
127 has_video: bool,
128 has_audio: bool,
129 require_video: bool,
130 require_audio: bool,
131 ) -> Result<(), ValidationError> {
132 if require_video && !has_video {
133 return Err(ValidationError::NoVideoStream);
134 }
135
136 if require_audio && !has_audio {
137 return Err(ValidationError::NoAudioStream);
138 }
139
140 Ok(())
141 }
142}
143
144pub struct OutputValidator;
146
147impl OutputValidator {
148 pub fn validate_path(path: &str, overwrite: bool) -> Result<(), ValidationError> {
154 let path_obj = Path::new(path);
155
156 if path_obj.exists() && !overwrite {
158 return Err(ValidationError::OutputExists(path.to_string()));
159 }
160
161 if let Some(parent) = path_obj.parent() {
163 if !parent.exists() {
164 return Err(ValidationError::InvalidOutputPath(
165 "Parent directory does not exist".to_string(),
166 ));
167 }
168
169 if let Err(_e) = std::fs::metadata(parent) {
171 return Err(ValidationError::OutputNotWritable(
172 parent.display().to_string(),
173 ));
174 }
175 }
176
177 Ok(())
178 }
179
180 pub fn validate_format(path: &str) -> Result<String, ValidationError> {
186 let path_obj = Path::new(path);
187 let extension = path_obj
188 .extension()
189 .and_then(|e| e.to_str())
190 .ok_or_else(|| ValidationError::InvalidOutputPath("No file extension".to_string()))?;
191
192 let ext_lower = extension.to_lowercase();
193
194 match ext_lower.as_str() {
195 "mp4" | "mkv" | "webm" | "avi" | "mov" | "m4v" | "ogv" => Ok(ext_lower),
196 _ => Err(ValidationError::InvalidOutputPath(format!(
197 "Unsupported output format: {extension}"
198 ))),
199 }
200 }
201
202 pub fn validate_codec(codec: &str) -> Result<(), ValidationError> {
208 let codec_lower = codec.to_lowercase();
209
210 match codec_lower.as_str() {
211 "vp8" | "vp9" | "av1" | "h264" | "h265" | "theora" | "opus" | "vorbis" | "aac"
212 | "mp3" | "flac" => Ok(()),
213 _ => Err(ValidationError::InvalidCodec(format!(
214 "Unsupported codec: {codec}"
215 ))),
216 }
217 }
218
219 pub fn validate_resolution(width: u32, height: u32) -> Result<(), ValidationError> {
225 if width == 0 || height == 0 {
226 return Err(ValidationError::InvalidResolution(
227 "Width and height must be greater than 0".to_string(),
228 ));
229 }
230
231 if width > 7680 || height > 4320 {
232 return Err(ValidationError::InvalidResolution(
233 "Resolution exceeds maximum (7680x4320)".to_string(),
234 ));
235 }
236
237 if width % 2 != 0 || height % 2 != 0 {
238 return Err(ValidationError::InvalidResolution(
239 "Width and height must be even numbers".to_string(),
240 ));
241 }
242
243 Ok(())
244 }
245
246 pub fn validate_bitrate(bitrate: u64, min: u64, max: u64) -> Result<(), ValidationError> {
252 if bitrate < min {
253 return Err(ValidationError::InvalidBitrate(format!(
254 "Bitrate too low (minimum: {min})"
255 )));
256 }
257
258 if bitrate > max {
259 return Err(ValidationError::InvalidBitrate(format!(
260 "Bitrate too high (maximum: {max})"
261 )));
262 }
263
264 Ok(())
265 }
266
267 pub fn validate_frame_rate(num: u32, den: u32) -> Result<(), ValidationError> {
273 if num == 0 || den == 0 {
274 return Err(ValidationError::InvalidFrameRate(
275 "Numerator and denominator must be greater than 0".to_string(),
276 ));
277 }
278
279 let fps = f64::from(num) / f64::from(den);
280
281 if fps < 1.0 {
282 return Err(ValidationError::InvalidFrameRate(
283 "Frame rate must be at least 1 fps".to_string(),
284 ));
285 }
286
287 if fps > 240.0 {
288 return Err(ValidationError::InvalidFrameRate(
289 "Frame rate exceeds maximum (240 fps)".to_string(),
290 ));
291 }
292
293 Ok(())
294 }
295}
296
297pub fn validate_codec_container_compatibility(
299 codec: &str,
300 container: &str,
301) -> Result<(), ValidationError> {
302 let codec_lower = codec.to_lowercase();
303 let container_lower = container.to_lowercase();
304
305 let compatible = match container_lower.as_str() {
306 "mp4" | "m4v" => matches!(codec_lower.as_str(), "h264" | "h265" | "av1" | "aac"),
307 "webm" => matches!(
308 codec_lower.as_str(),
309 "vp8" | "vp9" | "av1" | "opus" | "vorbis"
310 ),
311 "mkv" => true, "ogv" => matches!(codec_lower.as_str(), "theora" | "vorbis" | "opus"),
313 _ => false,
314 };
315
316 if compatible {
317 Ok(())
318 } else {
319 Err(ValidationError::Unsupported(format!(
320 "Codec '{codec}' is not compatible with container '{container}'"
321 )))
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn test_validate_format_valid() {
331 assert_eq!(
332 InputValidator::validate_format("test.mp4").expect("should succeed in test"),
333 "mp4"
334 );
335 assert_eq!(
336 InputValidator::validate_format("test.MKV").expect("should succeed in test"),
337 "mkv"
338 );
339 assert_eq!(
340 InputValidator::validate_format("test.webm").expect("should succeed in test"),
341 "webm"
342 );
343 }
344
345 #[test]
346 fn test_validate_format_invalid() {
347 assert!(InputValidator::validate_format("test.xyz").is_err());
348 assert!(InputValidator::validate_format("test").is_err());
349 }
350
351 #[test]
352 fn test_validate_codec_valid() {
353 assert!(OutputValidator::validate_codec("vp9").is_ok());
354 assert!(OutputValidator::validate_codec("h264").is_ok());
355 assert!(OutputValidator::validate_codec("opus").is_ok());
356 }
357
358 #[test]
359 fn test_validate_codec_invalid() {
360 assert!(OutputValidator::validate_codec("unknown").is_err());
361 assert!(OutputValidator::validate_codec("divx").is_err());
362 }
363
364 #[test]
365 fn test_validate_resolution_valid() {
366 assert!(OutputValidator::validate_resolution(1920, 1080).is_ok());
367 assert!(OutputValidator::validate_resolution(1280, 720).is_ok());
368 assert!(OutputValidator::validate_resolution(3840, 2160).is_ok());
369 }
370
371 #[test]
372 fn test_validate_resolution_invalid() {
373 assert!(OutputValidator::validate_resolution(0, 1080).is_err());
375 assert!(OutputValidator::validate_resolution(1920, 0).is_err());
376
377 assert!(OutputValidator::validate_resolution(1921, 1080).is_err());
379 assert!(OutputValidator::validate_resolution(1920, 1081).is_err());
380
381 assert!(OutputValidator::validate_resolution(10000, 10000).is_err());
383 }
384
385 #[test]
386 fn test_validate_bitrate_valid() {
387 assert!(OutputValidator::validate_bitrate(5_000_000, 100_000, 50_000_000).is_ok());
388 assert!(OutputValidator::validate_bitrate(100_000, 100_000, 50_000_000).is_ok());
389 assert!(OutputValidator::validate_bitrate(50_000_000, 100_000, 50_000_000).is_ok());
390 }
391
392 #[test]
393 fn test_validate_bitrate_invalid() {
394 assert!(OutputValidator::validate_bitrate(50_000, 100_000, 50_000_000).is_err());
395 assert!(OutputValidator::validate_bitrate(100_000_000, 100_000, 50_000_000).is_err());
396 }
397
398 #[test]
399 fn test_validate_frame_rate_valid() {
400 assert!(OutputValidator::validate_frame_rate(30, 1).is_ok());
401 assert!(OutputValidator::validate_frame_rate(60, 1).is_ok());
402 assert!(OutputValidator::validate_frame_rate(24000, 1001).is_ok());
403 }
404
405 #[test]
406 fn test_validate_frame_rate_invalid() {
407 assert!(OutputValidator::validate_frame_rate(0, 1).is_err());
408 assert!(OutputValidator::validate_frame_rate(30, 0).is_err());
409 assert!(OutputValidator::validate_frame_rate(1, 10).is_err()); assert!(OutputValidator::validate_frame_rate(300, 1).is_err()); }
412
413 #[test]
414 fn test_codec_container_compatibility() {
415 assert!(validate_codec_container_compatibility("h264", "mp4").is_ok());
417 assert!(validate_codec_container_compatibility("vp9", "webm").is_ok());
418 assert!(validate_codec_container_compatibility("opus", "webm").is_ok());
419 assert!(validate_codec_container_compatibility("theora", "ogv").is_ok());
420 assert!(validate_codec_container_compatibility("h264", "mkv").is_ok());
421 assert!(validate_codec_container_compatibility("vp9", "mkv").is_ok());
422
423 assert!(validate_codec_container_compatibility("vp9", "mp4").is_err());
425 assert!(validate_codec_container_compatibility("h264", "webm").is_err());
426 assert!(validate_codec_container_compatibility("aac", "webm").is_err());
427 }
428
429 #[test]
430 fn test_validate_streams() {
431 assert!(InputValidator::validate_streams(true, true, true, true).is_ok());
432 assert!(InputValidator::validate_streams(true, false, true, false).is_ok());
433 assert!(InputValidator::validate_streams(false, true, false, true).is_ok());
434 assert!(InputValidator::validate_streams(true, true, false, false).is_ok());
435
436 assert!(InputValidator::validate_streams(false, true, true, true).is_err());
437 assert!(InputValidator::validate_streams(true, false, true, true).is_err());
438 }
439
440 #[test]
441 fn test_validate_path_nonexistent() {
442 let result = InputValidator::validate_path("/nonexistent/file.mp4");
443 assert!(result.is_err());
444 assert!(matches!(
445 result.unwrap_err(),
446 ValidationError::InputNotFound(_)
447 ));
448 }
449
450 #[test]
451 fn test_output_path_validation() {
452 let result = OutputValidator::validate_path("/tmp/test_output.mp4", false);
454 assert!(result.is_ok());
455 }
456
457 #[test]
458 fn test_output_format_validation() {
459 assert_eq!(
460 OutputValidator::validate_format("output.mp4").expect("should succeed in test"),
461 "mp4"
462 );
463 assert_eq!(
464 OutputValidator::validate_format("output.webm").expect("should succeed in test"),
465 "webm"
466 );
467 assert!(OutputValidator::validate_format("output.xyz").is_err());
468 }
469}