1#![allow(
15 clippy::similar_names,
16 clippy::doc_markdown,
17 clippy::cast_possible_truncation,
18 clippy::cast_sign_loss,
19 clippy::manual_let_else,
20 clippy::match_same_arms,
21 clippy::single_match_else,
22 clippy::cast_possible_wrap
23)]
24
25use crate::error::DecodeError;
26use crate::photodb::types::{
27 MHBA, MHFD, MHIA, MHIF, MHII, MHL, MHNI, MHOD, MHSD, is_known_magic, read_i32, read_u32, read_u32_be, read_u32_le,
28};
29use crate::profile::Profile;
30use crate::profile_db::ProfileDb;
31
32#[derive(Debug, Clone)]
41pub struct BuildEntry {
42 pub format_id: i32,
44 pub data: Vec<u8>,
46}
47
48#[derive(Debug, Clone)]
50struct MhniEntry {
51 format_id: i32,
52 ithmb_offset: i32,
53 image_size: i32,
54}
55
56struct WalkState {
58 entries: Vec<MhniEntry>,
59 max_chunk_end: usize,
60 issues: Vec<String>,
61}
62
63fn has_child_chunks(data: &[u8], pos: usize, end: usize, little_endian: bool) -> bool {
72 if pos + 8 > end || pos + 8 > data.len() {
73 return false;
74 }
75 let hdr_size = read_u32(data, pos + 4, little_endian);
76 if hdr_size < 8 {
77 return false;
78 }
79 let magic = read_u32(data, pos, little_endian);
80 is_known_magic(magic)
81}
82
83fn can_open_photodb(data: &[u8]) -> bool {
86 if data.len() < 4 {
87 return false;
88 }
89 let magic_le = read_u32_le(data, 0);
93 let magic_be = read_u32_be(data, 0);
94 magic_le == MHFD || magic_be == MHFD
95}
96
97fn detect_endianness(data: &[u8]) -> Option<bool> {
102 if data.len() < 8 {
103 return None;
104 }
105 let le_magic = read_u32_le(data, 0);
106 let be_magic = read_u32_be(data, 0);
107 if le_magic == MHFD {
108 let hdr_size = read_u32_le(data, 4);
110 if hdr_size >= 12 && (hdr_size as usize) <= data.len() {
111 return Some(true);
112 }
113 }
114 if be_magic == MHFD {
115 let hdr_size = read_u32_be(data, 4);
116 if hdr_size >= 12 && (hdr_size as usize) <= data.len() {
117 return Some(false);
118 }
119 }
120 if le_magic == MHFD {
123 return Some(true);
124 }
125 if be_magic == MHFD {
126 return Some(false);
127 }
128 None
129}
130
131fn chunk_total_end(data: &[u8], pos: usize, magic: u32, _hdr_size: u32, little_endian: bool) -> usize {
134 let hdr_size = read_u32(data, pos + 4, little_endian) as usize;
135 match magic {
136 MHII => {
138 if pos + 12 <= data.len() {
139 pos + read_u32(data, pos + 8, little_endian) as usize
140 } else {
141 pos + 12
142 }
143 }
144 MHNI => {
149 if pos + 12 <= data.len() {
150 let total_len = read_u32(data, pos + 8, little_endian) as usize;
151 if total_len > hdr_size && total_len <= data.len().saturating_sub(pos) {
152 pos + total_len
153 } else {
154 pos + hdr_size
155 }
156 } else {
157 pos + hdr_size
158 }
159 }
160 _ => pos + hdr_size,
162 }
163}
164
165#[allow(clippy::too_many_arguments)]
176fn integrity_walk_tree(
177 data: &[u8],
178 offset: usize,
179 end: usize,
180 depth: usize,
181 little_endian: bool,
182 state: &mut WalkState,
183) {
184 if depth > 64 {
185 state.issues.push("Maximum chunk nesting depth (64) exceeded".into());
186 return;
187 }
188
189 let mut pos = offset;
190 while pos < end && pos + 8 <= data.len() {
191 if !has_child_chunks(data, pos, end, little_endian) {
193 break;
196 }
197
198 let magic = read_u32(data, pos, little_endian);
199 let hdr_size = read_u32(data, pos + 4, little_endian) as usize;
200 let chunk_end = chunk_total_end(data, pos, magic, hdr_size as u32, little_endian);
201 state.max_chunk_end = state.max_chunk_end.max(chunk_end);
202
203 if hdr_size < 8 {
205 state
206 .issues
207 .push(format!("Chunk at offset {pos} has invalid header_size={hdr_size}"));
208 pos = chunk_end;
209 continue;
210 }
211
212 match magic {
213 MHFD => {
214 let children_start = pos + 12;
216 let children_end = chunk_end.min(end);
217 if children_start < children_end {
218 integrity_walk_tree(data, children_start, children_end, depth + 1, little_endian, state);
219 }
220 }
221 MHSD => {
222 let children_start = pos + 16;
224 let children_end = chunk_end.min(end);
225 if children_start < children_end {
226 integrity_walk_tree(data, children_start, children_end, depth + 1, little_endian, state);
227 }
228 }
229 MHL => {
230 let children_start = pos + 12;
232 let children_end = chunk_end.min(end);
233 if children_start < children_end {
234 integrity_walk_tree(data, children_start, children_end, depth + 1, little_endian, state);
235 }
236 }
237 MHII | MHIF | MHOD => {
238 }
240 MHBA | MHIA => {
241 let children_start = pos + 12;
243 let children_end = chunk_end.min(end);
244 if children_start < children_end {
245 integrity_walk_tree(data, children_start, children_end, depth + 1, little_endian, state);
246 }
247 }
248 MHNI => {
249 if hdr_size >= 36 && pos + 28 <= data.len() {
251 let format_id = read_i32(data, pos + 16, little_endian);
252 let ithmb_offset = read_i32(data, pos + 20, little_endian);
253 let image_size = read_i32(data, pos + 24, little_endian);
254 state.entries.push(MhniEntry {
255 format_id,
256 ithmb_offset,
257 image_size,
258 });
259 }
260 }
262 _ => {
263 state
266 .issues
267 .push(format!("Unknown chunk magic 0x{magic:08x} at offset {pos}"));
268 }
269 }
270
271 let next_pos = chunk_end.max(pos + 1);
274 if next_pos <= pos {
275 break;
276 }
277 pos = next_pos;
278 }
279}
280
281fn write_u32_le(buf: &mut [u8], offset: usize, value: u32) {
287 buf[offset] = (value & 0xff) as u8;
288 buf[offset + 1] = ((value >> 8) & 0xff) as u8;
289 buf[offset + 2] = ((value >> 16) & 0xff) as u8;
290 buf[offset + 3] = ((value >> 24) & 0xff) as u8;
291}
292
293fn write_i32_le(buf: &mut [u8], offset: usize, value: i32) {
294 write_u32_le(buf, offset, value as u32);
295}
296
297fn write_u16_le(buf: &mut [u8], offset: usize, value: u16) {
298 buf[offset] = (value & 0xff) as u8;
299 buf[offset + 1] = ((value >> 8) & 0xff) as u8;
300}
301
302#[must_use]
326pub fn integrity_check_photodb(data: &[u8]) -> Vec<String> {
327 let mut issues: Vec<String> = Vec::new();
328
329 if data.len() < 4 {
331 issues.push(format!("Data too short: {} bytes (need at least 4)", data.len()));
332 return issues;
333 }
334
335 if !can_open_photodb(data) {
337 issues.push("File does not start with valid MHFD magic".into());
338 return issues;
339 }
340
341 let little_endian = match detect_endianness(data) {
343 Some(le) => le,
344 None => {
345 issues.push("Cannot determine endianness from MHFD header".into());
346 return issues;
347 }
348 };
349
350 let profile_db = match ProfileDb::load_builtin() {
352 Ok(db) => Some(db),
353 Err(e) => {
354 issues.push(format!(
355 "Cannot load profile DB (continuing with limited validation): {e}"
356 ));
357 None
358 }
359 };
360
361 if data.len() < 12 {
363 issues.push(format!(
364 "Data too short for MHFD header: {} bytes (need at least 12)",
365 data.len()
366 ));
367 return issues;
368 }
369 let mhfd_hdr_size = read_u32(data, 4, little_endian) as usize;
370 if mhfd_hdr_size < 12 {
371 issues.push(format!("MHFD header_size ({mhfd_hdr_size}) is less than minimum 12"));
372 }
373 if mhfd_hdr_size > data.len() {
374 issues.push(format!(
375 "MHFD header_size ({mhfd_hdr_size}) exceeds data length ({})",
376 data.len()
377 ));
378 }
379 let mhfd_entry_count = if data.len() >= 12 {
381 read_u32(data, 8, little_endian)
382 } else {
383 0
384 };
385
386 let mut state = WalkState {
388 entries: Vec::new(),
389 max_chunk_end: mhfd_hdr_size, issues: Vec::new(),
391 };
392
393 integrity_walk_tree(data, 0, data.len(), 0, little_endian, &mut state);
397
398 issues.append(&mut state.issues);
400
401 if let Some(ref db) = profile_db {
403 for (idx, entry) in state.entries.iter().enumerate() {
404 if db.get(entry.format_id).is_none() {
405 issues.push(format!(
406 "Entry {idx}: unknown format ID {} (no matching profile)",
407 entry.format_id
408 ));
409 }
410 }
411 }
412
413 for i in 0..state.entries.len() {
415 for j in (i + 1)..state.entries.len() {
416 let a = &state.entries[i];
417 let b = &state.entries[j];
418 if a.ithmb_offset >= 0 && b.ithmb_offset >= 0 && a.image_size > 0 && b.image_size > 0 {
421 let a_start = a.ithmb_offset as usize;
422 let a_end = a_start + a.image_size as usize;
423 let b_start = b.ithmb_offset as usize;
424 let b_end = b_start + b.image_size as usize;
425
426 if a_start < b_end && b_start < a_end {
427 issues.push(format!(
428 "Entries {i} and {j} have overlapping ithmb offset ranges \
429 (entry {i}: [{a_start}..{a_end}), entry {j}: [{b_start}..{b_end}))"
430 ));
431 }
432 }
433 }
434 }
435
436 let effective_end = state.max_chunk_end.max(mhfd_hdr_size);
438 if effective_end < data.len() {
439 let garbage_bytes = data.len() - effective_end;
440 if garbage_bytes > 3 {
442 issues.push(format!(
443 "Trailing garbage after last known chunk boundary: \
444 {garbage_bytes} bytes at offset {effective_end} (file length: {})",
445 data.len()
446 ));
447 }
448 }
449
450 if issues.is_empty() {
453 issues.push(format!(
454 "PhotoDB appears valid: {mhfd_entry_count} section(s), {} entry(s)",
455 state.entries.len()
456 ));
457 }
458
459 issues
460}
461
462pub fn try_build_photodb(
483 entries: &[BuildEntry],
484 mhni_header_size: i32,
485 mhni_padding_size: i32,
486) -> Result<Vec<u8>, DecodeError> {
487 if entries.is_empty() {
489 return Err(DecodeError::InvalidFormat(
490 "Cannot build PhotoDB with zero entries".into(),
491 ));
492 }
493
494 let db = ProfileDb::load_builtin().map_err(|e| DecodeError::Profile(format!("Failed to load profile DB: {e}")))?;
495
496 let mhni_total_len = mhni_header_size + mhni_padding_size;
497 let mhni_total_len_usize = mhni_total_len as usize;
498
499 let mut profiles: Vec<&Profile> = Vec::with_capacity(entries.len());
501 for (idx, entry) in entries.iter().enumerate() {
502 let profile = db
503 .get(entry.format_id)
504 .ok_or_else(|| DecodeError::InvalidFormat(format!("Entry {idx}: unknown format ID {}", entry.format_id)))?;
505 let expected_len = profile.frame_byte_length as usize;
506 if entry.data.len() != expected_len {
507 return Err(DecodeError::InvalidFormat(format!(
508 "Entry {idx}: format ID {} has data length {}, expected {} (frame_byte_length)",
509 entry.format_id,
510 entry.data.len(),
511 expected_len,
512 )));
513 }
514 profiles.push(profile);
515 }
516
517 let n = entries.len();
519 let mhsd_header_size = 16 + (n * mhni_total_len_usize) + entries.iter().map(|e| e.data.len()).sum::<usize>();
520 let total_size = 12 + mhsd_header_size;
521
522 let mut buf = vec![0u8; total_size];
523
524 buf[0..4].copy_from_slice(b"mhfd");
527 write_u32_le(&mut buf, 4, 12);
528 write_u32_le(&mut buf, 8, 1); let mhsd_offset = 12;
534 buf[mhsd_offset..mhsd_offset + 4].copy_from_slice(b"mhsd");
535 write_u32_le(&mut buf, mhsd_offset + 4, mhsd_header_size as u32);
536 write_u16_le(&mut buf, mhsd_offset + 8, 0); write_u16_le(&mut buf, mhsd_offset + 10, 4); write_u32_le(&mut buf, mhsd_offset + 12, n as u32); let mhni_start = mhsd_offset + 16; let pixel_data_start = mhni_start + n * mhni_total_len_usize;
543
544 let mut current_pixel_offset = pixel_data_start;
546
547 for (i, (entry, profile)) in entries.iter().zip(profiles.iter()).enumerate() {
548 let mhni_pos = mhni_start + i * mhni_total_len_usize;
549
550 buf[mhni_pos..mhni_pos + 4].copy_from_slice(b"mhni");
552 write_u32_le(&mut buf, mhni_pos + 4, mhni_header_size as u32);
554 write_u32_le(&mut buf, mhni_pos + 8, mhni_total_len as u32);
556 write_u32_le(&mut buf, mhni_pos + 12, 1);
558 write_i32_le(&mut buf, mhni_pos + 16, entry.format_id);
560 write_i32_le(&mut buf, mhni_pos + 20, current_pixel_offset as i32);
562 let image_size = entry.data.len() as i32;
564 write_i32_le(&mut buf, mhni_pos + 24, image_size);
565 write_u32_le(&mut buf, mhni_pos + 28, 0);
567 let height = profile.height as u16;
569 write_u16_le(&mut buf, mhni_pos + 32, height);
570 let width = profile.width as u16;
572 write_u16_le(&mut buf, mhni_pos + 34, width);
573 let pad_start = mhni_pos + 36;
577 let pad_end = mhni_pos + mhni_total_len_usize;
578 for b in &mut buf[pad_start..pad_end] {
579 *b = 0;
580 }
581
582 current_pixel_offset += entry.data.len();
584 }
585
586 let mut pixel_write_pos = pixel_data_start;
588 for entry in entries {
589 let data_len = entry.data.len();
590 buf[pixel_write_pos..pixel_write_pos + data_len].copy_from_slice(&entry.data);
591 pixel_write_pos += data_len;
592 }
593
594 Ok(buf)
595}
596
597#[cfg(test)]
602mod tests {
603 use super::*;
604
605 fn make_pixel_data(len: usize) -> Vec<u8> {
608 (0..len).map(|i| (i & 0xFF) as u8).collect()
609 }
610
611 fn build_minimal_photodb(entries: &[BuildEntry], mhni_header_size: i32, mhni_padding_size: i32) -> Vec<u8> {
613 try_build_photodb(entries, mhni_header_size, mhni_padding_size).expect("build should succeed")
614 }
615
616 #[test]
621 fn builder_empty_entries_fails() {
622 let result = try_build_photodb(&[], 36, 40);
623 assert!(result.is_err());
624 }
625
626 #[test]
627 fn builder_unknown_format_id_fails() {
628 let entry = BuildEntry {
629 format_id: 9999,
630 data: vec![0u8; 100],
631 };
632 let result = try_build_photodb(&[entry], 36, 40);
633 assert!(result.is_err());
634 }
635
636 #[test]
637 fn builder_data_length_mismatch_fails() {
638 let entry = BuildEntry {
640 format_id: 1016,
641 data: vec![0u8; 100], };
643 let result = try_build_photodb(&[entry], 36, 40);
644 assert!(result.is_err());
645 }
646
647 #[test]
648 fn builder_single_entry_succeeds() {
649 let entry = BuildEntry {
651 format_id: 1016,
652 data: make_pixel_data(39200),
653 };
654 let result = try_build_photodb(&[entry], 36, 40).unwrap();
655
656 let expected_size = 12 + 16 + 76 + 39200;
658 assert_eq!(result.len(), expected_size);
659
660 assert_eq!(&result[0..4], b"mhfd");
662 assert_eq!(read_u32_le(&result, 4), 12); assert_eq!(read_u32_le(&result, 8), 1); let mhsd_hdr_size: u32 = read_u32_le(&result, 12 + 4);
667 assert!(mhsd_hdr_size >= 16);
668 assert_eq!(read_u16_le(&result, 12 + 8), 0); assert_eq!(read_u16_le(&result, 12 + 10), 4); assert_eq!(&result[28..32], b"mhni");
673 assert_eq!(read_u32_le(&result, 28 + 4), 36); assert_eq!(read_u32_le(&result, 28 + 8), 76); assert_eq!(read_u32_le(&result, 28 + 12), 1); assert_eq!(read_i32_le(&result, 28 + 16), 1016); assert_eq!(read_i32_le(&result, 28 + 20), 28 + 76); assert_eq!(read_i32_le(&result, 28 + 24), 39200); let pixel_start = 28 + 76;
682 assert_eq!(&result[pixel_start..pixel_start + 39200], &make_pixel_data(39200));
683 }
684
685 #[test]
686 fn builder_multiple_entries_succeeds() {
687 let entries = vec![
688 BuildEntry {
689 format_id: 1016,
690 data: make_pixel_data(39200),
691 },
692 BuildEntry {
693 format_id: 3004,
694 data: make_pixel_data(6160),
695 },
696 ];
697 let result = try_build_photodb(&entries, 36, 40).unwrap();
698
699 let expected_size = 12 + 16 + 2 * 76 + 39200 + 6160;
700 assert_eq!(result.len(), expected_size);
701
702 let pixel_data_start = 12 + 16 + 2 * 76;
704 assert_eq!(read_i32_le(&result, 28 + 20), pixel_data_start as i32);
705 assert_eq!(read_i32_le(&result, 28 + 24), 39200);
706
707 let second_mhni = 28 + 76;
709 assert_eq!(
710 read_i32_le(&result, second_mhni + 20) as usize,
711 pixel_data_start + 39200
712 );
713 assert_eq!(read_i32_le(&result, second_mhni + 24), 6160);
714
715 let second_pixel = pixel_data_start + 39200;
717 assert_eq!(
718 &result[pixel_data_start..pixel_data_start + 39200],
719 &make_pixel_data(39200)
720 );
721 assert_eq!(&result[second_pixel..second_pixel + 6160], &make_pixel_data(6160));
722 }
723
724 #[test]
725 fn builder_custom_mhni_sizes() {
726 let entry = BuildEntry {
728 format_id: 1016,
729 data: make_pixel_data(39200),
730 };
731 let result = try_build_photodb(&[entry], 40, 50).unwrap();
732
733 let _ = 40 + 50;
734 assert_eq!(read_u32_le(&result, 28 + 4), 40); assert_eq!(read_u32_le(&result, 28 + 8), 90); let expected_off = 12 + 16 + 90; assert_eq!(read_i32_le(&result, 28 + 20), expected_off);
739 }
740
741 #[test]
746 fn integrity_empty_data() {
747 let issues = integrity_check_photodb(b"");
748 assert!(!issues.is_empty());
749 assert!(issues[0].contains("too short"));
750 }
751
752 #[test]
753 fn integrity_too_short() {
754 let issues = integrity_check_photodb(b"mhf");
755 assert!(!issues.is_empty());
756 }
757
758 #[test]
759 fn integrity_bad_magic() {
760 let issues = integrity_check_photodb(b"XXXX");
761 assert!(!issues.is_empty());
762 assert!(issues[0].contains("magic"));
763 }
764
765 #[test]
766 fn integrity_valid_built_file() {
767 let entry = BuildEntry {
768 format_id: 1016,
769 data: make_pixel_data(39200),
770 };
771 let data = build_minimal_photodb(&[entry], 36, 40);
772 let issues = integrity_check_photodb(&data);
773 let has_clean = issues.iter().any(|i| i.contains("appears valid"));
775 assert!(has_clean, "Expected clean result, got: {issues:?}");
776 }
777
778 #[test]
779 fn integrity_valid_two_entries() {
780 let entries = vec![
781 BuildEntry {
782 format_id: 1016,
783 data: make_pixel_data(39200),
784 },
785 BuildEntry {
786 format_id: 3004,
787 data: make_pixel_data(6160),
788 },
789 ];
790 let data = build_minimal_photodb(&entries, 36, 40);
791 let issues = integrity_check_photodb(&data);
792 let has_clean = issues.iter().any(|i| i.contains("appears valid"));
793 assert!(has_clean, "Expected clean result, got: {issues:?}");
794 }
795
796 #[test]
797 fn integrity_unknown_format_id() {
798 let entry = BuildEntry {
800 format_id: 1016,
801 data: make_pixel_data(39200),
802 };
803 let mut data = build_minimal_photodb(&[entry], 36, 40);
804 write_i32_le(&mut data, 44, 9999);
806 let issues = integrity_check_photodb(&data);
807 let has_unknown = issues.iter().any(|i| i.contains("unknown format ID"));
808 assert!(has_unknown, "Expected unknown format ID issue, got: {issues:?}");
809 }
810
811 #[test]
812 fn integrity_trailing_garbage() {
813 let entry = BuildEntry {
814 format_id: 1016,
815 data: make_pixel_data(39200),
816 };
817 let mut data = build_minimal_photodb(&[entry], 36, 40);
818 data.extend_from_slice(b"GARBAGE_AFTER_END");
820 let issues = integrity_check_photodb(&data);
821 let has_garbage = issues.iter().any(|i| i.contains("garbage"));
822 assert!(has_garbage, "Expected trailing garbage issue, got: {issues:?}");
823 }
824
825 #[test]
826 fn integrity_overlapping_offsets() {
827 let entries = vec![
830 BuildEntry {
831 format_id: 1016,
832 data: make_pixel_data(39200),
833 },
834 BuildEntry {
835 format_id: 3004,
836 data: make_pixel_data(6160),
837 },
838 ];
839 let mut data = build_minimal_photodb(&entries, 36, 40);
840 let first_entry_off = read_i32_le(&data, 28 + 20);
843 write_i32_le(&mut data, 104 + 20, first_entry_off + 100);
844 let issues = integrity_check_photodb(&data);
845 let has_overlap = issues.iter().any(|i| i.contains("overlapping"));
846 assert!(has_overlap, "Expected overlap issue, got: {issues:?}");
847 }
848
849 #[test]
854 fn build_then_integrity_check_roundtrip() {
855 let entries = vec![
856 BuildEntry {
857 format_id: 1007,
858 data: make_pixel_data(829_440),
859 },
860 BuildEntry {
861 format_id: 3004,
862 data: make_pixel_data(6160),
863 },
864 ];
865 let data = build_minimal_photodb(&entries, 36, 40);
866 let issues = integrity_check_photodb(&data);
867 let has_clean = issues.iter().any(|i| i.contains("appears valid"));
868 assert!(
869 has_clean,
870 "Roundtrip should produce a valid PhotoDB. Issues: {issues:?}"
871 );
872 }
873
874 fn read_u16_le(data: &[u8], offset: usize) -> u16 {
879 u16::from(data[offset]) | (u16::from(data[offset + 1]) << 8)
880 }
881
882 fn read_i32_le(data: &[u8], offset: usize) -> i32 {
883 let v = u32::from(data[offset])
884 | (u32::from(data[offset + 1]) << 8)
885 | (u32::from(data[offset + 2]) << 16)
886 | (u32::from(data[offset + 3]) << 24);
887 v as i32
888 }
889}