1#![allow(
6 clippy::wildcard_imports,
7 clippy::doc_markdown,
8 clippy::cast_possible_truncation,
9 clippy::cast_sign_loss
10)]
11
12use crate::error::DecodeError;
13use crate::photodb::types::*;
14use crate::profile_db::ProfileDb;
15
16const MAX_DEPTH: u32 = 64;
22
23#[derive(Debug, Clone)]
29pub struct PhotoDbEntry {
30 pub format_id: i32,
32 pub data: Vec<u8>,
34 pub ithmb_offset: i32,
37 pub image_size: i32,
39 pub width: i32,
41 pub height: i32,
43}
44
45#[must_use]
55pub fn detect_endianness(data: &[u8]) -> Option<bool> {
56 if data.len() < 4 {
57 return None;
58 }
59 if data[0] == 0x6d && data[1] == 0x68 && data[2] == 0x66 && data[3] == 0x64 {
61 return Some(true);
62 }
63 if data[0] == 0x64 && data[1] == 0x66 && data[2] == 0x68 && data[3] == 0x6d {
65 return Some(false);
66 }
67 None
68}
69
70#[must_use]
73pub fn can_open_photodb(data: &[u8]) -> bool {
74 detect_endianness(data).is_some()
75}
76
77pub fn try_parse_photodb(data: &[u8], entries: &mut Vec<PhotoDbEntry>) -> Result<(), DecodeError> {
90 let little_endian = detect_endianness(data)
91 .ok_or_else(|| DecodeError::InvalidFormat("not a valid PhotoDB/ArtworkDB file".into()))?;
92
93 let mut offset = 0usize;
95 let mhfd = MhfdHeader::parse(data, &mut offset, little_endian)?;
96 if mhfd.magic != MHFD {
97 return Err(DecodeError::InvalidFormat("MHFD header has wrong magic".into()));
98 }
99 walk_entries(data, offset, data.len(), little_endian, entries, 0)?;
103
104 let db = ProfileDb::load_builtin().ok();
106 for entry in entries.iter_mut() {
107 let has_profile = db.as_ref().and_then(|d| d.get(entry.format_id)).is_some();
108 if !has_profile && entry.data.len() >= 2 && entry.data[0] == 0xFF && entry.data[1] == 0xD8 {
109 trim_jpeg(&mut entry.data);
110 }
111 }
112
113 Ok(())
114}
115
116#[must_use]
128fn has_child_chunks(data: &[u8], start: usize, end: usize, little_endian: bool) -> bool {
129 if start + 8 > end || start + 8 > data.len() {
130 return false;
131 }
132 let hdr_size = read_u32(data, start + 4, little_endian);
133 if hdr_size < 8 {
134 return false;
135 }
136 let magic = read_u32(data, start, little_endian);
137 is_known_magic(magic)
138}
139
140#[allow(clippy::too_many_lines)]
152fn walk_entries(
153 data: &[u8],
154 start: usize,
155 end: usize,
156 little_endian: bool,
157 entries: &mut Vec<PhotoDbEntry>,
158 depth: u32,
159) -> Result<(), DecodeError> {
160 if depth > MAX_DEPTH {
161 return Ok(());
162 }
163 if start >= end || start >= data.len() {
164 return Ok(());
165 }
166
167 let mut pos = start;
168 while pos < end && pos < data.len() {
169 if pos + 8 > data.len() || pos + 8 > end {
171 break;
172 }
173
174 let magic = read_u32(data, pos, little_endian);
175 let hdr_size = read_u32(data, pos + 4, little_endian);
176
177 if !is_known_magic(magic) || hdr_size < 8 {
179 break;
180 }
181
182 let hdr_size_usize = hdr_size as usize;
184 let chunk_end = pos.saturating_add(hdr_size_usize).min(data.len());
185 if chunk_end <= pos {
186 break;
187 }
188 let mut next_pos = chunk_end;
191 match magic {
192 MHFD => {
193 let mut hdr_pos = pos;
196 let _ = MhfdHeader::parse(data, &mut hdr_pos, little_endian)?;
197 walk_entries(data, hdr_pos, chunk_end, little_endian, entries, depth + 1)?;
198 }
199
200 MHSD => {
201 let mut hdr_pos = pos;
203 let _ = MhsdHeader::parse(data, &mut hdr_pos, little_endian)?;
204 let child_start = pos + MhsdHeader::SIZE;
205 if child_start < chunk_end && has_child_chunks(data, child_start, chunk_end, little_endian) {
206 walk_entries(data, child_start, chunk_end, little_endian, entries, depth + 1)?;
207 }
208 }
209
210 MHL => {
211 let mut hdr_pos = pos;
213 let _ = MhlHeader::parse(data, &mut hdr_pos, little_endian)?;
214 let child_start = pos + MhlHeader::SIZE;
215 if child_start < chunk_end {
216 walk_entries(data, child_start, chunk_end, little_endian, entries, depth + 1)?;
217 }
218 }
219
220 MHII => {
221 let mut hdr_pos = pos;
224 let _ = MhiiHeader::parse(data, &mut hdr_pos, little_endian)?;
225 let total_len = read_u32(data, pos + 8, little_endian) as usize;
226 let child_start = pos + MhiiHeader::SIZE;
227 let child_end = pos.saturating_add(total_len).min(data.len());
228 if child_start < child_end {
229 walk_entries(data, child_start, child_end, little_endian, entries, depth + 1)?;
230 }
231 next_pos = child_end;
234 }
235
236 MHNI => {
237 let mut mhni_pos = pos;
240 let mhni = MhniHeader::parse(data, &mut mhni_pos, little_endian)?;
241
242 let entry_data = if mhni.ithmb_offset >= 0 && mhni.image_size > 0 {
243 let off = mhni.ithmb_offset as usize;
244 let sz = mhni.image_size as usize;
245 if off.saturating_add(sz) <= data.len() {
246 data[off..off + sz].to_vec()
247 } else {
248 Vec::new()
249 }
250 } else {
251 Vec::new()
252 };
253
254 entries.push(PhotoDbEntry {
255 format_id: mhni.format_id,
256 data: entry_data,
257 ithmb_offset: mhni.ithmb_offset,
258 image_size: mhni.image_size,
259 width: mhni.width,
260 height: mhni.height,
261 });
262 }
263
264 MHBA => {
265 let mut hdr_pos = pos;
267 let _ = MhbaHeader::parse(data, &mut hdr_pos, little_endian)?;
268 let child_start = pos + MhbaHeader::SIZE;
269 if child_start < chunk_end {
270 walk_entries(data, child_start, chunk_end, little_endian, entries, depth + 1)?;
271 }
272 }
273
274 MHIA => {
275 let mut hdr_pos = pos;
277 let _ = MhiaHeader::parse(data, &mut hdr_pos, little_endian)?;
278 let child_start = pos + MhiaHeader::SIZE;
279 if child_start < chunk_end {
280 walk_entries(data, child_start, chunk_end, little_endian, entries, depth + 1)?;
281 }
282 }
283
284 MHIF | MHOD => {
285 }
288
289 _ => {
290 break;
293 }
294 }
295
296 pos = next_pos;
297 }
298
299 Ok(())
300}
301
302fn trim_jpeg(data: &mut Vec<u8>) {
314 if data.len() < 2 {
315 return;
316 }
317 let mut i = data.len().saturating_sub(2);
319 loop {
320 if data[i] == 0xFF && data[i + 1] == 0xD9 {
321 data.truncate(i + 2);
322 return;
323 }
324 if i == 0 {
325 break;
326 }
327 i -= 1;
328 }
329}
330
331#[must_use]
342pub fn get_format_id_name(format_id: i32) -> String {
343 match ProfileDb::load_builtin() {
344 Ok(db) => match db.get(format_id) {
345 Some(profile) => {
346 format!(
347 "F{}.{}x{} {}",
348 profile.prefix,
349 profile.width,
350 profile.height,
351 format!("{:?}", profile.encoding).to_lowercase(),
352 )
353 }
354 None => format!("F{format_id} (unknown)"),
355 },
356 Err(_) => format!("F{format_id} (no profile db)"),
357 }
358}
359
360#[cfg(test)]
365#[allow(clippy::unwrap_used)]
366mod tests {
367 use super::*;
368
369 #[test]
372 fn detect_endianness_le() {
373 let data = b"mhfd\x0c\x00\x00\x00";
374 assert_eq!(detect_endianness(data), Some(true));
375 }
376
377 #[test]
378 fn detect_endianness_be() {
379 let data: &[u8] = &[0x64, 0x66, 0x68, 0x6d, 0x00, 0x00, 0x00, 0x0c];
380 assert_eq!(detect_endianness(data), Some(false));
381 }
382
383 #[test]
384 fn detect_endianness_invalid() {
385 assert_eq!(detect_endianness(b"xxxx"), None);
386 }
387
388 #[test]
389 fn detect_endianness_too_short() {
390 assert_eq!(detect_endianness(b"abc"), None);
391 assert_eq!(detect_endianness(b""), None);
392 }
393
394 #[test]
397 fn can_open_photodb_le() {
398 assert!(can_open_photodb(b"mhfd..."));
399 }
400
401 #[test]
402 fn can_open_photodb_be() {
403 let data: &[u8] = &[0x64, 0x66, 0x68, 0x6d];
404 assert!(can_open_photodb(data));
405 }
406
407 #[test]
408 fn can_open_photodb_invalid() {
409 assert!(!can_open_photodb(b"xxxx"));
410 assert!(!can_open_photodb(b""));
411 }
412
413 #[test]
416 fn has_child_chunks_recognises_mhsd() {
417 let mut data = vec![0u8; 32];
419 data[0..4].copy_from_slice(b"mhsd");
420 data[4..8].copy_from_slice(&[16, 0, 0, 0]);
422 assert!(has_child_chunks(&data, 0, 32, true));
423 }
424
425 #[test]
426 fn has_child_chunks_rejects_short_buffer() {
427 let data = b"mhsd";
428 assert!(!has_child_chunks(data, 0, 4, true));
429 }
430
431 #[test]
432 fn has_child_chunks_rejects_unknown_magic() {
433 let mut data = vec![0u8; 16];
434 data[0..4].copy_from_slice(b"xxxx");
435 data[4..8].copy_from_slice(&[16, 0, 0, 0]);
436 assert!(!has_child_chunks(&data, 0, 16, true));
437 }
438
439 #[test]
440 fn has_child_chunks_rejects_tiny_header_size() {
441 let mut data = vec![0u8; 16];
442 data[0..4].copy_from_slice(b"mhsd");
443 data[4..8].copy_from_slice(&[4, 0, 0, 0]); assert!(!has_child_chunks(&data, 0, 16, true));
445 }
446
447 fn build_minimal_photodb_le() -> Vec<u8> {
452 let tree_size = 12 + 16 + 12 + 12 + 36;
461 let pixel_data: &[u8] = &[
462 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01,
463 0x00, 0x00, 0xFF, 0xD9, 0xCC, 0xCC, 0xCC, 0xCC,
465 ]; let pixel_offset = tree_size;
467
468 let mut data = vec![0u8; tree_size + pixel_data.len()];
469
470 let mut off = 0usize;
471
472 data[off..off + 4].copy_from_slice(b"mhfd");
474 data[off + 4..off + 8].copy_from_slice(&[12, 0, 0, 0]); data[off + 8..off + 12].copy_from_slice(&[1, 0, 0, 0]); off += 12;
477
478 let mhsd_total: u32 = 16 + 12 + 12 + 36;
482 data[off..off + 4].copy_from_slice(b"mhsd");
483 data[off + 4..off + 8].copy_from_slice(&mhsd_total.to_le_bytes());
484 data[off + 8..off + 10].copy_from_slice(&[0, 0]); data[off + 10..off + 12].copy_from_slice(&[4, 0]); data[off + 12..off + 16].copy_from_slice(&[1, 0, 0, 0]); off += 16;
488
489 data[off..off + 4].copy_from_slice(b"mhli");
491 data[off + 4..off + 8].copy_from_slice(&[12, 0, 0, 0]); data[off + 8..off + 12].copy_from_slice(&[1, 0, 0, 0]); off += 12;
494
495 let mhii_total: u32 = 12 + 36;
497 data[off..off + 4].copy_from_slice(b"mhii");
498 data[off + 4..off + 8].copy_from_slice(&[12, 0, 0, 0]); data[off + 8..off + 12].copy_from_slice(&mhii_total.to_le_bytes()); off += 12;
501
502 let img_size = 22i32; data[off..off + 4].copy_from_slice(b"mhni");
508 data[off + 4..off + 8].copy_from_slice(&[36, 0, 0, 0]); data[off + 16..off + 20].copy_from_slice(&[0xFB, 0x03, 0, 0]); data[off + 20..off + 24].copy_from_slice(&i32::try_from(pixel_offset).unwrap().to_le_bytes()); data[off + 24..off + 28].copy_from_slice(&img_size.to_le_bytes()); data[off + 32..off + 34].copy_from_slice(&[16, 0]); data[off + 34..off + 36].copy_from_slice(&[16, 0]); off += 36;
517
518 data[off..off + pixel_data.len()].copy_from_slice(pixel_data);
520
521 data
522 }
523
524 #[test]
525 fn try_parse_photodb_extracts_inline_mhni() {
526 let photodb = build_minimal_photodb_le();
527 let mut entries = Vec::new();
528 try_parse_photodb(&photodb, &mut entries).unwrap();
529
530 assert_eq!(entries.len(), 1);
531 let entry = &entries[0];
532 assert_eq!(entry.format_id, 1019);
533 assert_eq!(entry.ithmb_offset, 88);
534 let pixel_offset: usize = 12 + 16 + 12 + 12 + 36;
536 assert_eq!(entry.ithmb_offset as usize, pixel_offset);
537 assert_eq!(entry.image_size, 22);
540 assert_eq!(entry.width, 16);
541 assert_eq!(entry.height, 16);
542 assert_eq!(entry.data.len(), 22);
544 }
545
546 #[test]
547 fn try_parse_photodb_invalid_magic() {
548 let data = b"xxxx";
549 let mut entries = Vec::new();
550 let result = try_parse_photodb(data, &mut entries);
551 assert!(result.is_err());
552 }
553
554 #[test]
555 fn try_parse_photodb_empty() {
556 let data = b"";
557 let mut entries = Vec::new();
558 let result = try_parse_photodb(data, &mut entries);
559 assert!(result.is_err());
560 }
561
562 #[test]
563 fn try_parse_photodb_trims_jpeg_for_unknown_profile() {
564 let mut photodb = build_minimal_photodb_le();
568 let mhni_offset = 52usize;
571 photodb[mhni_offset + 16..mhni_offset + 20].copy_from_slice(&[0x0F, 0x27, 0, 0]); let mut entries = Vec::new();
574 try_parse_photodb(&photodb, &mut entries).unwrap();
575
576 assert_eq!(entries.len(), 1);
577 let entry = &entries[0];
578 assert_eq!(entry.format_id, 9999);
579 assert_eq!(entry.data.len(), 22);
581 assert_eq!(&entry.data[..2], &[0xFF, 0xD8]);
582 assert_eq!(&entry.data[entry.data.len() - 2..], &[0xFF, 0xD9]);
583 }
584
585 #[test]
588 fn has_child_chunks_outside_end_range() {
589 let mut data = vec![0u8; 16];
590 data[0..4].copy_from_slice(b"mhsd");
591 data[4..8].copy_from_slice(&[16, 0, 0, 0]);
592 assert!(!has_child_chunks(&data, 0, 7, true));
594 }
595
596 #[test]
599 fn walk_entries_depth_limit_returns_early() {
600 let data = b"mhfd\x0c\x00\x00\x00\x00\x00\x00\x00";
602 let mut entries = Vec::new();
603 let result = walk_entries(data, 0, data.len(), true, &mut entries, MAX_DEPTH + 1);
604 assert!(result.is_ok());
605 assert!(entries.is_empty());
606 }
607
608 #[test]
609 fn walk_entries_empty_range() {
610 let data = b"";
611 let mut entries = Vec::new();
612 let result = walk_entries(data, 0, 0, true, &mut entries, 0);
613 assert!(result.is_ok());
614 assert!(entries.is_empty());
615 }
616
617 #[test]
620 fn get_format_id_name_known() {
621 let name = get_format_id_name(1007);
622 assert!(name.contains("1007"));
623 assert!(name.contains("480"));
624 assert!(name.contains("864"));
625 }
626
627 #[test]
628 fn get_format_id_name_unknown() {
629 let name = get_format_id_name(9999);
630 assert!(name.contains("unknown") || name.contains("9999"));
631 }
632
633 #[test]
636 fn trim_jpeg_finds_eoi() {
637 let mut data = vec![0xFF, 0xD8, 0x00, 0x00, 0xFF, 0xD9, 0xCC, 0xCC, 0xCC];
638 trim_jpeg(&mut data);
639 assert_eq!(data.len(), 6);
640 assert_eq!(&data[4..6], &[0xFF, 0xD9]);
641 }
642
643 #[test]
644 fn trim_jpeg_no_eoi() {
645 let mut data = vec![0xFF, 0xD8, 0x00, 0x00, 0x00, 0x00];
646 trim_jpeg(&mut data);
647 assert_eq!(data.len(), 6); }
649
650 #[test]
651 fn trim_jpeg_short_buffer() {
652 let mut data = vec![0xFF];
653 trim_jpeg(&mut data);
654 assert_eq!(data.len(), 1); }
656}