1use std::io::{Read, Seek, SeekFrom, Write};
2
3use crate::error::{ApeError, ApeResult};
4
5pub const APE_TAG_FLAG_CONTAINS_HEADER: u32 = 1 << 31;
10pub const APE_TAG_FLAG_CONTAINS_FOOTER: u32 = 1 << 30;
11pub const APE_TAG_FLAG_IS_HEADER: u32 = 1 << 29;
12
13pub const TAG_FIELD_FLAG_READ_ONLY: u32 = 1 << 0;
14pub const TAG_FIELD_FLAG_DATA_TYPE_MASK: u32 = 0x06;
15pub const TAG_FIELD_FLAG_DATA_TYPE_TEXT_UTF8: u32 = 0 << 1;
16pub const TAG_FIELD_FLAG_DATA_TYPE_BINARY: u32 = 1 << 1;
17pub const TAG_FIELD_FLAG_DATA_TYPE_EXTERNAL_INFO: u32 = 2 << 1;
18pub const TAG_FIELD_FLAG_DATA_TYPE_RESERVED: u32 = 3 << 1;
19
20const APE_TAG_FOOTER_BYTES: u32 = 32;
21const APE_TAG_MAGIC: &[u8; 8] = b"APETAGEX";
22const ID3V1_TAG_BYTES: u64 = 128;
23const MAX_FIELD_DATA_BYTES: u32 = 256 * 1024 * 1024;
24const MAX_TAG_FIELDS: u32 = 65536;
25const MAX_TAG_VERSION: u32 = 2000;
26
27pub mod field_names {
32 pub const TITLE: &str = "Title";
33 pub const ARTIST: &str = "Artist";
34 pub const ALBUM: &str = "Album";
35 pub const ALBUM_ARTIST: &str = "Album Artist";
36 pub const COMMENT: &str = "Comment";
37 pub const YEAR: &str = "Year";
38 pub const TRACK: &str = "Track";
39 pub const DISC: &str = "Disc";
40 pub const GENRE: &str = "Genre";
41 pub const COVER_ART_FRONT: &str = "Cover Art (front)";
42 pub const NOTES: &str = "Notes";
43 pub const LYRICS: &str = "Lyrics";
44 pub const COPYRIGHT: &str = "Copyright";
45 pub const BUY_URL: &str = "Buy URL";
46 pub const ARTIST_URL: &str = "Artist URL";
47 pub const PUBLISHER_URL: &str = "Publisher URL";
48 pub const FILE_URL: &str = "File URL";
49 pub const COPYRIGHT_URL: &str = "Copyright URL";
50 pub const TOOL_NAME: &str = "Tool Name";
51 pub const TOOL_VERSION: &str = "Tool Version";
52 pub const PEAK_LEVEL: &str = "Peak Level";
53 pub const REPLAY_GAIN_RADIO: &str = "Replay Gain (radio)";
54 pub const REPLAY_GAIN_ALBUM: &str = "Replay Gain (album)";
55 pub const COMPOSER: &str = "Composer";
56 pub const CONDUCTOR: &str = "Conductor";
57 pub const ORCHESTRA: &str = "Orchestra";
58 pub const KEYWORDS: &str = "Keywords";
59 pub const RATING: &str = "Rating";
60 pub const PUBLISHER: &str = "Publisher";
61 pub const BPM: &str = "BPM";
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum TagFieldType {
70 TextUtf8,
71 Binary,
72 ExternalInfo,
73 Reserved,
74}
75
76#[derive(Debug, Clone)]
81pub struct ApeTagField {
82 pub name: String,
83 pub value: Vec<u8>,
84 pub flags: u32,
85}
86
87impl ApeTagField {
88 pub fn field_type(&self) -> TagFieldType {
90 match self.flags & TAG_FIELD_FLAG_DATA_TYPE_MASK {
91 TAG_FIELD_FLAG_DATA_TYPE_TEXT_UTF8 => TagFieldType::TextUtf8,
92 TAG_FIELD_FLAG_DATA_TYPE_BINARY => TagFieldType::Binary,
93 TAG_FIELD_FLAG_DATA_TYPE_EXTERNAL_INFO => TagFieldType::ExternalInfo,
94 _ => TagFieldType::Reserved,
95 }
96 }
97
98 pub fn is_read_only(&self) -> bool {
100 self.flags & TAG_FIELD_FLAG_READ_ONLY != 0
101 }
102
103 pub fn value_as_str(&self) -> Option<&str> {
106 if self.field_type() != TagFieldType::TextUtf8 {
107 return None;
108 }
109 std::str::from_utf8(&self.value).ok()
110 }
111}
112
113#[derive(Debug, Clone)]
118pub struct ApeTag {
119 pub version: u32,
120 pub fields: Vec<ApeTagField>,
121 pub has_header: bool,
122}
123
124impl ApeTag {
125 pub fn field(&self, name: &str) -> Option<&ApeTagField> {
127 let name_lower = name.to_ascii_lowercase();
128 self.fields
129 .iter()
130 .find(|f| f.name.to_ascii_lowercase() == name_lower)
131 }
132
133 pub fn get(&self, name: &str) -> Option<&str> {
135 self.field(name).and_then(|f| f.value_as_str())
136 }
137
138 pub fn title(&self) -> Option<&str> {
140 self.get(field_names::TITLE)
141 }
142
143 pub fn artist(&self) -> Option<&str> {
145 self.get(field_names::ARTIST)
146 }
147
148 pub fn album(&self) -> Option<&str> {
150 self.get(field_names::ALBUM)
151 }
152
153 pub fn year(&self) -> Option<&str> {
155 self.get(field_names::YEAR)
156 }
157
158 pub fn track(&self) -> Option<&str> {
160 self.get(field_names::TRACK)
161 }
162
163 pub fn genre(&self) -> Option<&str> {
165 self.get(field_names::GENRE)
166 }
167
168 pub fn comment(&self) -> Option<&str> {
170 self.get(field_names::COMMENT)
171 }
172
173 pub fn new() -> Self {
177 ApeTag {
178 version: 2000,
179 fields: Vec::new(),
180 has_header: true,
181 }
182 }
183
184 pub fn set(&mut self, name: &str, value: &str) {
187 let name_lower = name.to_ascii_lowercase();
188 if let Some(field) = self
189 .fields
190 .iter_mut()
191 .find(|f| f.name.to_ascii_lowercase() == name_lower)
192 {
193 field.value = value.as_bytes().to_vec();
194 field.flags = TAG_FIELD_FLAG_DATA_TYPE_TEXT_UTF8;
195 } else {
196 self.fields.push(ApeTagField {
197 name: name.to_string(),
198 value: value.as_bytes().to_vec(),
199 flags: TAG_FIELD_FLAG_DATA_TYPE_TEXT_UTF8,
200 });
201 }
202 }
203
204 pub fn set_binary(&mut self, name: &str, value: Vec<u8>) {
206 let name_lower = name.to_ascii_lowercase();
207 if let Some(field) = self
208 .fields
209 .iter_mut()
210 .find(|f| f.name.to_ascii_lowercase() == name_lower)
211 {
212 field.value = value;
213 field.flags = TAG_FIELD_FLAG_DATA_TYPE_BINARY;
214 } else {
215 self.fields.push(ApeTagField {
216 name: name.to_string(),
217 value,
218 flags: TAG_FIELD_FLAG_DATA_TYPE_BINARY,
219 });
220 }
221 }
222
223 pub fn remove(&mut self, name: &str) -> bool {
225 let name_lower = name.to_ascii_lowercase();
226 let before = self.fields.len();
227 self.fields
228 .retain(|f| f.name.to_ascii_lowercase() != name_lower);
229 self.fields.len() != before
230 }
231
232 pub fn to_bytes(&self) -> Vec<u8> {
234 let mut field_data = Vec::new();
236 for field in &self.fields {
237 field_data.extend_from_slice(&(field.value.len() as u32).to_le_bytes());
238 field_data.extend_from_slice(&field.flags.to_le_bytes());
239 field_data.extend_from_slice(field.name.as_bytes());
240 field_data.push(0); field_data.extend_from_slice(&field.value);
242 }
243
244 let tag_size = field_data.len() as u32 + APE_TAG_FOOTER_BYTES;
245 let tag_flags = APE_TAG_FLAG_CONTAINS_FOOTER
246 | if self.has_header {
247 APE_TAG_FLAG_CONTAINS_HEADER
248 } else {
249 0
250 };
251
252 let mut result = Vec::new();
253
254 if self.has_header {
256 result.extend_from_slice(APE_TAG_MAGIC);
257 result.extend_from_slice(&self.version.to_le_bytes());
258 result.extend_from_slice(&tag_size.to_le_bytes());
259 result.extend_from_slice(&(self.fields.len() as u32).to_le_bytes());
260 result.extend_from_slice(&(tag_flags | APE_TAG_FLAG_IS_HEADER).to_le_bytes());
261 result.extend_from_slice(&[0u8; 8]); }
263
264 result.extend_from_slice(&field_data);
266
267 result.extend_from_slice(APE_TAG_MAGIC);
269 result.extend_from_slice(&self.version.to_le_bytes());
270 result.extend_from_slice(&tag_size.to_le_bytes());
271 result.extend_from_slice(&(self.fields.len() as u32).to_le_bytes());
272 result.extend_from_slice(&tag_flags.to_le_bytes());
273 result.extend_from_slice(&[0u8; 8]); result
276 }
277}
278
279pub fn write_tag<W: Read + Write + Seek>(writer: &mut W, tag: &ApeTag) -> ApeResult<()> {
286 let file_size = writer.seek(SeekFrom::End(0))?;
287
288 let mut id3v1_data: Option<[u8; 128]> = None;
290 if file_size >= ID3V1_TAG_BYTES {
291 writer.seek(SeekFrom::End(-(ID3V1_TAG_BYTES as i64)))?;
292 let mut buf = [0u8; 128];
293 writer.read_exact(&mut buf)?;
294 if &buf[0..3] == b"TAG" {
295 id3v1_data = Some(buf);
296 }
297 }
298
299 let footer_offset = if id3v1_data.is_some() {
301 file_size - ID3V1_TAG_BYTES - APE_TAG_FOOTER_BYTES as u64
302 } else {
303 file_size - APE_TAG_FOOTER_BYTES as u64
304 };
305
306 let mut truncate_to = if id3v1_data.is_some() {
307 file_size - ID3V1_TAG_BYTES
308 } else {
309 file_size
310 };
311
312 if footer_offset < file_size {
313 writer.seek(SeekFrom::Start(footer_offset))?;
314 let mut footer_buf = [0u8; 32];
315 if writer.read_exact(&mut footer_buf).is_ok() && &footer_buf[0..8] == APE_TAG_MAGIC {
316 let existing_size = u32::from_le_bytes([
318 footer_buf[12],
319 footer_buf[13],
320 footer_buf[14],
321 footer_buf[15],
322 ]);
323 let existing_flags = u32::from_le_bytes([
324 footer_buf[20],
325 footer_buf[21],
326 footer_buf[22],
327 footer_buf[23],
328 ]);
329 let has_existing_header = existing_flags & APE_TAG_FLAG_CONTAINS_HEADER != 0;
330 let total_existing = existing_size as u64
331 + if has_existing_header {
332 APE_TAG_FOOTER_BYTES as u64
333 } else {
334 0
335 };
336 truncate_to = truncate_to.saturating_sub(total_existing);
337 }
338 }
339
340 writer.seek(SeekFrom::Start(truncate_to))?;
342 let tag_bytes = tag.to_bytes();
344 writer.write_all(&tag_bytes)?;
345
346 if let Some(id3v1) = id3v1_data {
348 writer.write_all(&id3v1)?;
349 }
350
351 Ok(())
355}
356
357pub fn remove_tag<W: Read + Write + Seek>(writer: &mut W) -> ApeResult<()> {
359 let file_size = writer.seek(SeekFrom::End(0))?;
360
361 let mut has_id3v1 = false;
363 let mut id3v1_data = [0u8; 128];
364 if file_size >= ID3V1_TAG_BYTES {
365 writer.seek(SeekFrom::End(-(ID3V1_TAG_BYTES as i64)))?;
366 writer.read_exact(&mut id3v1_data)?;
367 has_id3v1 = &id3v1_data[0..3] == b"TAG";
368 }
369
370 let footer_offset = if has_id3v1 {
372 file_size - ID3V1_TAG_BYTES - APE_TAG_FOOTER_BYTES as u64
373 } else {
374 file_size - APE_TAG_FOOTER_BYTES as u64
375 };
376
377 if footer_offset >= file_size {
378 return Ok(()); }
380
381 writer.seek(SeekFrom::Start(footer_offset))?;
382 let mut footer_buf = [0u8; 32];
383 if writer.read_exact(&mut footer_buf).is_err() || &footer_buf[0..8] != APE_TAG_MAGIC {
384 return Ok(()); }
386
387 let tag_size = u32::from_le_bytes([
389 footer_buf[12],
390 footer_buf[13],
391 footer_buf[14],
392 footer_buf[15],
393 ]);
394 let tag_flags = u32::from_le_bytes([
395 footer_buf[20],
396 footer_buf[21],
397 footer_buf[22],
398 footer_buf[23],
399 ]);
400 let has_header = tag_flags & APE_TAG_FLAG_CONTAINS_HEADER != 0;
401 let total_tag_bytes = tag_size as u64
402 + if has_header {
403 APE_TAG_FOOTER_BYTES as u64
404 } else {
405 0
406 };
407
408 let base = if has_id3v1 {
410 file_size - ID3V1_TAG_BYTES
411 } else {
412 file_size
413 };
414 let truncate_to = base.saturating_sub(total_tag_bytes);
415 writer.seek(SeekFrom::Start(truncate_to))?;
416
417 if has_id3v1 {
419 writer.write_all(&id3v1_data)?;
420 }
421
422 Ok(())
423}
424
425pub fn read_tag<R: Read + Seek>(reader: &mut R) -> ApeResult<Option<ApeTag>> {
434 let file_size = reader.seek(SeekFrom::End(0))?;
436
437 if file_size < APE_TAG_FOOTER_BYTES as u64 {
439 return Ok(None);
440 }
441
442 let has_id3v1 = if file_size >= ID3V1_TAG_BYTES {
444 reader.seek(SeekFrom::End(-(ID3V1_TAG_BYTES as i64)))?;
445 let mut id3_header = [0u8; 3];
446 reader.read_exact(&mut id3_header)?;
447 &id3_header == b"TAG"
448 } else {
449 false
450 };
451
452 let footer_end = if has_id3v1 {
454 file_size - ID3V1_TAG_BYTES
455 } else {
456 file_size
457 };
458
459 if footer_end < APE_TAG_FOOTER_BYTES as u64 {
460 return Ok(None);
461 }
462
463 let footer_start = footer_end - APE_TAG_FOOTER_BYTES as u64;
464
465 reader.seek(SeekFrom::Start(footer_start))?;
467 let mut footer_buf = [0u8; 32];
468 reader.read_exact(&mut footer_buf)?;
469
470 if &footer_buf[0..8] != APE_TAG_MAGIC {
472 return Ok(None);
473 }
474
475 let version = u32::from_le_bytes(footer_buf[8..12].try_into().unwrap());
477 let size = u32::from_le_bytes(footer_buf[12..16].try_into().unwrap());
478 let num_fields = u32::from_le_bytes(footer_buf[16..20].try_into().unwrap());
479 let flags = u32::from_le_bytes(footer_buf[20..24].try_into().unwrap());
480
481 if flags & APE_TAG_FLAG_IS_HEADER != 0 {
483 return Ok(None);
484 }
485
486 if version > MAX_TAG_VERSION {
488 return Err(ApeError::InvalidFormat("APE tag version too high"));
489 }
490 if num_fields > MAX_TAG_FIELDS {
491 return Err(ApeError::InvalidFormat("APE tag has too many fields"));
492 }
493 if size < APE_TAG_FOOTER_BYTES {
494 return Err(ApeError::InvalidFormat("APE tag size too small"));
495 }
496 let field_bytes = size - APE_TAG_FOOTER_BYTES;
497 if field_bytes > MAX_FIELD_DATA_BYTES {
498 return Err(ApeError::InvalidFormat("APE tag field data too large"));
499 }
500
501 let has_header = flags & APE_TAG_FLAG_CONTAINS_HEADER != 0;
502
503 let field_data_start = footer_end
506 .checked_sub(size as u64)
507 .ok_or(ApeError::InvalidFormat(
508 "APE tag size extends before start of file",
509 ))?;
510
511 if field_bytes == 0 {
512 return Ok(Some(ApeTag {
513 version,
514 fields: Vec::new(),
515 has_header,
516 }));
517 }
518
519 reader.seek(SeekFrom::Start(field_data_start))?;
521 let mut field_data = vec![0u8; field_bytes as usize];
522 reader.read_exact(&mut field_data)?;
523
524 let fields = parse_fields(&field_data, num_fields)?;
526
527 Ok(Some(ApeTag {
528 version,
529 fields,
530 has_header,
531 }))
532}
533
534fn parse_fields(data: &[u8], num_fields: u32) -> ApeResult<Vec<ApeTagField>> {
536 let mut fields = Vec::with_capacity(num_fields as usize);
537 let mut offset = 0usize;
538
539 for _ in 0..num_fields {
540 if offset + 8 > data.len() {
542 return Err(ApeError::InvalidFormat("APE tag field truncated (header)"));
543 }
544
545 let value_size = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
546 let field_flags = u32::from_le_bytes(data[offset + 4..offset + 8].try_into().unwrap());
547 offset += 8;
548
549 let name_start = offset;
551 let name_end = data[name_start..]
552 .iter()
553 .position(|&b| b == 0)
554 .map(|pos| name_start + pos)
555 .ok_or(ApeError::InvalidFormat(
556 "APE tag field name not null-terminated",
557 ))?;
558
559 let name_bytes = &data[name_start..name_end];
561 if name_bytes.is_empty() {
562 return Err(ApeError::InvalidFormat("APE tag field name is empty"));
563 }
564 for &b in name_bytes {
565 if b < 0x20 || b > 0x7E {
566 return Err(ApeError::InvalidFormat(
567 "APE tag field name contains non-printable character",
568 ));
569 }
570 }
571 let name = String::from_utf8(name_bytes.to_vec())
572 .map_err(|_| ApeError::InvalidFormat("APE tag field name is not valid ASCII"))?;
573
574 offset = name_end + 1;
576
577 let value_size = value_size as usize;
579 if offset + value_size > data.len() {
580 return Err(ApeError::InvalidFormat("APE tag field value truncated"));
581 }
582 let value = data[offset..offset + value_size].to_vec();
583 offset += value_size;
584
585 fields.push(ApeTagField {
586 name,
587 value,
588 flags: field_flags,
589 });
590 }
591
592 Ok(fields)
593}
594
595#[cfg(test)]
600mod tests {
601 use super::*;
602 use std::io::Cursor;
603
604 fn build_tag(fields: &[(&str, &[u8], u32)], with_id3v1: bool, with_header: bool) -> Vec<u8> {
608 let mut field_data = Vec::new();
609 for &(name, value, flags) in fields {
610 field_data.extend_from_slice(&(value.len() as u32).to_le_bytes());
611 field_data.extend_from_slice(&flags.to_le_bytes());
612 field_data.extend_from_slice(name.as_bytes());
613 field_data.push(0); field_data.extend_from_slice(value);
615 }
616
617 let field_bytes = field_data.len() as u32;
618 let tag_size = field_bytes + APE_TAG_FOOTER_BYTES;
619
620 let mut tag_flags = APE_TAG_FLAG_CONTAINS_FOOTER;
621 if with_header {
622 tag_flags |= APE_TAG_FLAG_CONTAINS_HEADER;
623 }
624
625 let mut footer = Vec::new();
627 footer.extend_from_slice(APE_TAG_MAGIC);
628 footer.extend_from_slice(&2000u32.to_le_bytes());
629 footer.extend_from_slice(&tag_size.to_le_bytes());
630 footer.extend_from_slice(&(fields.len() as u32).to_le_bytes());
631 footer.extend_from_slice(&tag_flags.to_le_bytes());
632 footer.extend_from_slice(&[0u8; 8]);
633
634 let mut buf = Vec::new();
635
636 if with_header {
638 let mut header = Vec::new();
639 header.extend_from_slice(APE_TAG_MAGIC);
640 header.extend_from_slice(&2000u32.to_le_bytes());
641 header.extend_from_slice(&tag_size.to_le_bytes());
642 header.extend_from_slice(&(fields.len() as u32).to_le_bytes());
643 header.extend_from_slice(&(tag_flags | APE_TAG_FLAG_IS_HEADER).to_le_bytes());
644 header.extend_from_slice(&[0u8; 8]);
645 buf.extend_from_slice(&header);
646 }
647
648 buf.extend_from_slice(&field_data);
649 buf.extend_from_slice(&footer);
650
651 if with_id3v1 {
652 let mut id3v1 = vec![0u8; 128];
653 id3v1[0] = b'T';
654 id3v1[1] = b'A';
655 id3v1[2] = b'G';
656 buf.extend_from_slice(&id3v1);
657 }
658
659 buf
660 }
661
662 fn build_mac_tool_tag() -> Vec<u8> {
664 build_tag(
665 &[
666 (field_names::TOOL_NAME, b"Monkey's Audio", 0),
667 (field_names::TOOL_VERSION, b"10.44", 0),
668 (field_names::TITLE, b"Sine Wave", 0),
669 (field_names::ARTIST, b"Test Generator", 0),
670 (field_names::ALBUM, b"Test Signals", 0),
671 (field_names::YEAR, b"2024", 0),
672 (field_names::TRACK, b"1", 0),
673 (field_names::GENRE, b"Test", 0),
674 (field_names::COMMENT, b"Generated for testing", 0),
675 ],
676 false,
677 false,
678 )
679 }
680
681 #[test]
682 fn read_tag_from_fixture() {
683 let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
686 .join("tests/fixtures/ape/sine_16s_c2000.ape");
687 let mut file = std::fs::File::open(&path).expect("failed to open test fixture");
688 let result = read_tag(&mut file).expect("read_tag should not error");
689 assert!(result.is_none(), "fixture has no APE tag");
690 }
691
692 #[test]
693 fn tool_name_and_version_fields_exist() {
694 let data = build_mac_tool_tag();
695 let mut cursor = Cursor::new(data);
696 let tag = read_tag(&mut cursor)
697 .expect("read_tag failed")
698 .expect("expected tag");
699
700 let tool_name = tag.get(field_names::TOOL_NAME);
701 assert_eq!(tool_name, Some("Monkey's Audio"));
702
703 let tool_version = tag.get(field_names::TOOL_VERSION);
704 assert_eq!(tool_version, Some("10.44"));
705 }
706
707 #[test]
708 fn case_insensitive_field_lookup() {
709 let data = build_mac_tool_tag();
710 let mut cursor = Cursor::new(data);
711 let tag = read_tag(&mut cursor)
712 .expect("read_tag failed")
713 .expect("expected tag");
714
715 let upper = tag.get("TOOL NAME");
717 let lower = tag.get("tool name");
718 let mixed = tag.get("Tool Name");
719
720 assert_eq!(upper, mixed);
721 assert_eq!(lower, mixed);
722 assert_eq!(mixed, Some("Monkey's Audio"));
723
724 assert_eq!(tag.title(), Some("Sine Wave"));
726 assert_eq!(tag.artist(), Some("Test Generator"));
727 assert_eq!(tag.album(), Some("Test Signals"));
728 assert_eq!(tag.year(), Some("2024"));
729 assert_eq!(tag.track(), Some("1"));
730 assert_eq!(tag.genre(), Some("Test"));
731 assert_eq!(tag.comment(), Some("Generated for testing"));
732 }
733
734 #[test]
735 fn nonexistent_field_returns_none() {
736 let data = build_mac_tool_tag();
737 let mut cursor = Cursor::new(data);
738 let tag = read_tag(&mut cursor)
739 .expect("read_tag failed")
740 .expect("expected tag");
741
742 assert!(tag.field("Nonexistent Field 12345").is_none());
743 assert!(tag.get("Nonexistent Field 12345").is_none());
744 }
745
746 #[test]
747 fn value_as_str_for_text_fields() {
748 let data = build_mac_tool_tag();
749 let mut cursor = Cursor::new(data);
750 let tag = read_tag(&mut cursor)
751 .expect("read_tag failed")
752 .expect("expected tag");
753
754 for field in &tag.fields {
755 if field.field_type() == TagFieldType::TextUtf8 {
756 assert!(
757 field.value_as_str().is_some(),
758 "text field '{}' should be valid UTF-8",
759 field.name
760 );
761 }
762 }
763 }
764
765 #[test]
766 fn value_as_str_returns_none_for_binary() {
767 let field = ApeTagField {
768 name: "test".to_string(),
769 value: vec![0xFF, 0xFE],
770 flags: TAG_FIELD_FLAG_DATA_TYPE_BINARY,
771 };
772 assert!(field.value_as_str().is_none());
773 }
774
775 #[test]
776 fn field_type_classification() {
777 let text = ApeTagField {
778 name: "t".into(),
779 value: vec![],
780 flags: TAG_FIELD_FLAG_DATA_TYPE_TEXT_UTF8,
781 };
782 assert_eq!(text.field_type(), TagFieldType::TextUtf8);
783
784 let binary = ApeTagField {
785 name: "b".into(),
786 value: vec![],
787 flags: TAG_FIELD_FLAG_DATA_TYPE_BINARY,
788 };
789 assert_eq!(binary.field_type(), TagFieldType::Binary);
790
791 let external = ApeTagField {
792 name: "e".into(),
793 value: vec![],
794 flags: TAG_FIELD_FLAG_DATA_TYPE_EXTERNAL_INFO,
795 };
796 assert_eq!(external.field_type(), TagFieldType::ExternalInfo);
797
798 let reserved = ApeTagField {
799 name: "r".into(),
800 value: vec![],
801 flags: TAG_FIELD_FLAG_DATA_TYPE_RESERVED,
802 };
803 assert_eq!(reserved.field_type(), TagFieldType::Reserved);
804 }
805
806 #[test]
807 fn is_read_only_flag() {
808 let ro = ApeTagField {
809 name: "ro".into(),
810 value: vec![],
811 flags: TAG_FIELD_FLAG_READ_ONLY,
812 };
813 assert!(ro.is_read_only());
814
815 let rw = ApeTagField {
816 name: "rw".into(),
817 value: vec![],
818 flags: 0,
819 };
820 assert!(!rw.is_read_only());
821 }
822
823 #[test]
824 fn file_too_small_returns_none() {
825 let data = vec![0u8; 16];
826 let mut cursor = Cursor::new(data);
827 let result = read_tag(&mut cursor).expect("should not error");
828 assert!(result.is_none());
829 }
830
831 #[test]
832 fn no_tag_returns_none() {
833 let data = vec![0u8; 64];
835 let mut cursor = Cursor::new(data);
836 let result = read_tag(&mut cursor).expect("should not error");
837 assert!(result.is_none());
838 }
839
840 #[test]
841 fn synthetic_minimal_tag() {
842 let data = build_tag(&[("Test", b"Hello", 0)], false, false);
843 let mut cursor = Cursor::new(data);
844 let tag = read_tag(&mut cursor)
845 .expect("read_tag failed")
846 .expect("expected tag");
847
848 assert_eq!(tag.version, 2000);
849 assert_eq!(tag.fields.len(), 1);
850 assert_eq!(tag.get("Test"), Some("Hello"));
851 assert_eq!(tag.get("test"), Some("Hello")); assert!(!tag.has_header);
853 }
854
855 #[test]
856 fn synthetic_tag_with_id3v1() {
857 let data = build_tag(&[("Foo", b"Bar", 0)], true, false);
858 let mut cursor = Cursor::new(data);
859 let tag = read_tag(&mut cursor)
860 .expect("read_tag failed")
861 .expect("expected tag");
862
863 assert_eq!(tag.get("Foo"), Some("Bar"));
864 }
865
866 #[test]
867 fn synthetic_tag_with_header() {
868 let data = build_tag(&[("Artist", b"Someone", 0)], false, true);
869 let mut cursor = Cursor::new(data);
870 let tag = read_tag(&mut cursor)
871 .expect("read_tag failed")
872 .expect("expected tag");
873
874 assert!(tag.has_header);
875 assert_eq!(tag.artist(), Some("Someone"));
876 }
877
878 #[test]
879 fn multiple_fields_parsed_correctly() {
880 let data = build_mac_tool_tag();
881 let mut cursor = Cursor::new(data);
882 let tag = read_tag(&mut cursor)
883 .expect("read_tag failed")
884 .expect("expected tag");
885
886 assert_eq!(tag.version, 2000);
887 assert_eq!(tag.fields.len(), 9);
888 }
889
890 #[test]
893 fn new_tag_is_empty() {
894 let tag = ApeTag::new();
895 assert_eq!(tag.version, 2000);
896 assert!(tag.fields.is_empty());
897 assert!(tag.has_header);
898 }
899
900 #[test]
901 fn set_and_get_text_field() {
902 let mut tag = ApeTag::new();
903 tag.set("Title", "Test Song");
904 tag.set("Artist", "Test Artist");
905
906 assert_eq!(tag.title(), Some("Test Song"));
907 assert_eq!(tag.artist(), Some("Test Artist"));
908 }
909
910 #[test]
911 fn set_updates_existing_field() {
912 let mut tag = ApeTag::new();
913 tag.set("Title", "Original");
914 tag.set("Title", "Updated");
915
916 assert_eq!(tag.title(), Some("Updated"));
917 assert_eq!(tag.fields.len(), 1); }
919
920 #[test]
921 fn remove_field() {
922 let mut tag = ApeTag::new();
923 tag.set("Title", "Song");
924 tag.set("Artist", "Band");
925
926 assert!(tag.remove("title")); assert_eq!(tag.title(), None);
928 assert_eq!(tag.artist(), Some("Band"));
929 assert!(!tag.remove("Title")); }
931
932 #[test]
933 fn serialize_and_parse_roundtrip() {
934 let mut tag = ApeTag::new();
935 tag.set("Title", "Round Trip");
936 tag.set("Artist", "Tester");
937 tag.set("Year", "2026");
938
939 let bytes = tag.to_bytes();
940 let mut cursor = Cursor::new(bytes);
941 let parsed = read_tag(&mut cursor).unwrap().expect("should parse");
942
943 assert_eq!(parsed.version, 2000);
944 assert_eq!(parsed.fields.len(), 3);
945 assert_eq!(parsed.get("Title"), Some("Round Trip"));
946 assert_eq!(parsed.get("Artist"), Some("Tester"));
947 assert_eq!(parsed.get("Year"), Some("2026"));
948 }
949
950 #[test]
951 fn serialize_empty_tag() {
952 let tag = ApeTag::new();
953 let bytes = tag.to_bytes();
954 assert_eq!(bytes.len(), 64);
956
957 let mut cursor = Cursor::new(bytes);
958 let parsed = read_tag(&mut cursor)
959 .unwrap()
960 .expect("should parse empty tag");
961 assert!(parsed.fields.is_empty());
962 }
963
964 #[test]
965 fn write_tag_to_file() {
966 let mut file_data = vec![0xAA; 100];
968 let mut cursor = Cursor::new(&mut file_data);
969
970 let mut tag = ApeTag::new();
971 tag.set("Title", "Written");
972 tag.set("Artist", "Writer");
973
974 write_tag(&mut cursor, &tag).unwrap();
975
976 let data = cursor.into_inner();
978 let mut read_cursor = Cursor::new(data.as_slice());
979 let parsed = read_tag(&mut read_cursor)
980 .unwrap()
981 .expect("should read written tag");
982
983 assert_eq!(parsed.get("Title"), Some("Written"));
984 assert_eq!(parsed.get("Artist"), Some("Writer"));
985 }
986
987 #[test]
988 fn write_tag_replaces_existing() {
989 let mut tag1 = ApeTag::new();
991 tag1.set("Title", "First");
992 let tag1_bytes = tag1.to_bytes();
993
994 let mut file_data: Vec<u8> = vec![0xBB; 50];
995 file_data.extend_from_slice(&tag1_bytes);
996
997 let mut cursor = Cursor::new(file_data);
998
999 let mut tag2 = ApeTag::new();
1001 tag2.set("Title", "Second");
1002 tag2.set("Album", "New Album");
1003 write_tag(&mut cursor, &tag2).unwrap();
1004
1005 let data = cursor.into_inner();
1007 let mut read_cursor = Cursor::new(data.as_slice());
1008 let parsed = read_tag(&mut read_cursor)
1009 .unwrap()
1010 .expect("should read replaced tag");
1011
1012 assert_eq!(parsed.get("Title"), Some("Second"));
1013 assert_eq!(parsed.get("Album"), Some("New Album"));
1014 assert_eq!(parsed.fields.len(), 2); }
1016
1017 #[test]
1018 fn remove_tag_from_file() {
1019 let mut tag = ApeTag::new();
1021 tag.set("Title", "To Remove");
1022 let tag_bytes = tag.to_bytes();
1023
1024 let content_size = 50;
1025 let mut file_data: Vec<u8> = vec![0xCC; content_size];
1026 file_data.extend_from_slice(&tag_bytes);
1027
1028 let mut cursor = Cursor::new(file_data);
1029 remove_tag(&mut cursor).unwrap();
1030
1031 let new_end = cursor.position() as usize;
1034 let mut data = cursor.into_inner();
1035 data.truncate(new_end);
1036
1037 let mut read_cursor = Cursor::new(data.as_slice());
1039 let result = read_tag(&mut read_cursor).unwrap();
1040 assert!(result.is_none());
1041 assert_eq!(new_end, content_size); }
1043
1044 #[test]
1045 fn set_binary_field() {
1046 let mut tag = ApeTag::new();
1047 tag.set_binary("Cover Art (front)", vec![0xFF, 0xD8, 0xFF, 0xE0]);
1048
1049 let field = tag.field("Cover Art (front)").unwrap();
1050 assert_eq!(field.field_type(), TagFieldType::Binary);
1051 assert_eq!(field.value, vec![0xFF, 0xD8, 0xFF, 0xE0]);
1052 assert!(field.value_as_str().is_none()); }
1054}