1pub mod string;
22
23use crate::pdb::string::DeviceSQLString;
24use crate::util::ColorIndex;
25use binrw::{
26 binread, binrw,
27 file_ptr::FilePtrArgs,
28 io::{Read, Seek, SeekFrom, Write},
29 BinRead, BinResult, BinWrite, Endian, FilePtr16, FilePtr8,
30};
31
32fn current_offset<R: Read + Seek>(reader: &mut R, _: Endian, _: ()) -> BinResult<u64> {
34 reader.stream_position().map_err(binrw::Error::Io)
35}
36
37#[binrw]
39#[derive(Debug, PartialEq, Eq, Clone, Copy)]
40#[brw(little)]
41pub enum PageType {
42 #[brw(magic = 0u32)]
44 Tracks,
45 #[brw(magic = 1u32)]
47 Genres,
48 #[brw(magic = 2u32)]
50 Artists,
51 #[brw(magic = 3u32)]
53 Albums,
54 #[brw(magic = 4u32)]
56 Labels,
57 #[brw(magic = 5u32)]
59 Keys,
60 #[brw(magic = 6u32)]
62 Colors,
63 #[brw(magic = 7u32)]
66 PlaylistTree,
67 #[brw(magic = 8u32)]
69 PlaylistEntries,
70 #[brw(magic = 11u32)]
73 HistoryPlaylists,
74 #[brw(magic = 12u32)]
76 HistoryEntries,
77 #[brw(magic = 13u32)]
79 Artwork,
80 #[brw(magic = 16u32)]
82 Columns,
83 #[brw(magic = 19u32)]
85 History,
86 Unknown(u32),
88}
89
90#[binrw]
93#[derive(Clone, Debug, PartialEq, Eq, PartialOrd)]
94#[brw(little)]
95pub struct PageIndex(u32);
96
97impl PageIndex {
98 #[must_use]
100 pub fn offset(&self, page_size: u32) -> u64 {
101 u64::from(self.0) * u64::from(page_size)
102 }
103}
104
105#[binrw]
108#[derive(Debug, PartialEq, Eq, Clone)]
109#[brw(little)]
110pub struct Table {
111 pub page_type: PageType,
113 #[allow(dead_code)]
116 empty_candidate: u32,
117 pub first_page: PageIndex,
122 pub last_page: PageIndex,
124}
125
126#[binrw]
128#[derive(Debug, PartialEq, Eq, Clone)]
129#[brw(little)]
130pub struct Header {
131 #[br(temp, assert(unknown1 == 0))]
133 #[bw(calc = 0u32)]
134 unknown1: u32,
135 pub page_size: u32,
139 #[br(temp)]
141 #[bw(calc = tables.len().try_into().expect("too many tables"))]
142 num_tables: u32,
143 #[allow(dead_code)]
145 next_unused_page: PageIndex,
146 #[allow(dead_code)]
148 unknown: u32,
149 pub sequence: u32,
151 #[br(temp, assert(gap == 0))]
153 #[bw(calc = 0u32)]
154 gap: u32,
155 #[br(count = num_tables)]
157 pub tables: Vec<Table>,
158}
159
160impl Header {
161 pub fn read_pages<R: Read + Seek>(
163 &self,
164 reader: &mut R,
165 _: Endian,
166 args: (&PageIndex, &PageIndex),
167 ) -> BinResult<Vec<Page>> {
168 let endian = Endian::Little;
169 let (first_page, last_page) = args;
170
171 let mut pages = vec![];
172 let mut page_index = first_page.clone();
173 loop {
174 let page_offset = SeekFrom::Start(page_index.offset(self.page_size));
175 reader.seek(page_offset).map_err(binrw::Error::Io)?;
176 let page = Page::read_options(reader, endian, (self.page_size,))?;
177 let is_last_page = &page.page_index == last_page;
178 page_index = page.next_page.clone();
179 pages.push(page);
180
181 if is_last_page {
182 break;
183 }
184 }
185 Ok(pages)
186 }
187}
188
189#[binrw]
190#[derive(Debug, PartialEq, Eq, Clone, Copy)]
191struct PageFlags(u8);
192
193impl PageFlags {
194 #[must_use]
195 pub fn page_has_data(&self) -> bool {
196 (self.0 & 0x40) == 0
197 }
198}
199
200#[binread]
209#[derive(Debug, PartialEq)]
210#[br(little, magic = 0u32)]
211#[br(import(page_size: u32))]
212pub struct Page {
213 pub page_index: PageIndex,
217 pub page_type: PageType,
221 pub next_page: PageIndex,
226 #[allow(dead_code)]
228 unknown1: u32,
229 #[allow(dead_code)]
231 unknown2: u32,
232 pub num_rows_small: u8,
237 #[allow(dead_code)]
242 unknown3: u8,
243 #[allow(dead_code)]
248 unknown4: u8,
249 page_flags: PageFlags,
254 pub free_size: u16,
256 pub used_size: u16,
258 #[allow(dead_code)]
263 unknown5: u16,
264 pub num_rows_large: u16,
269 #[allow(dead_code)]
271 unknown6: u16,
272 #[allow(dead_code)]
277 unknown7: u16,
278 #[br(temp)]
282 #[br(calc = if num_rows_large > num_rows_small.into() && num_rows_large != 0x1fff { num_rows_large } else { num_rows_small.into() })]
283 num_rows: u16,
284 #[br(temp)]
288 #[br(calc = page_index.offset(page_size) + u64::from(Self::HEADER_SIZE))]
289 page_heap_offset: u64,
290 #[br(seek_before(SeekFrom::Current(i64::from(page_size) - i64::from(Self::HEADER_SIZE))), restore_position)]
292 #[br(parse_with = Self::parse_row_groups, args(page_type, page_heap_offset, num_rows, page_flags))]
293 pub row_groups: Vec<RowGroup>,
294}
295
296impl Page {
297 pub const HEADER_SIZE: u32 = 0x28;
299
300 fn parse_row_groups<R: Read + Seek>(
302 reader: &mut R,
303 _: Endian,
304 args: (PageType, u64, u16, PageFlags),
305 ) -> BinResult<Vec<RowGroup>> {
306 let endian = Endian::Little;
307
308 let (page_type, page_heap_offset, num_rows, page_flags) = args;
309 if num_rows == 0 || !page_flags.page_has_data() {
310 return Ok(vec![]);
311 }
312
313 let stream_position = reader.stream_position()?;
314
315 let estimated_number_of_row_groups =
317 usize::from(num_rows).div_ceil(RowGroup::MAX_ROW_COUNT);
318 let mut row_groups = Vec::with_capacity(estimated_number_of_row_groups);
319 for i in 0..estimated_number_of_row_groups {
320 reader.seek(
321 u64::try_from(row_groups.len())
322 .ok()
323 .and_then(|index| index.checked_mul(36))
324 .and_then(|x| stream_position.checked_sub(x))
325 .map(SeekFrom::Start)
326 .ok_or_else(|| binrw::Error::AssertFail {
327 pos: stream_position,
328 message: format!("Failed to calculate seek position for row group {}", i),
329 })?,
330 )?;
331 let row_group = RowGroup::read_options(reader, endian, (page_type, page_heap_offset))?;
332 row_groups.insert(0, row_group);
333 }
334
335 Ok(row_groups)
336 }
337
338 #[must_use]
339 pub fn has_data(&self) -> bool {
341 self.page_flags.page_has_data()
342 }
343
344 #[must_use]
345 pub fn num_rows(&self) -> u16 {
349 if self.num_rows_large > self.num_rows_small.into() && self.num_rows_large != 0x1fff {
350 self.num_rows_large
351 } else {
352 self.num_rows_small.into()
353 }
354 }
355}
356
357#[derive(Debug, PartialEq)]
361pub struct RowGroup {
362 rows: [Option<FilePtr16<Row>>; Self::MAX_ROW_COUNT],
366 row_presence_flags: u16,
367 unknown: u16,
371}
372
373impl RowGroup {
374 const MAX_ROW_COUNT: usize = 16;
375
376 pub fn present_rows(&self) -> impl Iterator<Item = Row> + '_ {
378 self.rows
379 .iter()
380 .rev()
381 .filter_map(|row_offset| row_offset.as_ref().map(|r| r.value.clone()))
382 }
383}
384
385impl BinRead for RowGroup {
386 type Args<'a> = (PageType, u64);
387
388 fn read_options<R: Read + Seek>(
393 reader: &mut R,
394 endian: Endian,
395 (page_type, page_heap_offset): Self::Args<'_>,
396 ) -> BinResult<Self> {
397 let row_group_end_position = reader.stream_position()?;
398 reader.seek(SeekFrom::Current(-4))?;
399 let row_presence_flags = u16::read_options(reader, endian, ())?;
400 let unknown = u16::read_options(reader, endian, ())?;
401 debug_assert!(row_group_end_position == reader.stream_position()?);
402
403 const MISSING_ROW: Option<FilePtr16<Row>> = None;
404
405 let mut rows: [Option<FilePtr16<Row>>; Self::MAX_ROW_COUNT] =
406 [MISSING_ROW; Self::MAX_ROW_COUNT];
407 if row_presence_flags.count_ones() == 0 {
408 return Ok(RowGroup {
409 rows,
410 row_presence_flags,
411 unknown,
412 });
413 }
414
415 let mut needs_seek = true;
417 for i in (0..RowGroup::MAX_ROW_COUNT).rev() {
418 let row_present = row_presence_flags & (1 << i) != 0;
419 if row_present {
420 if needs_seek {
421 let index = u64::try_from(i).map_err(|_| binrw::Error::AssertFail {
422 pos: row_group_end_position,
423 message: format!("Failed to calculate row index {}", i),
424 })?;
425 reader.seek(SeekFrom::Start(
426 row_group_end_position - 4 - 2 * (index + 1),
427 ))?;
428 }
429 let row = FilePtr16::read_options(
430 reader,
431 endian,
432 FilePtrArgs {
433 offset: page_heap_offset,
434 inner: (page_type,),
435 },
436 )?;
437 rows[i] = Some(row);
438 }
439 needs_seek = !row_present;
440 }
441
442 reader.seek(SeekFrom::Start(row_group_end_position))?;
443
444 Ok(RowGroup {
445 rows,
446 row_presence_flags,
447 unknown,
448 })
449 }
450}
451
452#[binrw]
454#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
455#[brw(little)]
456pub struct TrackId(pub u32);
457
458#[binrw]
460#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
461#[brw(little)]
462pub struct ArtworkId(pub u32);
463
464#[binrw]
466#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
467#[brw(little)]
468pub struct AlbumId(pub u32);
469
470#[binrw]
472#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
473#[brw(little)]
474pub struct ArtistId(pub u32);
475
476#[binrw]
478#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
479#[brw(little)]
480pub struct GenreId(pub u32);
481
482#[binrw]
484#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
485#[brw(little)]
486pub struct KeyId(pub u32);
487
488#[binrw]
490#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
491#[brw(little)]
492pub struct LabelId(pub u32);
493
494#[binrw]
496#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
497#[brw(little)]
498pub struct PlaylistTreeNodeId(pub u32);
499
500#[binrw]
502#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
503#[brw(little)]
504pub struct HistoryPlaylistId(pub u32);
505
506#[binrw]
508#[derive(Debug, PartialEq, Eq, Clone)]
509#[brw(little)]
510pub struct Album {
511 #[br(temp, parse_with = current_offset)]
515 #[bw(ignore)]
516 base_offset: u64,
517 unknown1: u16,
519 index_shift: u16,
521 unknown2: u32,
523 artist_id: ArtistId,
525 id: AlbumId,
527 unknown3: u32,
529 unknown4: u8,
531 #[br(offset = base_offset, parse_with = FilePtr8::parse)]
533 name: DeviceSQLString,
534}
535
536#[binrw]
538#[derive(Debug, PartialEq, Eq, Clone)]
539#[brw(little)]
540pub struct Artist {
541 subtype: u16,
543 index_shift: u16,
545 id: ArtistId,
547 unknown1: u8,
549 ofs_name_near: u8,
551 #[br(if(subtype == 0x64))]
555 ofs_name_far: Option<u16>,
556 #[br(seek_before = Artist::calculate_name_seek(ofs_name_near, &ofs_name_far))]
558 #[bw(seek_before = Artist::calculate_name_seek(*ofs_name_near, ofs_name_far))]
559 #[brw(restore_position)]
560 name: DeviceSQLString,
561}
562
563impl Artist {
564 fn calculate_name_seek(ofs_near: u8, ofs_far: &Option<u16>) -> SeekFrom {
565 let offset: u16 = ofs_far.map_or_else(|| ofs_near.into(), |v| v - 2) - 10;
566 SeekFrom::Current(offset.into())
567 }
568}
569
570#[binrw]
572#[derive(Debug, PartialEq, Eq, Clone)]
573#[brw(little)]
574pub struct Artwork {
575 id: ArtworkId,
577 path: DeviceSQLString,
579}
580
581#[binrw]
583#[derive(Debug, PartialEq, Eq, Clone)]
584#[brw(little)]
585pub struct Color {
586 unknown1: u32,
588 unknown2: u8,
590 color: ColorIndex,
592 unknown3: u16,
594 name: DeviceSQLString,
596}
597
598#[binrw]
600#[derive(Debug, PartialEq, Eq, Clone)]
601#[brw(little)]
602pub struct Genre {
603 id: GenreId,
605 name: DeviceSQLString,
607}
608
609#[binrw]
611#[derive(Debug, PartialEq, Eq, Clone)]
612#[brw(little)]
613pub struct HistoryPlaylist {
614 id: HistoryPlaylistId,
616 name: DeviceSQLString,
618}
619
620#[binrw]
622#[derive(Debug, PartialEq, Eq, Clone)]
623#[brw(little)]
624pub struct HistoryEntry {
625 track_id: TrackId,
627 playlist_id: HistoryPlaylistId,
629 entry_index: u32,
631}
632
633#[binrw]
635#[derive(Debug, PartialEq, Eq, Clone)]
636#[brw(little)]
637pub struct Key {
638 id: KeyId,
640 id2: u32,
642 name: DeviceSQLString,
644}
645
646#[binrw]
648#[derive(Debug, PartialEq, Eq, Clone)]
649#[brw(little)]
650pub struct Label {
651 id: LabelId,
653 name: DeviceSQLString,
655}
656
657#[binrw]
659#[derive(Debug, PartialEq, Eq, Clone)]
660#[brw(little)]
661pub struct PlaylistTreeNode {
662 pub parent_id: PlaylistTreeNodeId,
664 unknown: u32,
666 sort_order: u32,
668 pub id: PlaylistTreeNodeId,
670 node_is_folder: u32,
672 pub name: DeviceSQLString,
674}
675
676impl PlaylistTreeNode {
677 #[must_use]
679 pub fn is_folder(&self) -> bool {
680 self.node_is_folder > 0
681 }
682}
683
684#[binrw]
686#[derive(Debug, PartialEq, Eq, Clone)]
687#[brw(little)]
688pub struct PlaylistEntry {
689 entry_index: u32,
691 track_id: TrackId,
693 playlist_id: PlaylistTreeNodeId,
695}
696
697#[binrw]
700#[derive(Debug, PartialEq, Eq, Clone)]
701#[brw(little)]
702pub struct ColumnEntry {
703 id: u16,
708 unknown0: u16,
711 pub column_name: DeviceSQLString,
718}
719
720#[binread]
722#[derive(Debug, PartialEq, Eq, Clone)]
723#[br(little)]
724pub struct Track {
725 #[br(temp, parse_with = current_offset)]
729 base_offset: u64,
730 unknown1: u16,
732 index_shift: u16,
734 bitmask: u32,
736 sample_rate: u32,
738 composer_id: ArtistId,
740 file_size: u32,
742 unknown2: u32,
744 unknown3: u16,
746 unknown4: u16,
748 artwork_id: ArtworkId,
750 key_id: KeyId,
752 orig_artist_id: ArtistId,
754 label_id: LabelId,
756 remixer_id: ArtistId,
758 bitrate: u32,
760 track_number: u32,
762 tempo: u32,
764 genre_id: GenreId,
766 album_id: AlbumId,
768 artist_id: ArtistId,
770 id: TrackId,
772 disc_number: u16,
774 play_count: u16,
776 year: u16,
778 sample_depth: u16,
780 duration: u16,
782 unknown5: u16,
784 color: ColorIndex,
786 rating: u8,
788 unknown6: u16,
790 unknown7: u16,
792 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
794 isrc: DeviceSQLString,
795 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
797 unknown_string1: DeviceSQLString,
798 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
800 unknown_string2: DeviceSQLString,
801 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
803 unknown_string3: DeviceSQLString,
804 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
806 unknown_string4: DeviceSQLString,
807 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
809 message: DeviceSQLString,
810 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
812 kuvo_public: DeviceSQLString,
813 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
815 autoload_hotcues: DeviceSQLString,
816 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
818 unknown_string5: DeviceSQLString,
819 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
821 unknown_string6: DeviceSQLString,
822 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
824 date_added: DeviceSQLString,
825 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
827 release_date: DeviceSQLString,
828 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
830 mix_name: DeviceSQLString,
831 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
833 unknown_string7: DeviceSQLString,
834 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
836 analyze_path: DeviceSQLString,
837 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
839 analyze_date: DeviceSQLString,
840 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
842 comment: DeviceSQLString,
843 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
845 title: DeviceSQLString,
846 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
848 unknown_string8: DeviceSQLString,
849 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
851 filename: DeviceSQLString,
852 #[br(offset = base_offset, parse_with = FilePtr16::parse)]
854 file_path: DeviceSQLString,
855}
856
857impl binrw::meta::WriteEndian for Track {
860 const ENDIAN: binrw::meta::EndianKind = binrw::meta::EndianKind::Endian(Endian::Little);
861}
862
863impl BinWrite for Track {
864 type Args<'a> = ();
865
866 fn write_options<W: Write + Seek>(
867 &self,
868 writer: &mut W,
869 endian: Endian,
870 _args: Self::Args<'_>,
871 ) -> BinResult<()> {
872 debug_assert!(endian == Endian::Little);
873
874 let base_position = writer.stream_position()?;
875 self.unknown1.write_options(writer, endian, ())?;
876 self.index_shift.write_options(writer, endian, ())?;
877 self.bitmask.write_options(writer, endian, ())?;
878 self.sample_rate.write_options(writer, endian, ())?;
879 self.composer_id.write_options(writer, endian, ())?;
880 self.file_size.write_options(writer, endian, ())?;
881 self.unknown2.write_options(writer, endian, ())?;
882 self.unknown3.write_options(writer, endian, ())?;
883 self.unknown4.write_options(writer, endian, ())?;
884 self.artwork_id.write_options(writer, endian, ())?;
885 self.key_id.write_options(writer, endian, ())?;
886 self.orig_artist_id.write_options(writer, endian, ())?;
887 self.label_id.write_options(writer, endian, ())?;
888 self.remixer_id.write_options(writer, endian, ())?;
889 self.bitrate.write_options(writer, endian, ())?;
890 self.track_number.write_options(writer, endian, ())?;
891 self.tempo.write_options(writer, endian, ())?;
892 self.genre_id.write_options(writer, endian, ())?;
893 self.album_id.write_options(writer, endian, ())?;
894 self.artist_id.write_options(writer, endian, ())?;
895 self.id.write_options(writer, endian, ())?;
896 self.disc_number.write_options(writer, endian, ())?;
897 self.play_count.write_options(writer, endian, ())?;
898 self.year.write_options(writer, endian, ())?;
899 self.sample_depth.write_options(writer, endian, ())?;
900 self.duration.write_options(writer, endian, ())?;
901 self.unknown5.write_options(writer, endian, ())?;
902 self.color.write_options(writer, endian, ())?;
903 self.rating.write_options(writer, endian, ())?;
904 self.unknown6.write_options(writer, endian, ())?;
905 self.unknown7.write_options(writer, endian, ())?;
906
907 let start_of_string_section = writer.stream_position()?;
908 debug_assert_eq!(start_of_string_section - base_position, 0x5e);
909
910 let mut string_offsets = [0u16; 21];
912 writer.seek(SeekFrom::Current(0x2a))?;
913 for (i, string) in [
914 &self.isrc,
915 &self.unknown_string1,
916 &self.unknown_string2,
917 &self.unknown_string3,
918 &self.unknown_string4,
919 &self.message,
920 &self.kuvo_public,
921 &self.autoload_hotcues,
922 &self.unknown_string5,
923 &self.unknown_string6,
924 &self.date_added,
925 &self.release_date,
926 &self.mix_name,
927 &self.unknown_string7,
928 &self.analyze_path,
929 &self.analyze_date,
930 &self.comment,
931 &self.title,
932 &self.unknown_string8,
933 &self.filename,
934 &self.file_path,
935 ]
936 .into_iter()
937 .enumerate()
938 {
939 let current_position = writer.stream_position()?;
940 let offset: u16 = current_position
941 .checked_sub(base_position)
942 .and_then(|v| u16::try_from(v).ok())
943 .ok_or_else(|| binrw::Error::AssertFail {
944 pos: current_position,
945 message: "Wraparound while calculating row offset".to_string(),
946 })?;
947 string_offsets[i] = offset;
948 string.write_options(writer, endian, ())?;
949 }
950
951 let end_of_row = writer.stream_position()?;
952 writer.seek(SeekFrom::Start(start_of_string_section))?;
953 string_offsets.write_options(writer, endian, ())?;
954 writer.seek(SeekFrom::Start(end_of_row))?;
955
956 Ok(())
957 }
958}
959
960#[binrw]
962#[derive(Debug, PartialEq, Eq, Clone)]
963#[brw(little)]
964#[br(import(page_type: PageType))]
965#[allow(clippy::large_enum_variant)]
970pub enum Row {
971 #[br(pre_assert(page_type == PageType::Albums))]
973 Album(Album),
974 #[br(pre_assert(page_type == PageType::Artists))]
976 Artist(Artist),
977 #[br(pre_assert(page_type == PageType::Artwork))]
979 Artwork(Artwork),
980 #[br(pre_assert(page_type == PageType::Colors))]
982 Color(Color),
983 #[br(pre_assert(page_type == PageType::Genres))]
985 Genre(Genre),
986 #[br(pre_assert(page_type == PageType::HistoryPlaylists))]
988 HistoryPlaylist(HistoryPlaylist),
989 #[br(pre_assert(page_type == PageType::HistoryEntries))]
991 HistoryEntry(HistoryEntry),
992 #[br(pre_assert(page_type == PageType::Keys))]
994 Key(Key),
995 #[br(pre_assert(page_type == PageType::Labels))]
997 Label(Label),
998 #[br(pre_assert(page_type == PageType::PlaylistTree))]
1000 PlaylistTreeNode(PlaylistTreeNode),
1001 #[br(pre_assert(page_type == PageType::PlaylistEntries))]
1003 PlaylistEntry(PlaylistEntry),
1004 #[br(pre_assert(page_type == PageType::Columns))]
1006 ColumnEntry(ColumnEntry),
1007 #[br(pre_assert(page_type == PageType::Tracks))]
1009 Track(Track),
1010 #[br(pre_assert(matches!(page_type, PageType::History | PageType::Unknown(_))))]
1012 Unknown,
1013}
1014
1015#[cfg(test)]
1016mod test {
1017 use super::*;
1018 use crate::util::testing::test_roundtrip;
1019
1020 #[test]
1021 fn empty_header() {
1022 let header = Header {
1023 page_size: 4096,
1024 next_unused_page: PageIndex(1),
1025 unknown: 0,
1026 sequence: 1,
1027 tables: vec![],
1028 };
1029 test_roundtrip(
1030 &[
1031 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
1032 ],
1033 header,
1034 );
1035 }
1036
1037 #[test]
1038 fn demo_tracks_header() {
1039 let header = Header {
1040 page_size: 4096,
1041 next_unused_page: PageIndex(51),
1042 unknown: 5,
1043 sequence: 34,
1044 tables: [
1045 Table {
1046 page_type: PageType::Tracks,
1047 empty_candidate: 47,
1048 first_page: PageIndex(1),
1049 last_page: PageIndex(2),
1050 },
1051 Table {
1052 page_type: PageType::Genres,
1053 empty_candidate: 4,
1054 first_page: PageIndex(3),
1055 last_page: PageIndex(3),
1056 },
1057 Table {
1058 page_type: PageType::Artists,
1059 empty_candidate: 49,
1060 first_page: PageIndex(5),
1061 last_page: PageIndex(6),
1062 },
1063 Table {
1064 page_type: PageType::Albums,
1065 empty_candidate: 8,
1066 first_page: PageIndex(7),
1067 last_page: PageIndex(7),
1068 },
1069 Table {
1070 page_type: PageType::Labels,
1071 empty_candidate: 50,
1072 first_page: PageIndex(9),
1073 last_page: PageIndex(10),
1074 },
1075 Table {
1076 page_type: PageType::Keys,
1077 empty_candidate: 46,
1078 first_page: PageIndex(11),
1079 last_page: PageIndex(12),
1080 },
1081 Table {
1082 page_type: PageType::Colors,
1083 empty_candidate: 42,
1084 first_page: PageIndex(13),
1085 last_page: PageIndex(14),
1086 },
1087 Table {
1088 page_type: PageType::PlaylistTree,
1089 empty_candidate: 16,
1090 first_page: PageIndex(15),
1091 last_page: PageIndex(15),
1092 },
1093 Table {
1094 page_type: PageType::PlaylistEntries,
1095 empty_candidate: 18,
1096 first_page: PageIndex(17),
1097 last_page: PageIndex(17),
1098 },
1099 Table {
1100 page_type: PageType::Unknown(9),
1101 empty_candidate: 20,
1102 first_page: PageIndex(19),
1103 last_page: PageIndex(19),
1104 },
1105 Table {
1106 page_type: PageType::Unknown(10),
1107 empty_candidate: 22,
1108 first_page: PageIndex(21),
1109 last_page: PageIndex(21),
1110 },
1111 Table {
1112 page_type: PageType::HistoryPlaylists,
1113 empty_candidate: 24,
1114 first_page: PageIndex(23),
1115 last_page: PageIndex(23),
1116 },
1117 Table {
1118 page_type: PageType::HistoryEntries,
1119 empty_candidate: 26,
1120 first_page: PageIndex(25),
1121 last_page: PageIndex(25),
1122 },
1123 Table {
1124 page_type: PageType::Artwork,
1125 empty_candidate: 28,
1126 first_page: PageIndex(27),
1127 last_page: PageIndex(27),
1128 },
1129 Table {
1130 page_type: PageType::Unknown(14),
1131 empty_candidate: 30,
1132 first_page: PageIndex(29),
1133 last_page: PageIndex(29),
1134 },
1135 Table {
1136 page_type: PageType::Unknown(15),
1137 empty_candidate: 32,
1138 first_page: PageIndex(31),
1139 last_page: PageIndex(31),
1140 },
1141 Table {
1142 page_type: PageType::Columns,
1143 empty_candidate: 43,
1144 first_page: PageIndex(33),
1145 last_page: PageIndex(34),
1146 },
1147 Table {
1148 page_type: PageType::Unknown(17),
1149 empty_candidate: 44,
1150 first_page: PageIndex(35),
1151 last_page: PageIndex(36),
1152 },
1153 Table {
1154 page_type: PageType::Unknown(18),
1155 empty_candidate: 45,
1156 first_page: PageIndex(37),
1157 last_page: PageIndex(38),
1158 },
1159 Table {
1160 page_type: PageType::History,
1161 empty_candidate: 48,
1162 first_page: PageIndex(39),
1163 last_page: PageIndex(41),
1164 },
1165 ]
1166 .to_vec(),
1167 };
1168
1169 test_roundtrip(
1170 &[
1171 0, 0, 0, 0, 0, 16, 0, 0, 20, 0, 0, 0, 51, 0, 0, 0, 5, 0, 0, 0, 34, 0, 0, 0, 0, 0,
1172 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 3,
1173 0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 49, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 3, 0, 0, 0,
1174 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 50, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0,
1175 0, 5, 0, 0, 0, 46, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 6, 0, 0, 0, 42, 0, 0, 0, 13,
1176 0, 0, 0, 14, 0, 0, 0, 7, 0, 0, 0, 16, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 8, 0, 0,
1177 0, 18, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 9, 0, 0, 0, 20, 0, 0, 0, 19, 0, 0, 0, 19,
1178 0, 0, 0, 10, 0, 0, 0, 22, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 11, 0, 0, 0, 24, 0, 0,
1179 0, 23, 0, 0, 0, 23, 0, 0, 0, 12, 0, 0, 0, 26, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0,
1180 13, 0, 0, 0, 28, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 14, 0, 0, 0, 30, 0, 0, 0, 29,
1181 0, 0, 0, 29, 0, 0, 0, 15, 0, 0, 0, 32, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 16, 0, 0,
1182 0, 43, 0, 0, 0, 33, 0, 0, 0, 34, 0, 0, 0, 17, 0, 0, 0, 44, 0, 0, 0, 35, 0, 0, 0,
1183 36, 0, 0, 0, 18, 0, 0, 0, 45, 0, 0, 0, 37, 0, 0, 0, 38, 0, 0, 0, 19, 0, 0, 0, 48,
1184 0, 0, 0, 39, 0, 0, 0, 41, 0, 0, 0,
1185 ],
1186 header,
1187 );
1188 }
1189
1190 #[test]
1191 fn track_row() {
1192 let row = Track {
1193 unknown1: 36,
1194 index_shift: 160,
1195 bitmask: 788224,
1196 sample_rate: 44100,
1197 composer_id: ArtistId(0),
1198 file_size: 6899624,
1199 unknown2: 214020570,
1200 unknown3: 64128,
1201 unknown4: 1511,
1202 artwork_id: ArtworkId(0),
1203 key_id: KeyId(5),
1204 orig_artist_id: ArtistId(0),
1205 label_id: LabelId(1),
1206 remixer_id: ArtistId(0),
1207 bitrate: 320,
1208 track_number: 0,
1209 tempo: 12800,
1210 genre_id: GenreId(0),
1211 album_id: AlbumId(0),
1212 artist_id: ArtistId(1),
1213 id: TrackId(1),
1214 disc_number: 0,
1215 play_count: 0,
1216 year: 0,
1217 sample_depth: 16,
1218 duration: 172,
1219 unknown5: 41,
1220 color: ColorIndex::None,
1221 rating: 0,
1222 unknown6: 1,
1223 unknown7: 3,
1224 isrc: DeviceSQLString::new_isrc("".to_string()).unwrap(),
1225 unknown_string1: DeviceSQLString::empty(),
1226 unknown_string2: DeviceSQLString::new("3".to_string()).unwrap(),
1227 unknown_string3: DeviceSQLString::new("3".to_string()).unwrap(),
1228 unknown_string4: DeviceSQLString::empty(),
1229 message: DeviceSQLString::empty(),
1230 kuvo_public: DeviceSQLString::empty(),
1231 autoload_hotcues: DeviceSQLString::new("ON".to_string()).unwrap(),
1232 unknown_string5: DeviceSQLString::empty(),
1233 unknown_string6: DeviceSQLString::empty(),
1234 date_added: DeviceSQLString::new("2018-05-25".to_string()).unwrap(),
1235 release_date: DeviceSQLString::empty(),
1236 mix_name: DeviceSQLString::empty(),
1237 unknown_string7: DeviceSQLString::empty(),
1238 analyze_path: DeviceSQLString::new(
1239 "/PIONEER/USBANLZ/P016/0000875E/ANLZ0000.DAT".to_string(),
1240 )
1241 .unwrap(),
1242 analyze_date: DeviceSQLString::new("2022-02-02".to_string()).unwrap(),
1243 comment: DeviceSQLString::new("Tracks by www.loopmasters.com".to_string()).unwrap(),
1244 title: DeviceSQLString::new("Demo Track 1".to_string()).unwrap(),
1245 unknown_string8: DeviceSQLString::empty(),
1246 filename: DeviceSQLString::new("Demo Track 1.mp3".to_string()).unwrap(),
1247 file_path: DeviceSQLString::new(
1248 "/Contents/Loopmasters/UnknownAlbum/Demo Track 1.mp3".to_string(),
1249 )
1250 .unwrap(),
1251 };
1252 test_roundtrip(
1253 &[
1254 36, 0, 160, 0, 0, 7, 12, 0, 68, 172, 0, 0, 0, 0, 0, 0, 168, 71, 105, 0, 218, 177,
1255 193, 12, 128, 250, 231, 5, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
1256 0, 64, 1, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0,
1257 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 172, 0, 41, 0, 0, 0, 1, 0, 3, 0, 136, 0, 137, 0,
1258 138, 0, 140, 0, 142, 0, 143, 0, 144, 0, 145, 0, 148, 0, 149, 0, 150, 0, 161, 0,
1259 162, 0, 163, 0, 164, 0, 208, 0, 219, 0, 249, 0, 6, 1, 7, 1, 24, 1, 3, 3, 5, 51, 5,
1260 51, 3, 3, 3, 7, 79, 78, 3, 3, 23, 50, 48, 49, 56, 45, 48, 53, 45, 50, 53, 3, 3, 3,
1261 89, 47, 80, 73, 79, 78, 69, 69, 82, 47, 85, 83, 66, 65, 78, 76, 90, 47, 80, 48, 49,
1262 54, 47, 48, 48, 48, 48, 56, 55, 53, 69, 47, 65, 78, 76, 90, 48, 48, 48, 48, 46, 68,
1263 65, 84, 23, 50, 48, 50, 50, 45, 48, 50, 45, 48, 50, 61, 84, 114, 97, 99, 107, 115,
1264 32, 98, 121, 32, 119, 119, 119, 46, 108, 111, 111, 112, 109, 97, 115, 116, 101,
1265 114, 115, 46, 99, 111, 109, 27, 68, 101, 109, 111, 32, 84, 114, 97, 99, 107, 32,
1266 49, 3, 35, 68, 101, 109, 111, 32, 84, 114, 97, 99, 107, 32, 49, 46, 109, 112, 51,
1267 105, 47, 67, 111, 110, 116, 101, 110, 116, 115, 47, 76, 111, 111, 112, 109, 97,
1268 115, 116, 101, 114, 115, 47, 85, 110, 107, 110, 111, 119, 110, 65, 108, 98, 117,
1269 109, 47, 68, 101, 109, 111, 32, 84, 114, 97, 99, 107, 32, 49, 46, 109, 112, 51,
1270 ],
1271 row,
1272 );
1273 }
1274
1275 #[test]
1276 fn artist_row() {
1277 let row = Artist {
1278 subtype: 96,
1279 index_shift: 32,
1280 id: ArtistId(1),
1281 unknown1: 3,
1282 ofs_name_near: 10,
1283 ofs_name_far: None,
1284 name: DeviceSQLString::new("Loopmasters".to_string()).unwrap(),
1285 };
1286 test_roundtrip(
1287 &[
1288 96, 0, 32, 0, 1, 0, 0, 0, 3, 10, 25, 76, 111, 111, 112, 109, 97, 115, 116, 101,
1289 114, 115,
1290 ],
1291 row,
1292 );
1293 }
1294
1295 #[test]
1296 fn label_row() {
1297 let row = Label {
1298 id: LabelId(1),
1299 name: DeviceSQLString::new("Loopmasters".to_string()).unwrap(),
1300 };
1301 test_roundtrip(
1302 &[
1303 1, 0, 0, 0, 25, 76, 111, 111, 112, 109, 97, 115, 116, 101, 114, 115,
1304 ],
1305 row,
1306 );
1307 }
1308
1309 #[test]
1310 fn key_row() {
1311 let row = Key {
1312 id: KeyId(1),
1313 id2: 1,
1314 name: DeviceSQLString::new("Dm".to_string()).unwrap(),
1315 };
1316 test_roundtrip(&[1, 0, 0, 0, 1, 0, 0, 0, 7, 68, 109], row);
1317 }
1318
1319 #[test]
1320 fn color_row() {
1321 let row = Color {
1322 unknown1: 0,
1323 unknown2: 1,
1324 color: ColorIndex::Pink,
1325 unknown3: 0,
1326 name: DeviceSQLString::new("Pink".to_string()).unwrap(),
1327 };
1328 test_roundtrip(&[0, 0, 0, 0, 1, 1, 0, 0, 11, 80, 105, 110, 107], row);
1329 }
1330 #[test]
1331 fn column_entry() {
1332 let row = ColumnEntry {
1333 id: 1,
1334 unknown0: 128,
1335 column_name: DeviceSQLString::new("\u{fffa}GENRE\u{fffb}".into()).unwrap(),
1336 };
1337 let bin = &[
1338 0x01, 0x00, 0x80, 0x00, 0x90, 0x12, 0x00, 0x00, 0xfa, 0xff, 0x47, 0x00, 0x45, 0x00,
1339 0x4e, 0x00, 0x52, 0x00, 0x45, 0x00, 0xfb, 0xff,
1340 ];
1341 test_roundtrip(bin, row);
1342 }
1343}