1use super::constants::{GENRES, ID3V1_TAG_MARKER};
2use super::tag::Id3v1Tag;
3use crate::config::ParsingMode;
4use crate::error::LoftyError;
5use crate::macros::err;
6use crate::util::text::latin1_decode;
7
8impl Id3v1Tag {
9 #[doc(hidden)]
11 pub fn parse(reader: [u8; 128], parse_mode: ParsingMode) -> Result<Self, LoftyError> {
12 let mut tag = Self {
13 title: None,
14 artist: None,
15 album: None,
16 year: None,
17 comment: None,
18 track_number: None,
19 genre: None,
20 };
21
22 if reader[..3] != ID3V1_TAG_MARKER {
23 err!(FakeTag);
24 }
25
26 let reader = &reader[3..];
27
28 tag.title = decode_text(&reader[..30]);
29 tag.artist = decode_text(&reader[30..60]);
30 tag.album = decode_text(&reader[60..90]);
31
32 tag.year = try_parse_year(&reader[90..94], parse_mode)?;
33
34 let range = if reader[122] == 0 && reader[123] != 0 {
38 tag.track_number = Some(reader[123]);
39
40 94_usize..123
41 } else {
42 94..124
43 };
44
45 tag.comment = decode_text(&reader[range]);
46
47 if reader[124] < GENRES.len() as u8 {
48 tag.genre = Some(reader[124]);
49 }
50
51 Ok(tag)
52 }
53}
54
55fn decode_text(data: &[u8]) -> Option<String> {
56 let mut first_null_pos = data.len();
57 if let Some(null_pos) = data.iter().position(|&b| b == 0) {
58 if null_pos == 0 {
59 return None;
60 }
61
62 if data[null_pos..].iter().any(|b| *b != b'\0') {
63 log::warn!("ID3v1 text field contains trailing junk, skipping");
64 }
65
66 first_null_pos = null_pos;
67 }
68
69 Some(latin1_decode(&data[..first_null_pos]))
70}
71
72fn try_parse_year(input: &[u8], parse_mode: ParsingMode) -> Result<Option<u16>, LoftyError> {
73 let (num_digits, year) = input
74 .iter()
75 .take_while(|c| (**c).is_ascii_digit())
76 .fold((0usize, 0u16), |(num_digits, year), c| {
77 (num_digits + 1, year * 10 + u16::from(*c - b'0'))
78 });
79 if num_digits != 4 {
80 if parse_mode == ParsingMode::Strict {
84 err!(TextDecode(
85 "ID3v1 year field contains non-ASCII digit characters"
86 ));
87 }
88
89 return Ok(None);
90 }
91
92 Ok(Some(year))
93}