Skip to main content

lofty/id3/v1/
read.rs

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	/// This is **NOT** a public API
10	#[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		// Determine the range of the comment (30 bytes for ID3v1 and 28 for ID3v1.1)
35		// We check for the null terminator 28 bytes in, and for a non-zero track number after it.
36		// A track number of 0 is invalid.
37		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		// The official test suite says that any year that isn't 4 characters should be a decoding failure.
81		// However, it seems most popular libraries (including us) will write "\0\0\0\0" for empty
82		// years, rather than "0000" as the "spec" would suggest.
83		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}