1use crate::error::TiffError;
19
20use super::parser::{ByteOrder, Ifd};
21use super::pyramid::{PyramidLevel, TiffPyramid};
22use super::tags::{Compression, TiffTag};
23
24#[derive(Debug, Clone)]
30pub struct ValidationResult {
31 pub is_valid: bool,
33
34 pub errors: Vec<ValidationError>,
36
37 pub warnings: Vec<String>,
39}
40
41impl ValidationResult {
42 pub fn ok() -> Self {
44 ValidationResult {
45 is_valid: true,
46 errors: Vec::new(),
47 warnings: Vec::new(),
48 }
49 }
50
51 pub fn error(error: ValidationError) -> Self {
53 ValidationResult {
54 is_valid: false,
55 errors: vec![error],
56 warnings: Vec::new(),
57 }
58 }
59
60 pub fn add_error(&mut self, error: ValidationError) {
62 self.is_valid = false;
63 self.errors.push(error);
64 }
65
66 pub fn add_warning(&mut self, warning: String) {
68 self.warnings.push(warning);
69 }
70
71 pub fn into_result(self) -> Result<(), TiffError> {
75 if self.is_valid {
76 Ok(())
77 } else {
78 Err(self.errors.into_iter().next().unwrap().into())
79 }
80 }
81}
82
83#[derive(Debug, Clone)]
85pub enum ValidationError {
86 MissingTag {
88 ifd_index: usize,
90 tag: &'static str,
92 },
93 StripOrganization {
95 ifd_index: usize,
97 },
98
99 UnsupportedCompression {
101 ifd_index: usize,
103 compression: u16,
105 compression_name: String,
107 },
108
109 MissingTileTags {
111 ifd_index: usize,
113 missing_tags: Vec<&'static str>,
115 },
116
117 NoPyramidLevels,
119
120 InvalidTileDimensions {
122 ifd_index: usize,
124 tile_width: u32,
126 tile_height: u32,
128 message: String,
130 },
131}
132
133impl From<ValidationError> for TiffError {
134 fn from(error: ValidationError) -> Self {
135 match error {
136 ValidationError::MissingTag { tag, .. } => TiffError::MissingTag(tag),
137 ValidationError::StripOrganization { .. } => TiffError::StripOrganization,
138 ValidationError::UnsupportedCompression {
139 compression_name, ..
140 } => TiffError::UnsupportedCompression(compression_name),
141 ValidationError::MissingTileTags { missing_tags, .. } => {
142 TiffError::MissingTag(missing_tags.first().copied().unwrap_or("TileOffsets"))
143 }
144 ValidationError::NoPyramidLevels => {
145 TiffError::MissingTag("No valid pyramid levels found")
146 }
147 ValidationError::InvalidTileDimensions { message, .. } => TiffError::InvalidTagValue {
148 tag: "TileWidth/TileLength",
149 message,
150 },
151 }
152 }
153}
154
155pub fn validate_ifd(ifd: &Ifd, ifd_index: usize, byte_order: ByteOrder) -> ValidationResult {
168 let mut result = ValidationResult::ok();
169
170 if ifd.is_stripped() && !ifd.is_tiled() {
172 result.add_error(ValidationError::StripOrganization { ifd_index });
173 return result; }
175
176 if !ifd.is_tiled() {
178 return result;
179 }
180
181 if let Some(compression_value) = ifd.compression(byte_order) {
183 if let Some(compression) = Compression::from_u16(compression_value) {
184 if !compression.is_supported() {
185 result.add_error(ValidationError::UnsupportedCompression {
186 ifd_index,
187 compression: compression_value,
188 compression_name: compression.name().to_string(),
189 });
190 }
191 } else {
192 result.add_error(ValidationError::UnsupportedCompression {
194 ifd_index,
195 compression: compression_value,
196 compression_name: format!("Unknown ({})", compression_value),
197 });
198 }
199 } else {
200 result.add_error(ValidationError::MissingTag {
201 ifd_index,
202 tag: "Compression",
203 });
204 }
205
206 let mut missing_tags = Vec::new();
208
209 if ifd.get_entry_by_tag(TiffTag::TileWidth).is_none() {
210 missing_tags.push("TileWidth");
211 }
212 if ifd.get_entry_by_tag(TiffTag::TileLength).is_none() {
213 missing_tags.push("TileLength");
214 }
215 if ifd.get_entry_by_tag(TiffTag::TileOffsets).is_none() {
216 missing_tags.push("TileOffsets");
217 }
218 if ifd.get_entry_by_tag(TiffTag::TileByteCounts).is_none() {
219 missing_tags.push("TileByteCounts");
220 }
221
222 if !missing_tags.is_empty() {
223 result.add_error(ValidationError::MissingTileTags {
224 ifd_index,
225 missing_tags,
226 });
227 }
228
229 if let (Some(tile_width), Some(tile_height)) =
231 (ifd.tile_width(byte_order), ifd.tile_height(byte_order))
232 {
233 if tile_width == 0 || tile_height == 0 {
235 result.add_error(ValidationError::InvalidTileDimensions {
236 ifd_index,
237 tile_width,
238 tile_height,
239 message: "Tile dimensions cannot be zero".to_string(),
240 });
241 } else if tile_width > 4096 || tile_height > 4096 {
242 result.add_warning(format!(
244 "IFD {}: Large tile dimensions ({}x{}) may cause memory issues",
245 ifd_index, tile_width, tile_height
246 ));
247 }
248
249 if tile_width % 16 != 0 || tile_height % 16 != 0 {
251 result.add_warning(format!(
252 "IFD {}: Tile dimensions ({}x{}) are not multiples of 16",
253 ifd_index, tile_width, tile_height
254 ));
255 }
256 }
257
258 result
259}
260
261pub fn validate_level(level: &PyramidLevel, byte_order: ByteOrder) -> ValidationResult {
263 let mut result = ValidationResult::ok();
264
265 if let Some(compression_value) = level.ifd.compression(byte_order) {
267 if let Some(compression) = Compression::from_u16(compression_value) {
268 if !compression.is_supported() {
269 result.add_error(ValidationError::UnsupportedCompression {
270 ifd_index: level.ifd_index,
271 compression: compression_value,
272 compression_name: compression.name().to_string(),
273 });
274 }
275 } else {
276 result.add_error(ValidationError::UnsupportedCompression {
277 ifd_index: level.ifd_index,
278 compression: compression_value,
279 compression_name: format!("Unknown ({})", compression_value),
280 });
281 }
282 } else {
283 result.add_error(ValidationError::MissingTag {
284 ifd_index: level.ifd_index,
285 tag: "Compression",
286 });
287 }
288
289 if !level.has_tile_data() {
291 let mut missing = Vec::new();
292 if level.tile_offsets_entry.is_none() {
293 missing.push("TileOffsets");
294 }
295 if level.tile_byte_counts_entry.is_none() {
296 missing.push("TileByteCounts");
297 }
298 result.add_error(ValidationError::MissingTileTags {
299 ifd_index: level.ifd_index,
300 missing_tags: missing,
301 });
302 }
303
304 if level.tile_width == 0 || level.tile_height == 0 {
306 result.add_error(ValidationError::InvalidTileDimensions {
307 ifd_index: level.ifd_index,
308 tile_width: level.tile_width,
309 tile_height: level.tile_height,
310 message: "Tile dimensions cannot be zero".to_string(),
311 });
312 }
313
314 if level.jpeg_tables_entry.is_none() {
316 if let Some(compression_value) = level.ifd.compression(byte_order) {
317 if compression_value == 7 {
318 result.add_warning(format!(
320 "Level {}: No JPEGTables tag found (tiles may have inline tables)",
321 level.level_index
322 ));
323 }
324 }
325 }
326
327 result
328}
329
330pub fn validate_pyramid(pyramid: &TiffPyramid) -> ValidationResult {
337 let mut result = ValidationResult::ok();
338 let byte_order = pyramid.header.byte_order;
339
340 if pyramid.levels.is_empty() {
342 result.add_error(ValidationError::NoPyramidLevels);
343 return result;
344 }
345
346 for level in &pyramid.levels {
348 let level_result = validate_level(level, byte_order);
349 for error in level_result.errors {
350 result.add_error(error);
351 }
352 for warning in level_result.warnings {
353 result.add_warning(warning);
354 }
355 }
356
357 result
358}
359
360pub fn check_compression(ifd: &Ifd, byte_order: ByteOrder) -> Result<(), TiffError> {
368 if let Some(compression_value) = ifd.compression(byte_order) {
369 if let Some(compression) = Compression::from_u16(compression_value) {
370 if compression.is_supported() {
371 return Ok(());
372 }
373 return Err(TiffError::UnsupportedCompression(
374 compression.name().to_string(),
375 ));
376 }
377 return Err(TiffError::UnsupportedCompression(format!(
378 "Unknown ({})",
379 compression_value
380 )));
381 }
382 Err(TiffError::MissingTag("Compression"))
384}
385
386pub fn check_tiled(ifd: &Ifd) -> Result<(), TiffError> {
390 if ifd.is_stripped() && !ifd.is_tiled() {
391 return Err(TiffError::StripOrganization);
392 }
393 Ok(())
394}
395
396pub fn check_tile_tags(ifd: &Ifd) -> Result<(), TiffError> {
400 if ifd.get_entry_by_tag(TiffTag::TileWidth).is_none() {
401 return Err(TiffError::MissingTag("TileWidth"));
402 }
403 if ifd.get_entry_by_tag(TiffTag::TileLength).is_none() {
404 return Err(TiffError::MissingTag("TileLength"));
405 }
406 if ifd.get_entry_by_tag(TiffTag::TileOffsets).is_none() {
407 return Err(TiffError::MissingTag("TileOffsets"));
408 }
409 if ifd.get_entry_by_tag(TiffTag::TileByteCounts).is_none() {
410 return Err(TiffError::MissingTag("TileByteCounts"));
411 }
412 Ok(())
413}
414
415pub fn validate_ifd_strict(
420 ifd: &Ifd,
421 ifd_index: usize,
422 byte_order: ByteOrder,
423) -> Result<(), TiffError> {
424 let result = validate_ifd(ifd, ifd_index, byte_order);
425 result.into_result()
426}
427
428#[cfg(test)]
433mod tests {
434 use super::*;
435 use crate::format::tiff::parser::TiffHeader;
436 use crate::format::tiff::tags::FieldType;
437 use crate::format::tiff::IfdEntry;
438 use std::collections::HashMap;
439
440 fn make_header() -> TiffHeader {
441 TiffHeader {
442 byte_order: ByteOrder::LittleEndian,
443 is_bigtiff: false,
444 first_ifd_offset: 8,
445 }
446 }
447
448 fn make_entry(tag: TiffTag, value: u32) -> IfdEntry {
449 IfdEntry {
450 tag_id: tag.as_u16(),
451 field_type: Some(FieldType::Long),
452 field_type_raw: 4,
453 count: 1,
454 value_offset_bytes: value.to_le_bytes().to_vec(),
455 is_inline: true,
456 }
457 }
458
459 fn make_tiled_ifd() -> Ifd {
460 let entries = vec![
462 make_entry(TiffTag::ImageWidth, 10000),
463 make_entry(TiffTag::ImageLength, 8000),
464 make_entry(TiffTag::TileWidth, 256),
465 make_entry(TiffTag::TileLength, 256),
466 IfdEntry {
467 tag_id: TiffTag::TileOffsets.as_u16(),
468 field_type: Some(FieldType::Long),
469 field_type_raw: 4,
470 count: 100,
471 value_offset_bytes: vec![0, 0, 0, 0],
472 is_inline: false,
473 },
474 IfdEntry {
475 tag_id: TiffTag::TileByteCounts.as_u16(),
476 field_type: Some(FieldType::Long),
477 field_type_raw: 4,
478 count: 100,
479 value_offset_bytes: vec![0, 0, 0, 0],
480 is_inline: false,
481 },
482 IfdEntry {
483 tag_id: TiffTag::Compression.as_u16(),
484 field_type: Some(FieldType::Short),
485 field_type_raw: 3,
486 count: 1,
487 value_offset_bytes: vec![7, 0, 0, 0], is_inline: true,
489 },
490 ];
491
492 let mut entries_by_tag = HashMap::new();
493 for (i, entry) in entries.iter().enumerate() {
494 entries_by_tag.insert(entry.tag_id, i);
495 }
496
497 Ifd {
498 entries,
499 entries_by_tag,
500 next_ifd_offset: 0,
501 }
502 }
503
504 fn make_stripped_ifd() -> Ifd {
505 let entries = vec![
507 make_entry(TiffTag::ImageWidth, 1000),
508 make_entry(TiffTag::ImageLength, 800),
509 IfdEntry {
510 tag_id: TiffTag::StripOffsets.as_u16(),
511 field_type: Some(FieldType::Long),
512 field_type_raw: 4,
513 count: 10,
514 value_offset_bytes: vec![0, 0, 0, 0],
515 is_inline: false,
516 },
517 IfdEntry {
518 tag_id: TiffTag::StripByteCounts.as_u16(),
519 field_type: Some(FieldType::Long),
520 field_type_raw: 4,
521 count: 10,
522 value_offset_bytes: vec![0, 0, 0, 0],
523 is_inline: false,
524 },
525 ];
526
527 let mut entries_by_tag = HashMap::new();
528 for (i, entry) in entries.iter().enumerate() {
529 entries_by_tag.insert(entry.tag_id, i);
530 }
531
532 Ifd {
533 entries,
534 entries_by_tag,
535 next_ifd_offset: 0,
536 }
537 }
538
539 fn make_lzw_ifd() -> Ifd {
540 let entries = vec![
542 make_entry(TiffTag::ImageWidth, 10000),
543 make_entry(TiffTag::ImageLength, 8000),
544 make_entry(TiffTag::TileWidth, 256),
545 make_entry(TiffTag::TileLength, 256),
546 IfdEntry {
547 tag_id: TiffTag::TileOffsets.as_u16(),
548 field_type: Some(FieldType::Long),
549 field_type_raw: 4,
550 count: 100,
551 value_offset_bytes: vec![0, 0, 0, 0],
552 is_inline: false,
553 },
554 IfdEntry {
555 tag_id: TiffTag::TileByteCounts.as_u16(),
556 field_type: Some(FieldType::Long),
557 field_type_raw: 4,
558 count: 100,
559 value_offset_bytes: vec![0, 0, 0, 0],
560 is_inline: false,
561 },
562 IfdEntry {
563 tag_id: TiffTag::Compression.as_u16(),
564 field_type: Some(FieldType::Short),
565 field_type_raw: 3,
566 count: 1,
567 value_offset_bytes: vec![5, 0, 0, 0], is_inline: true,
569 },
570 ];
571
572 let mut entries_by_tag = HashMap::new();
573 for (i, entry) in entries.iter().enumerate() {
574 entries_by_tag.insert(entry.tag_id, i);
575 }
576
577 Ifd {
578 entries,
579 entries_by_tag,
580 next_ifd_offset: 0,
581 }
582 }
583
584 #[test]
589 fn test_validate_tiled_jpeg_ifd() {
590 let ifd = make_tiled_ifd();
591 let header = make_header();
592 let result = validate_ifd(&ifd, 0, header.byte_order);
593
594 assert!(result.is_valid);
595 assert!(result.errors.is_empty());
596 }
597
598 #[test]
599 fn test_validate_stripped_ifd() {
600 let ifd = make_stripped_ifd();
601 let header = make_header();
602 let result = validate_ifd(&ifd, 0, header.byte_order);
603
604 assert!(!result.is_valid);
605 assert_eq!(result.errors.len(), 1);
606 assert!(matches!(
607 result.errors[0],
608 ValidationError::StripOrganization { ifd_index: 0 }
609 ));
610 }
611
612 #[test]
613 fn test_validate_lzw_ifd() {
614 let ifd = make_lzw_ifd();
615 let header = make_header();
616 let result = validate_ifd(&ifd, 0, header.byte_order);
617
618 assert!(!result.is_valid);
619 assert!(matches!(
620 result.errors[0],
621 ValidationError::UnsupportedCompression { compression: 5, .. }
622 ));
623 }
624
625 #[test]
626 fn test_validate_missing_tile_tags() {
627 let entries = vec![
629 make_entry(TiffTag::ImageWidth, 10000),
630 make_entry(TiffTag::ImageLength, 8000),
631 make_entry(TiffTag::TileWidth, 256),
632 make_entry(TiffTag::TileLength, 256),
633 IfdEntry {
634 tag_id: TiffTag::Compression.as_u16(),
635 field_type: Some(FieldType::Short),
636 field_type_raw: 3,
637 count: 1,
638 value_offset_bytes: vec![7, 0, 0, 0],
639 is_inline: true,
640 },
641 ];
642
643 let mut entries_by_tag = HashMap::new();
644 for (i, entry) in entries.iter().enumerate() {
645 entries_by_tag.insert(entry.tag_id, i);
646 }
647
648 let ifd = Ifd {
649 entries,
650 entries_by_tag,
651 next_ifd_offset: 0,
652 };
653
654 let header = make_header();
655 let result = validate_ifd(&ifd, 0, header.byte_order);
656
657 assert!(!result.is_valid);
658 assert!(matches!(
659 result.errors[0],
660 ValidationError::MissingTileTags { .. }
661 ));
662 }
663
664 #[test]
669 fn test_check_compression_jpeg() {
670 let ifd = make_tiled_ifd();
671 let header = make_header();
672 assert!(check_compression(&ifd, header.byte_order).is_ok());
673 }
674
675 #[test]
676 fn test_check_compression_lzw() {
677 let ifd = make_lzw_ifd();
678 let header = make_header();
679 let result = check_compression(&ifd, header.byte_order);
680 assert!(matches!(result, Err(TiffError::UnsupportedCompression(_))));
681 }
682
683 #[test]
684 fn test_check_tiled_with_tiles() {
685 let ifd = make_tiled_ifd();
686 assert!(check_tiled(&ifd).is_ok());
687 }
688
689 #[test]
690 fn test_check_tiled_with_strips() {
691 let ifd = make_stripped_ifd();
692 let result = check_tiled(&ifd);
693 assert!(matches!(result, Err(TiffError::StripOrganization)));
694 }
695
696 #[test]
697 fn test_check_tile_tags_present() {
698 let ifd = make_tiled_ifd();
699 assert!(check_tile_tags(&ifd).is_ok());
700 }
701
702 #[test]
703 fn test_check_tile_tags_missing() {
704 let ifd = make_stripped_ifd();
705 let result = check_tile_tags(&ifd);
706 assert!(matches!(result, Err(TiffError::MissingTag(_))));
707 }
708
709 #[test]
714 fn test_validation_result_ok() {
715 let result = ValidationResult::ok();
716 assert!(result.is_valid);
717 assert!(result.errors.is_empty());
718 assert!(result.into_result().is_ok());
719 }
720
721 #[test]
722 fn test_validation_result_error() {
723 let result = ValidationResult::error(ValidationError::NoPyramidLevels);
724 assert!(!result.is_valid);
725 assert!(result.into_result().is_err());
726 }
727
728 #[test]
729 fn test_validation_error_to_tiff_error() {
730 let strip_error = ValidationError::StripOrganization { ifd_index: 0 };
731 let tiff_error: TiffError = strip_error.into();
732 assert!(matches!(tiff_error, TiffError::StripOrganization));
733
734 let compression_error = ValidationError::UnsupportedCompression {
735 ifd_index: 0,
736 compression: 5,
737 compression_name: "LZW".to_string(),
738 };
739 let tiff_error: TiffError = compression_error.into();
740 assert!(matches!(tiff_error, TiffError::UnsupportedCompression(_)));
741 }
742}