rekordcrate/pdb/
mod.rs

1// Copyright (c) 2025 Jan Holthuis <jan.holthuis@rub.de>
2//
3// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy
4// of the MPL was not distributed with this file, You can obtain one at
5// http://mozilla.org/MPL/2.0/.
6//
7// SPDX-License-Identifier: MPL-2.0
8
9//! Parser for Pioneer DeviceSQL database exports (PDB).
10//!
11//! The Rekordbox DJ software uses writes PDB files to `/PIONEER/rekordbox/export.pdb`.
12//!
13//! Most of the file format has been reverse-engineered by Henry Betts, Fabian Lesniak and James
14//! Elliott.
15//!
16//! - <https://github.com/Deep-Symmetry/crate-digger/blob/master/doc/Analysis.pdf>
17//! - <https://djl-analysis.deepsymmetry.org/rekordbox-export-analysis/exports.html>
18//! - <https://github.com/henrybetts/Rekordbox-Decoding>
19//! - <https://github.com/flesniak/python-prodj-link/tree/master/prodj/pdblib>
20
21pub 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
32/// Do not read anything, but the return the current stream position of `reader`.
33fn current_offset<R: Read + Seek>(reader: &mut R, _: Endian, _: ()) -> BinResult<u64> {
34    reader.stream_position().map_err(binrw::Error::Io)
35}
36
37/// The type of pages found inside a `Table`.
38#[binrw]
39#[derive(Debug, PartialEq, Eq, Clone, Copy)]
40#[brw(little)]
41pub enum PageType {
42    /// Holds rows of track metadata, such as title, artist, genre, artwork ID, playing time, etc.
43    #[brw(magic = 0u32)]
44    Tracks,
45    /// Holds rows of musical genres, for reference by tracks and searching.
46    #[brw(magic = 1u32)]
47    Genres,
48    /// Holds rows of artists, for reference by tracks and searching.
49    #[brw(magic = 2u32)]
50    Artists,
51    /// Holds rows of albums, for reference by tracks and searching.
52    #[brw(magic = 3u32)]
53    Albums,
54    /// Holds rows of music labels, for reference by tracks and searching.
55    #[brw(magic = 4u32)]
56    Labels,
57    /// Holds rows of musical keys, for reference by tracks, searching, and key matching.
58    #[brw(magic = 5u32)]
59    Keys,
60    /// Holds rows of color labels, for reference  by tracks and searching.
61    #[brw(magic = 6u32)]
62    Colors,
63    /// Holds rows that describe the hierarchical tree structure of available playlists and folders
64    /// grouping them.
65    #[brw(magic = 7u32)]
66    PlaylistTree,
67    /// Holds rows that links tracks to playlists, in the right order.
68    #[brw(magic = 8u32)]
69    PlaylistEntries,
70    /// Holds rows of history playlists, i.e. playlists that are recorded every time the device is
71    /// mounted by a player.
72    #[brw(magic = 11u32)]
73    HistoryPlaylists,
74    /// Holds rows that links tracks to history playlists, in the right order.
75    #[brw(magic = 12u32)]
76    HistoryEntries,
77    /// Holds rows pointing to album artwork images.
78    #[brw(magic = 13u32)]
79    Artwork,
80    /// Contains the metadata categories by which Tracks can be browsed by.
81    #[brw(magic = 16u32)]
82    Columns,
83    /// Holds information used by rekordbox to synchronize history playlists (not yet studied).
84    #[brw(magic = 19u32)]
85    History,
86    /// Unknown Page type.
87    Unknown(u32),
88}
89
90/// Points to a table page and can be used to calculate the page's file offset by multiplying it
91/// with the page size (found in the file header).
92#[binrw]
93#[derive(Clone, Debug, PartialEq, Eq, PartialOrd)]
94#[brw(little)]
95pub struct PageIndex(u32);
96
97impl PageIndex {
98    /// Calculate the absolute file offset of the page in the PDB file for the given `page_size`.
99    #[must_use]
100    pub fn offset(&self, page_size: u32) -> u64 {
101        u64::from(self.0) * u64::from(page_size)
102    }
103}
104
105/// Tables are linked lists of pages containing rows of a single type, which are organized
106/// into groups.
107#[binrw]
108#[derive(Debug, PartialEq, Eq, Clone)]
109#[brw(little)]
110pub struct Table {
111    /// Identifies the type of rows that this table contains.
112    pub page_type: PageType,
113    /// Unknown field, maybe links to a chain of empty pages if the database is ever garbage
114    /// collected (?).
115    #[allow(dead_code)]
116    empty_candidate: u32,
117    /// Index of the first page that belongs to this table.
118    ///
119    /// *Note:* The first page apparently does not contain any rows. If the table is non-empty, the
120    /// actual row data can be found in the pages after.
121    pub first_page: PageIndex,
122    /// Index of the last page that belongs to this table.
123    pub last_page: PageIndex,
124}
125
126/// The PDB header structure, including the list of tables.
127#[binrw]
128#[derive(Debug, PartialEq, Eq, Clone)]
129#[brw(little)]
130pub struct Header {
131    /// Unknown purpose, perhaps an unoriginal signature, seems to always have the value 0.
132    #[br(temp, assert(unknown1 == 0))]
133    #[bw(calc = 0u32)]
134    unknown1: u32,
135    /// Size of a single page in bytes.
136    ///
137    /// The byte offset of a page can be calculated by multiplying a page index with this value.
138    pub page_size: u32,
139    /// Number of tables.
140    #[br(temp)]
141    #[bw(calc = tables.len().try_into().expect("too many tables"))]
142    num_tables: u32,
143    /// Unknown field, not used as any `empty_candidate`, points past end of file.
144    #[allow(dead_code)]
145    next_unused_page: PageIndex,
146    /// Unknown field.
147    #[allow(dead_code)]
148    unknown: u32,
149    /// Always incremented by at least one, sometimes by two or three.
150    pub sequence: u32,
151    /// The gap seems to be always zero.
152    #[br(temp, assert(gap == 0))]
153    #[bw(calc = 0u32)]
154    gap: u32,
155    /// Each table is a linked list of pages containing rows of a particular type.
156    #[br(count = num_tables)]
157    pub tables: Vec<Table>,
158}
159
160impl Header {
161    /// Returns pages for the given Table.
162    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/// A table page.
201///
202/// Each page consists of a header that contains information about the type, number of rows, etc.,
203/// followed by the data section that holds the row data. Each row needs to be located using an
204/// offset found in the page footer at the end of the page.
205///
206/// **Note: The `Page` struct is currently not writable, because row offsets are not taken into
207/// account and rows are not serialized correctly yet.**
208#[binread]
209#[derive(Debug, PartialEq)]
210#[br(little, magic = 0u32)]
211#[br(import(page_size: u32))]
212pub struct Page {
213    /// Index of the page.
214    ///
215    /// Should match the index used for lookup and can be used to verify that the correct page was loaded.
216    pub page_index: PageIndex,
217    /// Type of information that the rows of this page contain.
218    ///
219    /// Should match the page type of the table that this page belongs to.
220    pub page_type: PageType,
221    /// Index of the next page with the same page type.
222    ///
223    /// If this page is the last one of that type, the page index stored in the field will point
224    /// past the end of the file.
225    pub next_page: PageIndex,
226    /// Unknown field.
227    #[allow(dead_code)]
228    unknown1: u32,
229    /// Unknown field.
230    #[allow(dead_code)]
231    unknown2: u32,
232    /// Number of rows in this table (8-bit version).
233    ///
234    /// Used if `num_rows_large` not greater than this value and not equal to `0x1FFF`, which means
235    /// that the number of rows fits into a single byte.
236    pub num_rows_small: u8,
237    /// Unknown field.
238    ///
239    /// According to [@flesniak](https://github.com/flesniak):
240    /// > a bitmask (first track: 32)
241    #[allow(dead_code)]
242    unknown3: u8,
243    /// Unknown field.
244    ///
245    /// According to [@flesniak](https://github.com/flesniak):
246    /// > often 0, sometimes larger, esp. for pages with high real_entry_count (e.g. 12 for 101 entries)
247    #[allow(dead_code)]
248    unknown4: u8,
249    /// Page flags.
250    ///
251    /// According to [@flesniak](https://github.com/flesniak):
252    /// > strange pages: 0x44, 0x64; otherwise seen: 0x24, 0x34
253    page_flags: PageFlags,
254    /// Free space in bytes in the data section of the page (excluding the row offsets in the page footer).
255    pub free_size: u16,
256    /// Used space in bytes in the data section of the page.
257    pub used_size: u16,
258    /// Unknown field.
259    ///
260    /// According to [@flesniak](https://github.com/flesniak):
261    /// > (0->1: 2)
262    #[allow(dead_code)]
263    unknown5: u16,
264    /// Number of rows in this table (16-bit version).
265    ///
266    /// Used when the number of rows does not fit into a single byte. In that case,`num_rows_large`
267    /// is greater than `num_rows_small`, but is not equal to `0x1FFF`.
268    pub num_rows_large: u16,
269    /// Unknown field.
270    #[allow(dead_code)]
271    unknown6: u16,
272    /// Unknown field.
273    ///
274    /// According to [@flesniak](https://github.com/flesniak):
275    /// > always 0, except 1 for history pages, num entries for strange pages?"
276    #[allow(dead_code)]
277    unknown7: u16,
278    /// Number of rows in this page.
279    ///
280    /// **Note:** This is a virtual field and not actually read from the file.
281    #[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    /// The offset at which the row data for this page are located.
285    ///
286    /// **Note:** This is a virtual field and not actually read from the file.
287    #[br(temp)]
288    #[br(calc = page_index.offset(page_size) + u64::from(Self::HEADER_SIZE))]
289    page_heap_offset: u64,
290    /// Row groups belonging to this page.
291    #[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    /// Size of the page header in bytes.
298    pub const HEADER_SIZE: u32 = 0x28;
299
300    /// Parse the row groups at the end of the page.
301    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        // Read row groups
316        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    /// Returns `true` if the page actually contains row data.
340    pub fn has_data(&self) -> bool {
341        self.page_flags.page_has_data()
342    }
343
344    #[must_use]
345    /// Number of rows on this page.
346    ///
347    /// Note that this number includes rows that have been flagged as missing by the row group.
348    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/// A group of row indices, which are built backwards from the end of the page. Holds up to sixteen
358/// row offsets, along with a bit mask that indicates whether each row is actually present in the
359/// table.
360#[derive(Debug, PartialEq)]
361pub struct RowGroup {
362    /// An offset which points to a row in the table, whose actual presence is controlled by one of the
363    /// bits in `row_present_flags`. This instance allows the row itself to be lazily loaded, unless it
364    /// is not present, in which case there is no content to be loaded.
365    rows: [Option<FilePtr16<Row>>; Self::MAX_ROW_COUNT],
366    row_presence_flags: u16,
367    /// Unknown field, probably padding.
368    ///
369    /// Apparently this is not always zero, so it might also be something different.
370    unknown: u16,
371}
372
373impl RowGroup {
374    const MAX_ROW_COUNT: usize = 16;
375
376    /// Return the ordered list of row offsets that are actually present.
377    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    /// Read a row group from the reader.
389    ///
390    /// Note: For this to work, the read position needs to be at the *end* of the row group. For
391    /// the first row group, this is the end of the page heap.
392    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        // TODO streamline this using iterators once std::iter::Iterator::map_windows is stable
416        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/// Identifies a track.
453#[binrw]
454#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
455#[brw(little)]
456pub struct TrackId(pub u32);
457
458/// Identifies an artwork item.
459#[binrw]
460#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
461#[brw(little)]
462pub struct ArtworkId(pub u32);
463
464/// Identifies an album.
465#[binrw]
466#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
467#[brw(little)]
468pub struct AlbumId(pub u32);
469
470/// Identifies an artist.
471#[binrw]
472#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
473#[brw(little)]
474pub struct ArtistId(pub u32);
475
476/// Identifies a genre.
477#[binrw]
478#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
479#[brw(little)]
480pub struct GenreId(pub u32);
481
482/// Identifies a key.
483#[binrw]
484#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
485#[brw(little)]
486pub struct KeyId(pub u32);
487
488/// Identifies a label.
489#[binrw]
490#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
491#[brw(little)]
492pub struct LabelId(pub u32);
493
494/// Identifies a playlist tree node.
495#[binrw]
496#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
497#[brw(little)]
498pub struct PlaylistTreeNodeId(pub u32);
499
500/// Identifies a history playlist.
501#[binrw]
502#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
503#[brw(little)]
504pub struct HistoryPlaylistId(pub u32);
505
506/// Contains the album name, along with an ID of the corresponding artist.
507#[binrw]
508#[derive(Debug, PartialEq, Eq, Clone)]
509#[brw(little)]
510pub struct Album {
511    /// Position of start of this row (needed of offset calculations).
512    ///
513    /// **Note:** This is a virtual field and not actually read from the file.
514    #[br(temp, parse_with = current_offset)]
515    #[bw(ignore)]
516    base_offset: u64,
517    /// Unknown field, usually `80 00`.
518    unknown1: u16,
519    /// Unknown field, called `index_shift` by [@flesniak](https://github.com/flesniak).
520    index_shift: u16,
521    /// Unknown field.
522    unknown2: u32,
523    /// ID of the artist row associated with this row.
524    artist_id: ArtistId,
525    /// ID of this row.
526    id: AlbumId,
527    /// Unknown field.
528    unknown3: u32,
529    /// Unknown field.
530    unknown4: u8,
531    /// Album name String
532    #[br(offset = base_offset, parse_with = FilePtr8::parse)]
533    name: DeviceSQLString,
534}
535
536/// Contains the artist name and ID.
537#[binrw]
538#[derive(Debug, PartialEq, Eq, Clone)]
539#[brw(little)]
540pub struct Artist {
541    /// Determines if the `name` string is located at the 8-bit offset (0x60) or the 16-bit offset (0x64).
542    subtype: u16,
543    /// Unknown field, called `index_shift` by [@flesniak](https://github.com/flesniak).
544    index_shift: u16,
545    /// ID of this row.
546    id: ArtistId,
547    /// Unknown field.
548    unknown1: u8,
549    /// One-byte name offset used if `subtype` is `0x60`.
550    ofs_name_near: u8,
551    /// Two-byte name offset used if `subtype` is `0x64`.
552    ///
553    /// In that case, the value of `ofs_name_near` is ignored
554    #[br(if(subtype == 0x64))]
555    ofs_name_far: Option<u16>,
556    /// Name of this artist.
557    #[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/// Contains the artwork path and ID.
571#[binrw]
572#[derive(Debug, PartialEq, Eq, Clone)]
573#[brw(little)]
574pub struct Artwork {
575    /// ID of this row.
576    id: ArtworkId,
577    /// Path to the album art file.
578    path: DeviceSQLString,
579}
580
581/// Contains numeric color ID
582#[binrw]
583#[derive(Debug, PartialEq, Eq, Clone)]
584#[brw(little)]
585pub struct Color {
586    /// Unknown field.
587    unknown1: u32,
588    /// Unknown field.
589    unknown2: u8,
590    /// Numeric color ID
591    color: ColorIndex,
592    /// Unknown field.
593    unknown3: u16,
594    /// User-defined name of the color.
595    name: DeviceSQLString,
596}
597
598/// Represents a musical genre.
599#[binrw]
600#[derive(Debug, PartialEq, Eq, Clone)]
601#[brw(little)]
602pub struct Genre {
603    /// ID of this row.
604    id: GenreId,
605    /// Name of the genre.
606    name: DeviceSQLString,
607}
608
609/// Represents a history playlist.
610#[binrw]
611#[derive(Debug, PartialEq, Eq, Clone)]
612#[brw(little)]
613pub struct HistoryPlaylist {
614    /// ID of this row.
615    id: HistoryPlaylistId,
616    /// Name of the playlist.
617    name: DeviceSQLString,
618}
619
620/// Represents a history playlist.
621#[binrw]
622#[derive(Debug, PartialEq, Eq, Clone)]
623#[brw(little)]
624pub struct HistoryEntry {
625    /// ID of the track played at this position in the playlist.
626    track_id: TrackId,
627    /// ID of the history playlist.
628    playlist_id: HistoryPlaylistId,
629    /// Position within the playlist.
630    entry_index: u32,
631}
632
633/// Represents a musical key.
634#[binrw]
635#[derive(Debug, PartialEq, Eq, Clone)]
636#[brw(little)]
637pub struct Key {
638    /// ID of this row.
639    id: KeyId,
640    /// Apparently a second copy of the row ID.
641    id2: u32,
642    /// Name of the key.
643    name: DeviceSQLString,
644}
645
646/// Represents a record label.
647#[binrw]
648#[derive(Debug, PartialEq, Eq, Clone)]
649#[brw(little)]
650pub struct Label {
651    /// ID of this row.
652    id: LabelId,
653    /// Name of the record label.
654    name: DeviceSQLString,
655}
656
657/// Represents a node in the playlist tree (either a folder or a playlist).
658#[binrw]
659#[derive(Debug, PartialEq, Eq, Clone)]
660#[brw(little)]
661pub struct PlaylistTreeNode {
662    /// ID of parent row of this row (which means that the parent is a folder).
663    pub parent_id: PlaylistTreeNodeId,
664    /// Unknown field.
665    unknown: u32,
666    /// Sort order indicastor.
667    sort_order: u32,
668    /// ID of this row.
669    pub id: PlaylistTreeNodeId,
670    /// Indicates if the node is a folder. Non-zero if it's a leaf node, i.e. a playlist.
671    node_is_folder: u32,
672    /// Name of this node, as shown when navigating the menu.
673    pub name: DeviceSQLString,
674}
675
676impl PlaylistTreeNode {
677    /// Indicates whether the node is a folder or a playlist.
678    #[must_use]
679    pub fn is_folder(&self) -> bool {
680        self.node_is_folder > 0
681    }
682}
683
684/// Represents a track entry in a playlist.
685#[binrw]
686#[derive(Debug, PartialEq, Eq, Clone)]
687#[brw(little)]
688pub struct PlaylistEntry {
689    /// Position within the playlist.
690    entry_index: u32,
691    /// ID of the track played at this position in the playlist.
692    track_id: TrackId,
693    /// ID of the playlist.
694    playlist_id: PlaylistTreeNodeId,
695}
696
697/// Contains the kinds of Metadata Categories tracks can be browsed by
698/// on CDJs.
699#[binrw]
700#[derive(Debug, PartialEq, Eq, Clone)]
701#[brw(little)]
702pub struct ColumnEntry {
703    // Possibly the primary key, though I don't know if that would
704    // make sense as I don't think there are references to these
705    // rows anywhere else. This could be a stable ID to identify
706    // a category by in hardware (instead of by name).
707    id: u16,
708    // Maybe a bitfield containing infos on sort order and which
709    // columns are displayed.
710    unknown0: u16,
711    /// TODO Contained string is prefixed by the "interlinear annotation"
712    /// characters "\u{fffa}" and postfixed with "\u{fffb}" for some reason?!
713    /// Contained strings are actually `DeviceSQLString::LongBody` even though
714    /// they only contain ascii (apart from their unicode annotations)
715    // TODO since there are only finite many categories, it would make sense
716    // to encode those as an enum as part of the high-level api.
717    pub column_name: DeviceSQLString,
718}
719
720/// Contains the album name, along with an ID of the corresponding artist.
721#[binread]
722#[derive(Debug, PartialEq, Eq, Clone)]
723#[br(little)]
724pub struct Track {
725    /// Position of start of this row (needed of offset calculations).
726    ///
727    /// **Note:** This is a virtual field and not actually read from the file.
728    #[br(temp, parse_with = current_offset)]
729    base_offset: u64,
730    /// Unknown field, usually `24 00`.
731    unknown1: u16,
732    /// Unknown field, called `index_shift` by [@flesniak](https://github.com/flesniak).
733    index_shift: u16,
734    /// Unknown field, called `bitmask` by [@flesniak](https://github.com/flesniak).
735    bitmask: u32,
736    /// Sample Rate in Hz.
737    sample_rate: u32,
738    /// Composer of this track as artist row ID (non-zero if set).
739    composer_id: ArtistId,
740    /// File size in bytes.
741    file_size: u32,
742    /// Unknown field (maybe another ID?)
743    unknown2: u32,
744    /// Unknown field ("always 19048?" according to [@flesniak](https://github.com/flesniak))
745    unknown3: u16,
746    /// Unknown field ("always 30967?" according to [@flesniak](https://github.com/flesniak))
747    unknown4: u16,
748    /// Artwork row ID for the cover art (non-zero if set),
749    artwork_id: ArtworkId,
750    /// Key row ID for the cover art (non-zero if set).
751    key_id: KeyId,
752    /// Artist row ID of the original performer (non-zero if set).
753    orig_artist_id: ArtistId,
754    /// Label row ID of the original performer (non-zero if set).
755    label_id: LabelId,
756    /// Artist row ID of the remixer (non-zero if set).
757    remixer_id: ArtistId,
758    /// Bitrate of the track.
759    bitrate: u32,
760    /// Track number of the track.
761    track_number: u32,
762    /// Track tempo in centi-BPM (= 1/100 BPM).
763    tempo: u32,
764    /// Genre row ID for this track (non-zero if set).
765    genre_id: GenreId,
766    /// Album row ID for this track (non-zero if set).
767    album_id: AlbumId,
768    /// Artist row ID for this track (non-zero if set).
769    artist_id: ArtistId,
770    /// Row ID of this track (non-zero if set).
771    id: TrackId,
772    /// Disc number of this track (non-zero if set).
773    disc_number: u16,
774    /// Number of times this track was played.
775    play_count: u16,
776    /// Year this track was released.
777    year: u16,
778    /// Bits per sample of the track aduio file.
779    sample_depth: u16,
780    /// Playback duration of this track in seconds (at normal speed).
781    duration: u16,
782    /// Unknown field, apparently always "29".
783    unknown5: u16,
784    /// Color row ID for this track (non-zero if set).
785    color: ColorIndex,
786    /// User rating of this track (0 to 5 starts).
787    rating: u8,
788    /// Unknown field, apparently always "1".
789    unknown6: u16,
790    /// Unknown field (alternating "2" and "3"?).
791    unknown7: u16,
792    /// International Standard Recording Code (ISRC), in mangled format.
793    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
794    isrc: DeviceSQLString,
795    /// Unknown string field.
796    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
797    unknown_string1: DeviceSQLString,
798    /// Unknown string field.
799    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
800    unknown_string2: DeviceSQLString,
801    /// Unknown string field.
802    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
803    unknown_string3: DeviceSQLString,
804    /// Unknown string field.
805    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
806    unknown_string4: DeviceSQLString,
807    /// Unknown string field (named by [@flesniak](https://github.com/flesniak)).
808    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
809    message: DeviceSQLString,
810    /// Probably describes whether the track is public on kuvo.com (?). Value is either "ON" or empty string.
811    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
812    kuvo_public: DeviceSQLString,
813    /// Determines if hotcues should be autoloaded. Value is either "ON" or empty string.
814    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
815    autoload_hotcues: DeviceSQLString,
816    /// Unknown string field.
817    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
818    unknown_string5: DeviceSQLString,
819    /// Unknown string field (usually empty).
820    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
821    unknown_string6: DeviceSQLString,
822    /// Date when the track was added to the Rekordbox collection.
823    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
824    date_added: DeviceSQLString,
825    /// Date when the track was released.
826    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
827    release_date: DeviceSQLString,
828    /// Name of the remix (if any).
829    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
830    mix_name: DeviceSQLString,
831    /// Unknown string field (usually empty).
832    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
833    unknown_string7: DeviceSQLString,
834    /// File path of the track analysis file.
835    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
836    analyze_path: DeviceSQLString,
837    /// Date when the track analysis was performed.
838    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
839    analyze_date: DeviceSQLString,
840    /// Track comment.
841    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
842    comment: DeviceSQLString,
843    /// Track title.
844    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
845    title: DeviceSQLString,
846    /// Unknown string field (usually empty).
847    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
848    unknown_string8: DeviceSQLString,
849    /// Name of the file.
850    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
851    filename: DeviceSQLString,
852    /// Path of the file.
853    #[br(offset = base_offset, parse_with = FilePtr16::parse)]
854    file_path: DeviceSQLString,
855}
856
857// #[bw(little)] on #[binread] types does
858// not seem to work so we manually define the endianness here.
859impl 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        // Skip offsets, because we want to write the actual strings first.
911        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/// A table row contains the actual data.
961#[binrw]
962#[derive(Debug, PartialEq, Eq, Clone)]
963#[brw(little)]
964#[br(import(page_type: PageType))]
965// The large enum size is unfortunate, but since users of this library will probably use iterators
966// to consume the results on demand, we can live with this. The alternative of using a `Box` would
967// require a heap allocation per row, which is arguably worse. Hence, the warning is disabled for
968// this enum.
969#[allow(clippy::large_enum_variant)]
970pub enum Row {
971    /// Contains the album name, along with an ID of the corresponding artist.
972    #[br(pre_assert(page_type == PageType::Albums))]
973    Album(Album),
974    /// Contains the artist name and ID.
975    #[br(pre_assert(page_type == PageType::Artists))]
976    Artist(Artist),
977    /// Contains the artwork path and ID.
978    #[br(pre_assert(page_type == PageType::Artwork))]
979    Artwork(Artwork),
980    /// Contains numeric color ID
981    #[br(pre_assert(page_type == PageType::Colors))]
982    Color(Color),
983    /// Represents a musical genre.
984    #[br(pre_assert(page_type == PageType::Genres))]
985    Genre(Genre),
986    /// Represents a history playlist.
987    #[br(pre_assert(page_type == PageType::HistoryPlaylists))]
988    HistoryPlaylist(HistoryPlaylist),
989    /// Represents a history playlist.
990    #[br(pre_assert(page_type == PageType::HistoryEntries))]
991    HistoryEntry(HistoryEntry),
992    /// Represents a musical key.
993    #[br(pre_assert(page_type == PageType::Keys))]
994    Key(Key),
995    /// Represents a record label.
996    #[br(pre_assert(page_type == PageType::Labels))]
997    Label(Label),
998    /// Represents a node in the playlist tree (either a folder or a playlist).
999    #[br(pre_assert(page_type == PageType::PlaylistTree))]
1000    PlaylistTreeNode(PlaylistTreeNode),
1001    /// Represents a track entry in a playlist.
1002    #[br(pre_assert(page_type == PageType::PlaylistEntries))]
1003    PlaylistEntry(PlaylistEntry),
1004    /// Contains the metadata categories by which Tracks can be browsed by.
1005    #[br(pre_assert(page_type == PageType::Columns))]
1006    ColumnEntry(ColumnEntry),
1007    /// Contains the album name, along with an ID of the corresponding artist.
1008    #[br(pre_assert(page_type == PageType::Tracks))]
1009    Track(Track),
1010    /// The row format (and also its size) is unknown, which means it can't be parsed.
1011    #[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}