use std::{fs::File, io::{Read, Write}}; use env_logger::Builder; use log::{error, info, LevelFilter}; use serde::{Deserialize, Serialize}; use xml::{XAlbumItem, XArgument, XDataSet, XDatabase, XPlaylist, XSomeList, XTrackItem}; mod xml; enum ChunkType { Database, DataSet, AlbumList, AlbumItem, TrackList, TrackItem, StringTypes, PlaylistList, Playlist, Unknown } impl From<[u8; 4]> for ChunkType { fn from(value: [u8; 4]) -> Self { match value { [0x6D, 0x68, 0x62, 0x64] => ChunkType::Database, [0x6D, 0x68, 0x73, 0x64] => ChunkType::DataSet, [0x6D, 0x68, 0x69, 0x61] => ChunkType::AlbumList, [0x6D, 0x68, 0x6C, 0x61] => ChunkType::AlbumItem, [0x6D, 0x68, 0x6C, 0x74] => ChunkType::TrackList, [0x6D, 0x68, 0x69, 0x74] => ChunkType::TrackItem, [0x6D, 0x68, 0x6F, 0x64] => ChunkType::StringTypes, [0x6D, 0x68, 0x6C, 0x70] => ChunkType::PlaylistList, [0x6D, 0x68, 0x79, 0x70] => ChunkType::Playlist, _ => ChunkType::Unknown } } } #[derive(Serialize, Deserialize, PartialEq, Debug)] struct ChunkHeader { chunk_type: [u8; 4], end_of_chunk: u32, children_count: u32 } #[derive(Serialize, Deserialize, PartialEq, Debug)] struct Database { unknown: u32, version: u32, children_count: u32, id: u64, unknown1: [u8; 32], language: u16, persistent_id: u64, hash: [u8; 20] } #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] struct DataSet { data_type: u32 } #[derive(Serialize, Deserialize, PartialEq, Debug)] struct AlbumItem { number_of_strings: u32, unknown: u16, album_id_for_track: u16, timestamp: u64, unknown1: u32 } #[derive(Serialize, Deserialize, PartialEq, Debug)] struct TrackItem { number_of_strings: u32, // number of mhod's count unique_id: u32, visible: u32, filetype: u32, type1: u8, type2: u8, compilation_flag: u8, stars: u8, last_modified_time: u32, size: u32, length: u32, track_number: u32, total_tracks: u32, year: u32, bitrate: u32, sample_rate: u32, volume: u32, start_time: u32, stop_time: u32, soundcheck: u32, play_count: u32, play_count2: u32, last_played_time: u32, disc_number: u32, total_discs: u32, userid: u32, date_added: u32, bookmark_time: u32, dbid: u64, checked: u8, application_rating: u8, bpm: u16, artwork_count: u16, unk9: u16, artwork_size: u32, unk11: u32, sample_rate2: u32, date_released: u32, unk14: u32, unk15: u32, unk16: u32, skip_count: u32, last_skipped: u32, has_artwork: u8, skip_when_shuffling: u8, remember_playback_position: u8, flag4: u8, dbid2: u64, lyrics_flag: u8, movie_file_flag: u8, played_mark: u8, unk17: u8, unk21: u32, pregap: u32, sample_count: u64, unk25: u32, postgap: u32, unk27: u32, media_type: u32, season_number: u32, episode_number: u32, unk31: [u8; 28], gapless_data: u32, unk38: u32, gapless_track_flag: u16, gapless_album_flag: u16, unk39_hash: [u8; 20], unk40: [u8; 18], album_id: u16, mhii_link: u32 } #[derive(Serialize, Deserialize, PartialEq, Debug)] struct StringEntry { // mhod entry_type: u32, unk1: u32, unk2: u32, position: u32, length: u32, unknown: u32, unk4: u32 } #[derive(Serialize, Deserialize, PartialEq, Debug)] struct PlaylistIndexEntry { // mhod entry_type: u32, unk1: u32, unk2: u32, index_type: u32, count: u32, null_padding: u64, null_padding1: u64, null_padding2: u64, null_padding3: u64, null_padding4: u64 } #[derive(Serialize, Deserialize, PartialEq, Debug)] struct LetterJumpEntry { entry_type: u32, unk1: u32, unk2: u32, index_type: u32, count: u32, null_padding: u64 } #[derive(Serialize, Deserialize, PartialEq, Debug)] struct JumpTable { letter: u32, // UTF-16 LE Uppercase with two padding null bytes entry_num: u32, // the number of the first entry in the corresponding MHOD52 index starting with this letter. Zero-based and incremented by one for each entry, not 4. count: u32 // the count of entries starting with this letter in the corresponding MHOD52. } #[derive(Serialize, Deserialize, PartialEq, Debug)] struct Playlist { data_object_child_count: u32, playlist_item_count: u32, is_master_playlist_flag: u8, unk: [u8; 3], timestamp: u32, persistent_playlist_id: u64, unk3: u32, string_mhod_count: u16, podcast_flag: u16, list_sort_order: u32 } enum ChunkState { Header, Data } pub fn parse_bytes(data: &[u8]) -> XDatabase { let mut xdb = XDatabase{data: None, children: Vec::new()}; let mut state = ChunkState::Header; let mut chunk_header: Option = None; let mut last_type: u32 = 0; let mut i = 0; while i < data.len() { state = match state { ChunkState::Header => { if i + 12 >= data.len() { break; } chunk_header = Some(bincode::deserialize(&data[i..i+12]).unwrap()); i += 12; ChunkState::Data }, ChunkState::Data => { let mut u = 0; let header = chunk_header.unwrap(); match ChunkType::from(header.chunk_type) { ChunkType::Database => { info!("Db header: {:?}", header); u = usize::try_from(header.end_of_chunk).unwrap() - 12; let db: Database = bincode::deserialize(&data[i..i+u]).unwrap(); info!("val: {:?}", db); xdb.data = Some(db); }, ChunkType::DataSet => { u = usize::try_from(header.end_of_chunk).unwrap() - 12; let ds: DataSet = bincode::deserialize(&data[i..i+u]).unwrap(); info!("val: {:?}", ds); xdb.children.push(XDataSet { data: ds.clone(), child: match ds.data_type { 3 => XSomeList::Playlists(Vec::new()), // Playlist List 4 => XSomeList::AlbumList(Vec::new()), // Album List _ => XSomeList::TrackList(Vec::new()) // 1 Track List }}); }, ChunkType::AlbumList => { info!("AlbumList"); u = usize::try_from(header.end_of_chunk).unwrap() - 12; last_type = 4; }, ChunkType::AlbumItem => { u = usize::try_from(header.end_of_chunk).unwrap() - 12; let ai: AlbumItem = bincode::deserialize(&data[i..i+u]).unwrap(); info!("val: {:?}", ai); if let XSomeList::AlbumList(albums) = &mut xdb.find_dataset(4).child { albums.push(XAlbumItem {data: ai,args: Vec::new()}); } }, ChunkType::TrackList => { info!("TrackList"); u = usize::try_from(header.end_of_chunk).unwrap() - 12; last_type = 1; }, ChunkType::TrackItem => { u = usize::try_from(header.end_of_chunk).unwrap() - 12; let ti: TrackItem = bincode::deserialize(&data[i..i+u]).unwrap(); info!("val: {:?}", ti); if let XSomeList::TrackList(tracks) = &mut xdb.find_dataset(1).child { tracks.push(XTrackItem {data: ti,args: Vec::new()}); } }, ChunkType::StringTypes => { u = usize::try_from(header.children_count).unwrap() - 12; let header_offset: usize = (header.end_of_chunk + 4) as usize; let entry_type = u32::from_le_bytes(data[i..i+4].try_into().unwrap()); match entry_type { 0..=15 => { let str_end: usize = (header.children_count - 12) as usize; let entry: StringEntry = bincode::deserialize(&data[i..i+28]).unwrap(); info!("val: {:?}", &entry); let mut bytes = Vec::new(); let mut h = i+header_offset; while h < i+str_end { if data[h] != 0 { bytes.push(data[h]); } h+=1; } let g = String::from_utf8(bytes).unwrap(); info!("str: {}", g); match &mut xdb.find_dataset(last_type).child { XSomeList::AlbumList(albums) => { albums.last_mut().unwrap().args.push(XArgument{ arg_type: entry_type, val: g}); }, XSomeList::Playlists(playlists) => { playlists.last_mut().unwrap().args.push(XArgument{ arg_type: entry_type, val: g}); }, XSomeList::TrackList(tracks) => { tracks.last_mut().unwrap().args.push(XArgument{ arg_type: entry_type, val: g}); } } }, 52 => { let entry: PlaylistIndexEntry = bincode::deserialize(&data[i..i+60]).unwrap(); info!("valPl: {:?}", &entry); let mut h = i+60; let mut v = Vec::new(); while h < i+60+((4*entry.count) as usize) { v.push(u32::from_le_bytes(data[h..h+4].try_into().unwrap())); h += 4; } info!("Indexes: {:?}", v); }, 53 => { let entry: LetterJumpEntry = bincode::deserialize(&data[i..i+28]).unwrap(); info!("valJT: {:?}", &entry); let mut h = i+28; let mut v: Vec = Vec::new(); while h < i+28+((12*entry.count) as usize) { v.push(bincode::deserialize(&data[h..h+12]).unwrap()); h += 12; } info!("Indexes: {:?}", v); }, 100 => { }, 102 => { }, _ => {} } }, ChunkType::PlaylistList => { info!("Playlists count: {}", header.children_count); u = usize::try_from(header.end_of_chunk).unwrap() - 12; last_type = 3; }, ChunkType::Playlist => { u = usize::try_from(header.end_of_chunk).unwrap() - 12; let playlist: Playlist = bincode::deserialize(&data[i..i+u]).unwrap(); info!("playlist: {:?}", playlist); if let XSomeList::Playlists(playlists) = &mut xdb.find_dataset(3).child { playlists.push(XPlaylist {data: playlist,args: Vec::new()}); } }, _ => { u = 1; } } i += u; chunk_header = None; ChunkState::Header } } } //let mut f = File::create("output.json").unwrap(); //let r = f.write(serde_json::to_string::(&xdb).unwrap().as_bytes()); //info!("Result: {:?}", r); xdb } /*fn main() { // Initialize the logger with 'info' as the default level Builder::new() .filter(None, LevelFilter::Info) .init(); let mut f = File::open("D:\\Documents\\iTunes\\iTunesDB").unwrap(); let mut buf = Vec::new(); match f.read_to_end(&mut buf) { Ok(n) => { let data = &buf[..n]; parse_bytes(data); }, Err(e) => { error!("Error: {}",e); } } }*/