1use std::fs;
19use std::io;
20use std::path::Path;
21
22use indexmap::IndexMap;
23
24const ORDER: [&str; 7] = ["TIT2", "TPE1", "TRCK", "TALB", "TPOS", "TDRC", "TCON"];
25
26#[derive(Clone, Debug, PartialEq)]
27pub enum Body {
28 Text { enc: u8, values: Vec<String> },
30 Txxx {
31 enc: u8,
32 desc: String,
33 values: Vec<String>,
34 },
35 Comm {
36 enc: u8,
37 lang: [u8; 3],
38 desc: String,
39 values: Vec<String>,
40 },
41 Uslt {
42 enc: u8,
43 lang: [u8; 3],
44 desc: String,
45 text: String,
46 },
47 Apic {
48 enc: u8,
49 mime: String,
50 ptype: u8,
51 desc: String,
52 data: Vec<u8>,
53 },
54 Raw(Vec<u8>),
56}
57
58#[derive(Clone, Debug, PartialEq)]
59pub struct Frame {
60 pub id: String,
61 pub body: Body,
62}
63
64impl Frame {
65 pub fn text(id: &str, value: &str) -> Frame {
66 Frame {
67 id: id.into(),
68 body: Body::Text {
69 enc: 3,
70 values: vec![value.into()],
71 },
72 }
73 }
74
75 pub fn hash_key(&self) -> String {
77 match &self.body {
78 Body::Txxx { desc, .. } => format!("TXXX:{desc}"),
79 Body::Comm { desc, lang, .. } => {
80 format!("COMM:{desc}:{}", String::from_utf8_lossy(lang))
81 }
82 Body::Uslt { desc, lang, .. } => {
83 format!("USLT:{desc}:{}", String::from_utf8_lossy(lang))
84 }
85 Body::Apic { desc, .. } => format!("APIC:{desc}"),
86 _ => self.id.clone(),
87 }
88 }
89}
90
91#[derive(Clone, Debug, Default)]
93pub struct Tag {
94 pub frames: IndexMap<String, Frame>,
95}
96
97fn syncsafe(n: usize) -> [u8; 4] {
98 [
99 ((n >> 21) & 0x7f) as u8,
100 ((n >> 14) & 0x7f) as u8,
101 ((n >> 7) & 0x7f) as u8,
102 (n & 0x7f) as u8,
103 ]
104}
105
106fn read_syncsafe(b: &[u8]) -> usize {
107 b.iter()
108 .fold(0usize, |acc, &x| (acc << 7) | (x & 0x7f) as usize)
109}
110
111fn encode(enc: u8, s: &str) -> Vec<u8> {
112 match enc {
113 0 => s
114 .chars()
115 .map(|c| if (c as u32) < 256 { c as u8 } else { b'?' })
116 .collect(),
117 1 => {
118 let mut v = vec![0xff, 0xfe];
119 v.extend(s.encode_utf16().flat_map(|u| u.to_le_bytes()));
120 v
121 }
122 2 => s.encode_utf16().flat_map(|u| u.to_be_bytes()).collect(),
123 _ => s.as_bytes().to_vec(),
124 }
125}
126
127fn term(enc: u8) -> &'static [u8] {
128 if enc == 1 || enc == 2 {
129 &[0, 0]
130 } else {
131 &[0]
132 }
133}
134
135fn encoded_text(enc: u8, s: &str) -> Vec<u8> {
136 let mut v = encode(enc, s);
137 v.extend_from_slice(term(enc));
138 v
139}
140
141fn read_encoded(enc: u8, data: &[u8]) -> (String, usize) {
143 let t = term(enc);
144 let step = t.len();
145 let mut i = 0;
146 let end = loop {
147 if i + step > data.len() {
148 break None;
149 }
150 if &data[i..i + step] == t {
151 break Some(i);
152 }
153 i += step;
154 };
155 let (raw, consumed) = match end {
156 Some(e) => (&data[..e], e + step),
157 None => (data, data.len()),
158 };
159 let s = match enc {
160 0 => raw.iter().map(|&b| b as char).collect(),
161 1 => {
162 let (le, body) = match raw {
163 [0xfe, 0xff, rest @ ..] => (false, rest),
164 [0xff, 0xfe, rest @ ..] => (true, rest),
165 rest => (true, rest),
166 };
167 let units: Vec<u16> = body
168 .chunks_exact(2)
169 .map(|c| {
170 if le {
171 u16::from_le_bytes([c[0], c[1]])
172 } else {
173 u16::from_be_bytes([c[0], c[1]])
174 }
175 })
176 .collect();
177 String::from_utf16_lossy(&units)
178 }
179 2 => {
180 let units: Vec<u16> = raw
181 .chunks_exact(2)
182 .map(|c| u16::from_be_bytes([c[0], c[1]]))
183 .collect();
184 String::from_utf16_lossy(&units)
185 }
186 _ => String::from_utf8_lossy(raw).into_owned(),
187 };
188 (s, consumed)
189}
190
191fn read_multi(enc: u8, mut data: &[u8]) -> Vec<String> {
192 let mut out = Vec::new();
193 while !data.is_empty() {
194 let (s, n) = read_encoded(enc, data);
195 out.push(s);
196 data = &data[n..];
197 }
198 out
199}
200
201fn parse_body(id: &str, data: &[u8]) -> Body {
202 let raw = || Body::Raw(data.to_vec());
203 if data.is_empty() {
204 return raw();
205 }
206 let enc = data[0];
207 if enc > 3 {
208 return raw();
209 }
210 let rest = &data[1..];
211 match id {
212 "TXXX" => {
213 let (desc, n) = read_encoded(enc, rest);
214 Body::Txxx {
215 enc,
216 desc,
217 values: read_multi(enc, &rest[n..]),
218 }
219 }
220 "COMM" | "USLT" if rest.len() >= 3 => {
221 let lang = [rest[0], rest[1], rest[2]];
222 let (desc, n) = read_encoded(enc, &rest[3..]);
223 let after = &rest[3 + n..];
224 if id == "COMM" {
225 Body::Comm {
226 enc,
227 lang,
228 desc,
229 values: read_multi(enc, after),
230 }
231 } else {
232 Body::Uslt {
233 enc,
234 lang,
235 desc,
236 text: read_encoded(enc, after).0,
237 }
238 }
239 }
240 "APIC" => {
241 let Some(mend) = rest.iter().position(|&b| b == 0) else {
242 return raw();
243 };
244 let mime: String = rest[..mend].iter().map(|&b| b as char).collect();
245 let after = &rest[mend + 1..];
246 if after.is_empty() {
247 return raw();
248 }
249 let ptype = after[0];
250 let (desc, n) = read_encoded(enc, &after[1..]);
251 Body::Apic {
252 enc,
253 mime,
254 ptype,
255 desc,
256 data: after[1 + n..].to_vec(),
257 }
258 }
259 t if t.starts_with('T') => Body::Text {
260 enc,
261 values: read_multi(enc, rest),
262 },
263 _ => raw(),
264 }
265}
266
267fn write_body(f: &Frame) -> Option<Vec<u8>> {
268 let mut v = Vec::new();
269 match &f.body {
270 Body::Text { enc, values } => {
271 if values.join("\0").is_empty() {
272 return None;
273 }
274 v.push(*enc);
275 for s in values {
276 let s = if f.id == "TDRC"
277 || f.id == "TDOR"
278 || f.id == "TDRL"
279 || f.id == "TDTG"
280 || f.id == "TDEN"
281 {
282 s.replace(' ', "T")
283 } else {
284 s.clone()
285 };
286 v.extend(encoded_text(*enc, &s));
287 }
288 }
289 Body::Txxx { enc, desc, values } => {
290 v.push(*enc);
291 v.extend(encoded_text(*enc, desc));
292 for s in values {
293 v.extend(encoded_text(*enc, s));
294 }
295 }
296 Body::Comm {
297 enc,
298 lang,
299 desc,
300 values,
301 } => {
302 v.push(*enc);
303 v.extend_from_slice(lang);
304 v.extend(encoded_text(*enc, desc));
305 for s in values {
306 v.extend(encoded_text(*enc, s));
307 }
308 }
309 Body::Uslt {
310 enc,
311 lang,
312 desc,
313 text,
314 } => {
315 v.push(*enc);
316 v.extend_from_slice(lang);
317 v.extend(encoded_text(*enc, desc));
318 v.extend(encoded_text(*enc, text));
319 }
320 Body::Apic {
321 enc,
322 mime,
323 ptype,
324 desc,
325 data,
326 } => {
327 v.push(*enc);
328 v.extend(mime.bytes());
329 v.push(0);
330 v.push(*ptype);
331 v.extend(encoded_text(*enc, desc));
332 v.extend_from_slice(data);
333 }
334 Body::Raw(data) => v.extend_from_slice(data),
335 }
336 Some(v)
337}
338
339fn existing_tag_size(data: &[u8]) -> usize {
342 if data.len() < 10 || &data[..3] != b"ID3" {
343 return 0;
344 }
345 let flags = data[5];
346 let footer = if data[3] == 4 && flags & 0x10 != 0 {
347 10
348 } else {
349 0
350 };
351 read_syncsafe(&data[6..10]) + 10 + footer
352}
353
354impl Tag {
355 pub fn load(data: &[u8]) -> Option<Tag> {
358 let size = existing_tag_size(data);
359 if size == 0 {
360 return None;
361 }
362 let version = data[3];
363 let flags = data[5];
364 let end = (size.min(data.len())).max(10);
365 let mut pos = 10;
366 if flags & 0x40 != 0 && pos + 4 <= end {
367 let ext = if version == 4 {
369 read_syncsafe(&data[pos..pos + 4])
370 } else {
371 u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
372 as usize
373 + 4
374 };
375 pos += ext;
376 }
377 let mut tag = Tag::default();
378 while pos + 10 <= end {
379 let id = &data[pos..pos + 4];
380 if id[0] == 0 {
381 break;
382 }
383 let fsize = if version == 4 {
384 read_syncsafe(&data[pos + 4..pos + 8])
385 } else {
386 u32::from_be_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]])
387 as usize
388 };
389 let body_start = pos + 10;
390 let body_end = (body_start + fsize).min(end);
391 let id = String::from_utf8_lossy(id).into_owned();
392 let frame = Frame {
393 id: id.clone(),
394 body: parse_body(&id, &data[body_start..body_end]),
395 };
396 tag.add(frame);
397 pos = body_start + fsize;
398 }
399 Some(tag)
400 }
401
402 pub fn add(&mut self, frame: Frame) {
404 self.frames.insert(frame.hash_key(), frame);
405 }
406
407 pub fn get(&self, key: &str) -> Option<&Frame> {
408 self.frames.get(key)
409 }
410
411 fn frame_data(&self) -> Vec<u8> {
413 let mut items: Vec<(usize, &Frame, Vec<u8>)> = Vec::new();
414 for (i, f) in self.frames.values().enumerate() {
415 let Some(body) = write_body(f) else { continue };
416 let mut data = Vec::with_capacity(body.len() + 10);
417 data.extend_from_slice(&f.id.as_bytes()[..4.min(f.id.len())]);
418 data.extend_from_slice(&syncsafe(body.len()));
419 data.extend_from_slice(&[0, 0]);
420 data.extend(body);
421 items.push((i, f, data));
422 }
423 let prio = |f: &Frame| match ORDER.iter().position(|o| *o == f.id) {
424 Some(p) => p,
425 None if f.id == "APIC" => ORDER.len() + 1,
426 None => ORDER.len(),
427 };
428 items.sort_by(|a, b| {
429 let ka = (
430 prio(a.1),
431 if a.1.id == "APIC" { a.0 } else { a.2.len() },
432 a.1.hash_key(),
433 );
434 let kb = (
435 prio(b.1),
436 if b.1.id == "APIC" { b.0 } else { b.2.len() },
437 b.1.hash_key(),
438 );
439 ka.cmp(&kb)
440 });
441 items.into_iter().flat_map(|(_, _, d)| d).collect()
442 }
443
444 pub fn save(&self, path: &Path) -> io::Result<()> {
446 let file = fs::read(path).unwrap_or_default();
447 let old_size = existing_tag_size(&file).min(file.len());
448 let frames = self.frame_data();
449 let needed = frames.len() + 10;
450 let trailing = file.len() as i64;
451 let available = old_size as i64 - needed as i64;
452 let high = 1024 * 10 + trailing / 100;
453 let low = 1024 + trailing / 1000;
454 let padding = if available >= 0 {
455 if available > high {
456 low
457 } else {
458 available
459 }
460 } else {
461 low
462 } as usize;
463 let new_size = needed + padding;
464 let mut out = Vec::with_capacity(new_size + file.len() - old_size);
465 out.extend_from_slice(b"ID3");
466 out.extend_from_slice(&[4, 0, 0]);
467 out.extend_from_slice(&syncsafe(new_size - 10));
468 out.extend(frames);
469 out.resize(new_size, 0);
470 out.extend_from_slice(&file[old_size..]);
471 fs::write(path, out)
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478
479 #[test]
480 fn syncsafe_round_trip() {
481 assert_eq!(syncsafe(1024), [0, 0, 8, 0]);
482 assert_eq!(read_syncsafe(&syncsafe(123_456)), 123_456);
483 }
484
485 #[test]
486 fn text_frames_round_trip_and_sort() {
487 let tmp = tempfile::tempdir().unwrap();
488 let p = tmp.path().join("a.mp3");
489 fs::write(&p, b"\xff\xfb\x90\x00audio").unwrap();
490 let mut t = Tag::default();
491 t.add(Frame::text("TCON", "Podcast"));
492 t.add(Frame::text("TALB", "Show"));
493 t.add(Frame::text("TIT2", "Title"));
494 t.add(Frame::text("TPE1", ""));
495 t.save(&p).unwrap();
496 let data = fs::read(&p).unwrap();
497 let loaded = Tag::load(&data).unwrap();
498 let ids: Vec<&str> = loaded.frames.values().map(|f| f.id.as_str()).collect();
499 assert_eq!(
500 ids,
501 ["TIT2", "TALB", "TCON"],
502 "empty TPE1 dropped, priority order"
503 );
504 assert!(data.ends_with(b"\xff\xfb\x90\x00audio"));
505 assert_eq!(existing_tag_size(&data), 10 + 17 + 16 + 19 + 1024);
507 }
508}