#![allow(
clippy::wildcard_imports,
clippy::doc_markdown,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
use crate::error::DecodeError;
use crate::photodb::types::*;
use crate::profile_db::ProfileDb;
const MAX_DEPTH: u32 = 64;
#[derive(Debug, Clone)]
pub struct PhotoDbEntry {
pub format_id: i32,
pub data: Vec<u8>,
pub ithmb_offset: i32,
pub image_size: i32,
pub width: i32,
pub height: i32,
}
#[must_use]
pub fn detect_endianness(data: &[u8]) -> Option<bool> {
if data.len() < 4 {
return None;
}
if data[0] == 0x6d && data[1] == 0x68 && data[2] == 0x66 && data[3] == 0x64 {
return Some(true);
}
if data[0] == 0x64 && data[1] == 0x66 && data[2] == 0x68 && data[3] == 0x6d {
return Some(false);
}
None
}
#[must_use]
pub fn can_open_photodb(data: &[u8]) -> bool {
detect_endianness(data).is_some()
}
pub fn try_parse_photodb(data: &[u8], entries: &mut Vec<PhotoDbEntry>) -> Result<(), DecodeError> {
let little_endian = detect_endianness(data)
.ok_or_else(|| DecodeError::InvalidFormat("not a valid PhotoDB/ArtworkDB file".into()))?;
let mut offset = 0usize;
let mhfd = MhfdHeader::parse(data, &mut offset, little_endian)?;
if mhfd.magic != MHFD {
return Err(DecodeError::InvalidFormat("MHFD header has wrong magic".into()));
}
walk_entries(data, offset, data.len(), little_endian, entries, 0)?;
let db = ProfileDb::load_builtin().ok();
for entry in entries.iter_mut() {
let has_profile = db.as_ref().and_then(|d| d.get(entry.format_id)).is_some();
if !has_profile && entry.data.len() >= 2 && entry.data[0] == 0xFF && entry.data[1] == 0xD8 {
trim_jpeg(&mut entry.data);
}
}
Ok(())
}
#[must_use]
fn has_child_chunks(data: &[u8], start: usize, end: usize, little_endian: bool) -> bool {
if start + 8 > end || start + 8 > data.len() {
return false;
}
let hdr_size = read_u32(data, start + 4, little_endian);
if hdr_size < 8 {
return false;
}
let magic = read_u32(data, start, little_endian);
is_known_magic(magic)
}
#[allow(clippy::too_many_lines)]
fn walk_entries(
data: &[u8],
start: usize,
end: usize,
little_endian: bool,
entries: &mut Vec<PhotoDbEntry>,
depth: u32,
) -> Result<(), DecodeError> {
if depth > MAX_DEPTH {
return Ok(());
}
if start >= end || start >= data.len() {
return Ok(());
}
let mut pos = start;
while pos < end && pos < data.len() {
if pos + 8 > data.len() || pos + 8 > end {
break;
}
let magic = read_u32(data, pos, little_endian);
let hdr_size = read_u32(data, pos + 4, little_endian);
if !is_known_magic(magic) || hdr_size < 8 {
break;
}
let hdr_size_usize = hdr_size as usize;
let chunk_end = pos.saturating_add(hdr_size_usize).min(data.len());
if chunk_end <= pos {
break;
}
let mut next_pos = chunk_end;
match magic {
MHFD => {
let mut hdr_pos = pos;
let _ = MhfdHeader::parse(data, &mut hdr_pos, little_endian)?;
walk_entries(data, hdr_pos, chunk_end, little_endian, entries, depth + 1)?;
}
MHSD => {
let mut hdr_pos = pos;
let _ = MhsdHeader::parse(data, &mut hdr_pos, little_endian)?;
let child_start = pos + MhsdHeader::SIZE;
if child_start < chunk_end && has_child_chunks(data, child_start, chunk_end, little_endian) {
walk_entries(data, child_start, chunk_end, little_endian, entries, depth + 1)?;
}
}
MHL => {
let mut hdr_pos = pos;
let _ = MhlHeader::parse(data, &mut hdr_pos, little_endian)?;
let child_start = pos + MhlHeader::SIZE;
if child_start < chunk_end {
walk_entries(data, child_start, chunk_end, little_endian, entries, depth + 1)?;
}
}
MHII => {
let mut hdr_pos = pos;
let _ = MhiiHeader::parse(data, &mut hdr_pos, little_endian)?;
let total_len = read_u32(data, pos + 8, little_endian) as usize;
let child_start = pos + MhiiHeader::SIZE;
let child_end = pos.saturating_add(total_len).min(data.len());
if child_start < child_end {
walk_entries(data, child_start, child_end, little_endian, entries, depth + 1)?;
}
next_pos = child_end;
}
MHNI => {
let mut mhni_pos = pos;
let mhni = MhniHeader::parse(data, &mut mhni_pos, little_endian)?;
let entry_data = if mhni.ithmb_offset >= 0 && mhni.image_size > 0 {
let off = mhni.ithmb_offset as usize;
let sz = mhni.image_size as usize;
if off.saturating_add(sz) <= data.len() {
data[off..off + sz].to_vec()
} else {
Vec::new()
}
} else {
Vec::new()
};
entries.push(PhotoDbEntry {
format_id: mhni.format_id,
data: entry_data,
ithmb_offset: mhni.ithmb_offset,
image_size: mhni.image_size,
width: mhni.width,
height: mhni.height,
});
}
MHBA => {
let mut hdr_pos = pos;
let _ = MhbaHeader::parse(data, &mut hdr_pos, little_endian)?;
let child_start = pos + MhbaHeader::SIZE;
if child_start < chunk_end {
walk_entries(data, child_start, chunk_end, little_endian, entries, depth + 1)?;
}
}
MHIA => {
let mut hdr_pos = pos;
let _ = MhiaHeader::parse(data, &mut hdr_pos, little_endian)?;
let child_start = pos + MhiaHeader::SIZE;
if child_start < chunk_end {
walk_entries(data, child_start, chunk_end, little_endian, entries, depth + 1)?;
}
}
MHIF | MHOD => {
}
_ => {
break;
}
}
pos = next_pos;
}
Ok(())
}
fn trim_jpeg(data: &mut Vec<u8>) {
if data.len() < 2 {
return;
}
let mut i = data.len().saturating_sub(2);
loop {
if data[i] == 0xFF && data[i + 1] == 0xD9 {
data.truncate(i + 2);
return;
}
if i == 0 {
break;
}
i -= 1;
}
}
#[must_use]
pub fn get_format_id_name(format_id: i32) -> String {
match ProfileDb::load_builtin() {
Ok(db) => match db.get(format_id) {
Some(profile) => {
format!(
"F{}.{}x{} {}",
profile.prefix,
profile.width,
profile.height,
format!("{:?}", profile.encoding).to_lowercase(),
)
}
None => format!("F{format_id} (unknown)"),
},
Err(_) => format!("F{format_id} (no profile db)"),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn detect_endianness_le() {
let data = b"mhfd\x0c\x00\x00\x00";
assert_eq!(detect_endianness(data), Some(true));
}
#[test]
fn detect_endianness_be() {
let data: &[u8] = &[0x64, 0x66, 0x68, 0x6d, 0x00, 0x00, 0x00, 0x0c];
assert_eq!(detect_endianness(data), Some(false));
}
#[test]
fn detect_endianness_invalid() {
assert_eq!(detect_endianness(b"xxxx"), None);
}
#[test]
fn detect_endianness_too_short() {
assert_eq!(detect_endianness(b"abc"), None);
assert_eq!(detect_endianness(b""), None);
}
#[test]
fn can_open_photodb_le() {
assert!(can_open_photodb(b"mhfd..."));
}
#[test]
fn can_open_photodb_be() {
let data: &[u8] = &[0x64, 0x66, 0x68, 0x6d];
assert!(can_open_photodb(data));
}
#[test]
fn can_open_photodb_invalid() {
assert!(!can_open_photodb(b"xxxx"));
assert!(!can_open_photodb(b""));
}
#[test]
fn has_child_chunks_recognises_mhsd() {
let mut data = vec![0u8; 32];
data[0..4].copy_from_slice(b"mhsd");
data[4..8].copy_from_slice(&[16, 0, 0, 0]);
assert!(has_child_chunks(&data, 0, 32, true));
}
#[test]
fn has_child_chunks_rejects_short_buffer() {
let data = b"mhsd";
assert!(!has_child_chunks(data, 0, 4, true));
}
#[test]
fn has_child_chunks_rejects_unknown_magic() {
let mut data = vec![0u8; 16];
data[0..4].copy_from_slice(b"xxxx");
data[4..8].copy_from_slice(&[16, 0, 0, 0]);
assert!(!has_child_chunks(&data, 0, 16, true));
}
#[test]
fn has_child_chunks_rejects_tiny_header_size() {
let mut data = vec![0u8; 16];
data[0..4].copy_from_slice(b"mhsd");
data[4..8].copy_from_slice(&[4, 0, 0, 0]); assert!(!has_child_chunks(&data, 0, 16, true));
}
fn build_minimal_photodb_le() -> Vec<u8> {
let tree_size = 12 + 16 + 12 + 12 + 36;
let pixel_data: &[u8] = &[
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01,
0x00, 0x00, 0xFF, 0xD9, 0xCC, 0xCC, 0xCC, 0xCC,
]; let pixel_offset = tree_size;
let mut data = vec![0u8; tree_size + pixel_data.len()];
let mut off = 0usize;
data[off..off + 4].copy_from_slice(b"mhfd");
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;
let mhsd_total: u32 = 16 + 12 + 12 + 36;
data[off..off + 4].copy_from_slice(b"mhsd");
data[off + 4..off + 8].copy_from_slice(&mhsd_total.to_le_bytes());
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;
data[off..off + 4].copy_from_slice(b"mhli");
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;
let mhii_total: u32 = 12 + 36;
data[off..off + 4].copy_from_slice(b"mhii");
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;
let img_size = 22i32; data[off..off + 4].copy_from_slice(b"mhni");
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;
data[off..off + pixel_data.len()].copy_from_slice(pixel_data);
data
}
#[test]
fn try_parse_photodb_extracts_inline_mhni() {
let photodb = build_minimal_photodb_le();
let mut entries = Vec::new();
try_parse_photodb(&photodb, &mut entries).unwrap();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(entry.format_id, 1019);
assert_eq!(entry.ithmb_offset, 88);
let pixel_offset: usize = 12 + 16 + 12 + 12 + 36;
assert_eq!(entry.ithmb_offset as usize, pixel_offset);
assert_eq!(entry.image_size, 22);
assert_eq!(entry.width, 16);
assert_eq!(entry.height, 16);
assert_eq!(entry.data.len(), 22);
}
#[test]
fn try_parse_photodb_invalid_magic() {
let data = b"xxxx";
let mut entries = Vec::new();
let result = try_parse_photodb(data, &mut entries);
assert!(result.is_err());
}
#[test]
fn try_parse_photodb_empty() {
let data = b"";
let mut entries = Vec::new();
let result = try_parse_photodb(data, &mut entries);
assert!(result.is_err());
}
#[test]
fn try_parse_photodb_trims_jpeg_for_unknown_profile() {
let mut photodb = build_minimal_photodb_le();
let mhni_offset = 52usize;
photodb[mhni_offset + 16..mhni_offset + 20].copy_from_slice(&[0x0F, 0x27, 0, 0]);
let mut entries = Vec::new();
try_parse_photodb(&photodb, &mut entries).unwrap();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(entry.format_id, 9999);
assert_eq!(entry.data.len(), 22);
assert_eq!(&entry.data[..2], &[0xFF, 0xD8]);
assert_eq!(&entry.data[entry.data.len() - 2..], &[0xFF, 0xD9]);
}
#[test]
fn has_child_chunks_outside_end_range() {
let mut data = vec![0u8; 16];
data[0..4].copy_from_slice(b"mhsd");
data[4..8].copy_from_slice(&[16, 0, 0, 0]);
assert!(!has_child_chunks(&data, 0, 7, true));
}
#[test]
fn walk_entries_depth_limit_returns_early() {
let data = b"mhfd\x0c\x00\x00\x00\x00\x00\x00\x00";
let mut entries = Vec::new();
let result = walk_entries(data, 0, data.len(), true, &mut entries, MAX_DEPTH + 1);
assert!(result.is_ok());
assert!(entries.is_empty());
}
#[test]
fn walk_entries_empty_range() {
let data = b"";
let mut entries = Vec::new();
let result = walk_entries(data, 0, 0, true, &mut entries, 0);
assert!(result.is_ok());
assert!(entries.is_empty());
}
#[test]
fn get_format_id_name_known() {
let name = get_format_id_name(1007);
assert!(name.contains("1007"));
assert!(name.contains("480"));
assert!(name.contains("864"));
}
#[test]
fn get_format_id_name_unknown() {
let name = get_format_id_name(9999);
assert!(name.contains("unknown") || name.contains("9999"));
}
#[test]
fn trim_jpeg_finds_eoi() {
let mut data = vec![0xFF, 0xD8, 0x00, 0x00, 0xFF, 0xD9, 0xCC, 0xCC, 0xCC];
trim_jpeg(&mut data);
assert_eq!(data.len(), 6);
assert_eq!(&data[4..6], &[0xFF, 0xD9]);
}
#[test]
fn trim_jpeg_no_eoi() {
let mut data = vec![0xFF, 0xD8, 0x00, 0x00, 0x00, 0x00];
trim_jpeg(&mut data);
assert_eq!(data.len(), 6); }
#[test]
fn trim_jpeg_short_buffer() {
let mut data = vec![0xFF];
trim_jpeg(&mut data);
assert_eq!(data.len(), 1); }
}