1use super::string::encode;
5
6fn u32s(out: &mut Vec<u8>, vals: &[u32]) {
7 for v in vals {
8 out.extend_from_slice(&v.to_le_bytes());
9 }
10}
11
12fn align4(n: usize) -> usize {
13 (n + 3) & !3
14}
15
16fn string_at(at: usize, encoded: &[u8]) -> usize {
21 if encoded[0] == 0x90 {
22 align4(at)
23 } else {
24 at
25 }
26}
27
28pub fn named(id: u32, name: &str) -> Vec<u8> {
30 let mut r = Vec::new();
31 u32s(&mut r, &[id]);
32 r.extend(encode(name));
33 r
34}
35
36pub fn artist(position_in_page: usize, id: u32, name: &str) -> Vec<u8> {
38 let mut r = Vec::new();
39 r.extend_from_slice(&0x60u16.to_le_bytes());
40 r.extend_from_slice(&((position_in_page as u16) * 0x20).to_le_bytes());
41 u32s(&mut r, &[id]);
42 with_name(r, name)
43}
44
45pub fn album(position_in_page: usize, id: u32, artist_id: u32, name: &str) -> Vec<u8> {
47 let mut r = Vec::new();
48 r.extend_from_slice(&0x80u16.to_le_bytes());
49 r.extend_from_slice(&((position_in_page as u16) * 0x20).to_le_bytes());
50 u32s(&mut r, &[0, artist_id, id, 0]);
51 with_name(r, name)
52}
53
54fn with_name(mut r: Vec<u8>, name: &str) -> Vec<u8> {
57 let name = encode(name);
58 let header = r.len() + 2;
59 let at = string_at(header, &name);
60 r.extend_from_slice(&[0x03, at as u8]);
61 r.resize(at, 0);
62 r.extend_from_slice(&name);
63 r.resize(align4(header) + align4(name.len()) + 4, 0);
64 r
65}
66
67pub fn key(id: u32, name: &str) -> Vec<u8> {
69 let mut r = Vec::new();
70 u32s(&mut r, &[id, id]);
71 r.extend(encode(name));
72 r
73}
74
75pub fn color(id: u8, name: &str) -> Vec<u8> {
77 let mut r = vec![0, 0, 0, 0, id, id, 0, 0];
78 r.extend(encode(name));
79 r
80}
81
82pub fn playlist_node(
84 parent: u32,
85 sort_order: u32,
86 id: u32,
87 is_folder: bool,
88 name: &str,
89) -> Vec<u8> {
90 let mut r = Vec::new();
91 u32s(&mut r, &[parent, 0, sort_order, id, is_folder as u32]);
92 r.extend(encode(name));
93 r
94}
95
96pub fn playlist_entry(entry_index: u32, track_id: u32, playlist_id: u32) -> Vec<u8> {
98 let mut r = Vec::new();
99 u32s(&mut r, &[entry_index, track_id, playlist_id]);
100 r
101}
102
103pub fn column(id: u16, code: u16, name: &str) -> Vec<u8> {
105 let mut r = Vec::new();
106 r.extend_from_slice(&id.to_le_bytes());
107 r.extend_from_slice(&code.to_le_bytes());
108 r.extend(encode(name));
109 r
110}
111
112pub fn history_property(track_count: u32, export_date: &str, device_name: &str) -> Vec<u8> {
117 let mut r = Vec::new();
118 u32s(&mut r, &[0x0280, track_count, 0]);
119 r.extend(encode(export_date));
120 r.extend_from_slice(&[0x19, 0x1e]);
121 r.extend(encode("1000"));
122 let name = encode(device_name);
123 r.resize(string_at(r.len(), &name), 0);
124 r.extend(name);
125 if r.len() < 40 {
126 r.resize(40, 0);
127 }
128 r
129}
130
131#[derive(Debug, Clone, Default)]
133pub struct TrackRow {
134 pub id: u32,
135 pub sample_rate: u32,
136 pub composer_id: u32,
137 pub file_size: u32,
138 pub artwork_id: u32,
139 pub key_id: u32,
140 pub original_artist_id: u32,
141 pub label_id: u32,
142 pub remixer_id: u32,
143 pub bitrate: u32,
144 pub track_number: u32,
145 pub tempo: u32,
147 pub genre_id: u32,
148 pub album_id: u32,
149 pub artist_id: u32,
150 pub disc_number: u16,
151 pub play_count: u16,
152 pub year: u16,
153 pub sample_depth: u16,
154 pub duration_seconds: u16,
155 pub color_id: u8,
156 pub rating: u8,
158 pub file_type: u16,
159 pub date_added: String,
160 pub release_date: String,
161 pub mix_name: String,
162 pub analyze_path: String,
163 pub analyze_date: String,
164 pub comment: String,
165 pub title: String,
166 pub filename: String,
167 pub file_path: String,
168}
169
170pub const FILE_TYPE_MP3: u16 = 1;
171pub const FILE_TYPE_M4A: u16 = 4;
172pub const FILE_TYPE_FLAC: u16 = 5;
173pub const FILE_TYPE_ALAC: u16 = 6;
174pub const FILE_TYPE_WAV: u16 = 11;
175pub const FILE_TYPE_AIFF: u16 = 12;
176
177impl TrackRow {
178 pub fn encode(&self, position_in_page: usize) -> Vec<u8> {
179 let mut r = Vec::with_capacity(0x88 + 256);
180 r.extend_from_slice(&0x24u16.to_le_bytes());
181 r.extend_from_slice(&((position_in_page as u16) * 0x20).to_le_bytes());
182 u32s(
183 &mut r,
184 &[
185 0xC0700,
186 self.sample_rate,
187 self.composer_id,
188 self.file_size,
189 self.id + 20,
190 3933607398,
191 self.artwork_id,
192 self.key_id,
193 self.original_artist_id,
194 self.label_id,
195 self.remixer_id,
196 self.bitrate,
197 self.track_number,
198 self.tempo,
199 self.genre_id,
200 self.album_id,
201 self.artist_id,
202 self.id,
203 ],
204 );
205 for v in [
206 self.disc_number,
207 self.play_count,
208 self.year,
209 self.sample_depth,
210 self.duration_seconds,
211 41,
212 ] {
213 r.extend_from_slice(&v.to_le_bytes());
214 }
215 r.push(self.color_id);
216 r.push(self.rating);
217 r.extend_from_slice(&self.file_type.to_le_bytes());
218 r.extend_from_slice(&3u16.to_le_bytes());
219 debug_assert_eq!(r.len(), 0x5e);
220
221 let strings: [&str; 21] = [
224 "",
225 "",
226 "1",
227 "1",
228 "1",
229 "",
230 "ON",
231 "ON",
232 "",
233 "",
234 &self.date_added,
235 &self.release_date,
236 &self.mix_name,
237 "",
238 &self.analyze_path,
239 &self.analyze_date,
240 &self.comment,
241 &self.title,
242 "",
243 &self.filename,
244 &self.file_path,
245 ];
246 let encoded: Vec<Vec<u8>> = strings.iter().map(|s| encode(s)).collect();
247 let mut offsets = Vec::with_capacity(encoded.len());
248 let mut off = 0x88;
249 for e in &encoded {
250 off = string_at(off, e);
251 r.extend_from_slice(&(off as u16).to_le_bytes());
252 offsets.push(off);
253 off += e.len();
254 }
255 debug_assert_eq!(r.len(), 0x88);
256 let alloc = 0x88 + encoded.iter().map(|e| align4(e.len())).sum::<usize>() + 4;
259 for (e, at) in encoded.iter().zip(offsets) {
260 r.resize(at, 0);
261 r.extend_from_slice(e);
262 }
263 r.resize(alloc, 0);
264 r
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn fixed_rows_match_fixture_bytes() {
274 assert_eq!(key(1, "1A"), hex("010000000100000007314100")[..11]);
275 assert_eq!(color(1, "Pink"), hex("00000000010100000b50696e6b"));
276 assert_eq!(named(1, "Techno"), hex("010000000f546563686e6f"));
277 let a = artist(1, 2, "tk_elektron");
278 assert_eq!(&a[..10], &hex("6000200002000000030a")[..]);
279 assert_eq!(a.len(), 28); assert_eq!(
281 &album(1, 2, 4, "x")[..22],
282 &hex("80002000000000000400000002000000000000000316")[..]
283 );
284 assert_eq!(
286 artist(10, 11, "Rødhåd"),
287 hex("600040010b000000030c0000901000005200f80064006800e500640000000000")
288 );
289 assert_eq!(
290 album(2, 153, 0, "atöm.04"),
291 hex(concat!(
292 "800040000000000000000000990000000000000003180000",
293 "9012000061007400f6006d002e0030003400000000000000"
294 ))
295 );
296 assert_eq!(
297 &playlist_node(0, 1, 3, false, "HT-70min")[..20],
298 &hex("0000000000000000010000000300000000000000")[..]
299 );
300 assert_eq!(
301 &column(1, 0x80, "\u{fffa}GENRE\u{fffb}")[..8],
302 &hex("0100800090120000")[..]
303 );
304 let h = history_property(0, "2024-11-16", "");
305 assert_eq!(h.len(), 40);
306 assert_eq!(
307 &h[..31],
308 &hex("80020000000000000000000017323032342d31312d3136191e0b3130303003")[..]
309 );
310 assert_eq!(
312 &history_property(587, "2025-04-11", "")[..8],
313 &hex("800200004b020000")[..]
314 );
315 }
316
317 #[test]
318 fn track_row_layout() {
319 let t = TrackRow {
320 id: 352,
321 title: "Unreal".into(),
322 file_type: FILE_TYPE_MP3,
323 ..Default::default()
324 };
325 let r = t.encode(0);
326 assert_eq!(&r[..4], &[0x24, 0, 0, 0]);
327 assert_eq!(u32::from_le_bytes(r[0x48..0x4c].try_into().unwrap()), 352);
328 assert_eq!(u16::from_le_bytes(r[0x5a..0x5c].try_into().unwrap()), 1);
329 let title_off =
330 u16::from_le_bytes(r[0x5e + 2 * 17..0x60 + 2 * 17].try_into().unwrap()) as usize;
331 assert_eq!(
332 super::super::string::decode(&r, title_off).unwrap().0,
333 "Unreal"
334 );
335 }
336
337 #[test]
338 fn track_row_aligns_utf16_strings() {
339 let t = TrackRow {
340 title: "American Boy 🅴".into(),
341 filename: "01. Crazy In Love.mp3".into(),
342 file_path: "/Contents/Beyoncé; JAŸ-Z/Crazy In Love/01. Crazy In Love.mp3".into(),
343 comment: "2008".into(),
344 ..Default::default()
345 };
346 let r = t.encode(0);
347 let offsets: Vec<usize> = (0..21)
348 .map(|k| u16::from_le_bytes(r[0x5e + 2 * k..0x60 + 2 * k].try_into().unwrap()) as usize)
349 .collect();
350 let mut end = 0x88;
351 for &off in &offsets {
352 let (_, len) = super::super::string::decode(&r, off).unwrap();
353 if r[off] == 0x90 {
354 assert_eq!(off % 4, 0);
355 assert!(r[end..off].iter().all(|&b| b == 0));
356 } else {
357 assert_eq!(off, end, "ASCII strings stay packed");
358 }
359 end = off + len;
360 }
361 assert!(end <= r.len());
362 let text = |k: usize| super::super::string::decode(&r, offsets[k]).unwrap().0;
363 assert_eq!(text(17), "American Boy 🅴");
364 assert_eq!(text(20), t.file_path);
365 }
366
367 fn hex(s: &str) -> Vec<u8> {
368 (0..s.len())
369 .step_by(2)
370 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
371 .collect()
372 }
373}