Skip to main content

baken_export/pdb/
page.rs

1//! DeviceSQL pages: 4096 bytes, a 40-byte header, rows growing up from
2//! offset 0x28 and a row directory growing down from the end in groups of 16.
3//!
4//! Header formulas were derived from pages rekordbox 7 wrote in one go
5//! (keys, colors, columns, playlist entries of a real export) and are
6//! reproduced byte for byte by the tests in `writer.rs`.
7
8pub const PAGE_LEN: usize = 4096;
9pub const HEAP_START: usize = 0x28;
10const GROUP_BYTES: usize = 36;
11const NONE: u32 = 0x03FF_FFFF;
12
13/// How the two "fixed" tables differ from everything else in the header.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum HeaderStyle {
16    /// `unknown5 = 1`, `num_rows_large = rows - 1`.
17    Normal,
18    /// colors and columns: `unknown5 = rows`, `num_rows_large = 0`.
19    Fixed,
20}
21
22#[derive(Debug, Clone)]
23pub struct DataPage {
24    pub index: u32,
25    pub table: u32,
26    pub next: u32,
27    pub seq: u32,
28    pub style: HeaderStyle,
29    rows: Vec<Vec<u8>>,
30    heap_used: usize,
31}
32
33fn align4(n: usize) -> usize {
34    (n + 3) & !3
35}
36
37fn dir_bytes(rows: usize) -> usize {
38    let full = rows / 16;
39    let rest = rows % 16;
40    full * GROUP_BYTES + if rest > 0 { rest * 2 + 4 } else { 0 }
41}
42
43impl DataPage {
44    pub fn new(index: u32, table: u32, seq: u32, style: HeaderStyle) -> Self {
45        DataPage {
46            index,
47            table,
48            next: NONE,
49            seq,
50            style,
51            rows: Vec::new(),
52            heap_used: 0,
53        }
54    }
55
56    pub fn rows(&self) -> usize {
57        self.rows.len()
58    }
59
60    pub fn is_empty(&self) -> bool {
61        self.rows.is_empty()
62    }
63
64    /// Append a row if it fits; the row is padded to 4 bytes like rekordbox does.
65    pub fn try_push(&mut self, row: &[u8]) -> bool {
66        let need = self.heap_used + align4(row.len()) + dir_bytes(self.rows.len() + 1);
67        if need > PAGE_LEN - HEAP_START || self.rows.len() >= 0x1fff {
68            return false;
69        }
70        self.heap_used += align4(row.len());
71        self.rows.push(row.to_vec());
72        true
73    }
74
75    pub fn to_bytes(&self) -> Vec<u8> {
76        let mut p = vec![0u8; PAGE_LEN];
77        let n = self.rows.len();
78        let free = PAGE_LEN - HEAP_START - self.heap_used - dir_bytes(n);
79        let (u5, nrl) = match self.style {
80            HeaderStyle::Normal => (1u16, n.saturating_sub(1) as u16),
81            HeaderStyle::Fixed => (n as u16, 0),
82        };
83        p[0x04..0x08].copy_from_slice(&self.index.to_le_bytes());
84        p[0x08..0x0c].copy_from_slice(&self.table.to_le_bytes());
85        p[0x0c..0x10].copy_from_slice(&self.next.to_le_bytes());
86        p[0x10..0x14].copy_from_slice(&self.seq.to_le_bytes());
87        p[0x18] = (n & 0xff) as u8;
88        p[0x19] = (((n & 7) << 5) | ((n >> 8) & 0x1f)) as u8;
89        p[0x1a] = (n >> 3) as u8;
90        p[0x1b] = 0x24;
91        p[0x1c..0x1e].copy_from_slice(&(free as u16).to_le_bytes());
92        p[0x1e..0x20].copy_from_slice(&(self.heap_used as u16).to_le_bytes());
93        p[0x20..0x22].copy_from_slice(&u5.to_le_bytes());
94        p[0x22..0x24].copy_from_slice(&nrl.to_le_bytes());
95
96        let mut off = 0usize;
97        for (i, row) in self.rows.iter().enumerate() {
98            let start = HEAP_START + off;
99            p[start..start + row.len()].copy_from_slice(row);
100            let group = i / 16;
101            let slot = i % 16;
102            let base = PAGE_LEN - group * GROUP_BYTES;
103            let at = base - 6 - 2 * slot;
104            p[at..at + 2].copy_from_slice(&(off as u16).to_le_bytes());
105            let flags = base - 4;
106            let bit = 1u16 << slot;
107            let present = u16::from_le_bytes([p[flags], p[flags + 1]]) | bit;
108            p[flags..flags + 2].copy_from_slice(&present.to_le_bytes());
109            p[flags + 2..flags + 4].copy_from_slice(&present.to_le_bytes());
110            off += align4(row.len());
111        }
112        p
113    }
114}
115
116/// The page every table starts with. `next` is the first data page, or the
117/// empty candidate when the table has no rows.
118pub fn index_page(index: u32, table: u32, next: u32, first_data: Option<u32>) -> Vec<u8> {
119    let mut p = vec![0u8; PAGE_LEN];
120    p[0x04..0x08].copy_from_slice(&index.to_le_bytes());
121    p[0x08..0x0c].copy_from_slice(&table.to_le_bytes());
122    p[0x0c..0x10].copy_from_slice(&next.to_le_bytes());
123    p[0x10..0x14].copy_from_slice(&1u32.to_le_bytes());
124    p[0x1b] = 0x64;
125    p[0x20..0x22].copy_from_slice(&0x1fffu16.to_le_bytes());
126    p[0x22..0x24].copy_from_slice(&0x1fffu16.to_le_bytes());
127    p[0x24..0x26].copy_from_slice(&0x03ecu16.to_le_bytes());
128    let words = [index, first_data.unwrap_or(NONE), NONE, 0, 0x1FFF_0000];
129    let mut off = HEAP_START;
130    for w in words {
131        p[off..off + 4].copy_from_slice(&w.to_le_bytes());
132        off += 4;
133    }
134    for _ in 0..1004 {
135        p[off..off + 4].copy_from_slice(&0x1FFF_FFF8u32.to_le_bytes());
136        off += 4;
137    }
138    debug_assert_eq!(off, PAGE_LEN - 20);
139    p
140}
141
142/// File header: page 0.
143pub fn header_page(
144    num_tables: u32,
145    next_unused: u32,
146    sequence: u32,
147    pointers: &[(u32, u32, u32, u32)],
148) -> Vec<u8> {
149    let mut p = vec![0u8; PAGE_LEN];
150    p[0x04..0x08].copy_from_slice(&(PAGE_LEN as u32).to_le_bytes());
151    p[0x08..0x0c].copy_from_slice(&num_tables.to_le_bytes());
152    p[0x0c..0x10].copy_from_slice(&next_unused.to_le_bytes());
153    p[0x10..0x14].copy_from_slice(&1u32.to_le_bytes());
154    p[0x14..0x18].copy_from_slice(&sequence.to_le_bytes());
155    let mut off = 0x1c;
156    for &(ty, empty, first, last) in pointers {
157        for w in [ty, empty, first, last] {
158            p[off..off + 4].copy_from_slice(&w.to_le_bytes());
159            off += 4;
160        }
161    }
162    p
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn packed_row_count_matches_rekordbox() {
171        // playlist_entries page of the reference export: 284 rows -> 1c 81 23
172        let mut pg = DataPage::new(18, 8, 4318, HeaderStyle::Normal);
173        for i in 0..284u32 {
174            assert!(pg.try_push(&[i.to_le_bytes(), [0; 4], [0; 4]].concat()));
175        }
176        let b = pg.to_bytes();
177        assert_eq!((b[0x18], b[0x19], b[0x1a]), (28, 129, 35));
178        assert_eq!(u16::from_le_bytes([b[0x1c], b[0x1d]]), 8);
179        assert_eq!(u16::from_le_bytes([b[0x1e], b[0x1f]]), 3408);
180        assert_eq!(u16::from_le_bytes([b[0x22], b[0x23]]), 283);
181    }
182
183    #[test]
184    fn page_fills_then_rejects() {
185        let mut pg = DataPage::new(1, 8, 1, HeaderStyle::Normal);
186        let mut n = 0;
187        while pg.try_push(&[0u8; 12]) {
188            n += 1;
189        }
190        assert_eq!(n, 284);
191    }
192}