lofty/id3/v2/items/
encapsulated_object.rs

1use crate::error::{ErrorKind, Id3v2Error, Id3v2ErrorKind, LoftyError, Result};
2use crate::id3::v2::{FrameFlags, FrameHeader, FrameId};
3use crate::util::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text};
4
5use std::io::{Cursor, Read};
6
7const FRAME_ID: FrameId<'static> = FrameId::Valid(std::borrow::Cow::Borrowed("GEOB"));
8
9/// Allows for encapsulation of any file type inside an ID3v2 tag
10#[derive(PartialEq, Clone, Debug, Eq, Hash)]
11pub struct GeneralEncapsulatedObject<'a> {
12	pub(crate) header: FrameHeader<'a>,
13	/// The text encoding of `file_name` and `description`
14	pub encoding: TextEncoding,
15	/// The file's mimetype
16	pub mime_type: Option<String>,
17	/// The file's name
18	pub file_name: Option<String>,
19	/// A unique content descriptor
20	pub descriptor: Option<String>,
21	/// The file's content
22	pub data: Vec<u8>,
23}
24
25impl GeneralEncapsulatedObject<'_> {
26	/// Create a new [`GeneralEncapsulatedObject`]
27	pub fn new(
28		encoding: TextEncoding,
29		mime_type: Option<String>,
30		file_name: Option<String>,
31		descriptor: Option<String>,
32		data: Vec<u8>,
33	) -> Self {
34		let header = FrameHeader::new(FRAME_ID, FrameFlags::default());
35		Self {
36			header,
37			encoding,
38			mime_type,
39			file_name,
40			descriptor,
41			data,
42		}
43	}
44
45	/// Get the ID for the frame
46	pub fn id(&self) -> FrameId<'_> {
47		FRAME_ID
48	}
49
50	/// Get the flags for the frame
51	pub fn flags(&self) -> FrameFlags {
52		self.header.flags
53	}
54
55	/// Set the flags for the frame
56	pub fn set_flags(&mut self, flags: FrameFlags) {
57		self.header.flags = flags;
58	}
59
60	/// Read a [`GeneralEncapsulatedObject`] from a slice
61	///
62	/// NOTE: This expects the frame header to have already been skipped
63	///
64	/// # Errors
65	///
66	/// This function will return an error if at any point it's unable to parse the data
67	pub fn parse(data: &[u8], frame_flags: FrameFlags) -> Result<Self> {
68		if data.len() < 4 {
69			return Err(Id3v2Error::new(Id3v2ErrorKind::BadFrameLength).into());
70		}
71
72		let encoding = TextEncoding::from_u8(data[0])
73			.ok_or_else(|| LoftyError::new(ErrorKind::TextDecode("Found invalid encoding")))?;
74
75		let mut cursor = Cursor::new(&data[1..]);
76
77		let mime_type = decode_text(
78			&mut cursor,
79			TextDecodeOptions::new()
80				.encoding(TextEncoding::Latin1)
81				.terminated(true),
82		)?;
83
84		let text_decode_options = TextDecodeOptions::new().encoding(encoding).terminated(true);
85
86		let file_name = decode_text(&mut cursor, text_decode_options)?;
87		let descriptor = decode_text(&mut cursor, text_decode_options)?;
88
89		let mut data = Vec::new();
90		cursor.read_to_end(&mut data)?;
91
92		let header = FrameHeader::new(FRAME_ID, frame_flags);
93		Ok(Self {
94			header,
95			encoding,
96			mime_type: mime_type.text_or_none(),
97			file_name: file_name.text_or_none(),
98			descriptor: descriptor.text_or_none(),
99			data,
100		})
101	}
102
103	/// Convert a [`GeneralEncapsulatedObject`] into an ID3v2 GEOB frame byte Vec
104	///
105	/// NOTE: This does not include a frame header
106	pub fn as_bytes(&self) -> Vec<u8> {
107		let encoding = self.encoding;
108
109		let mut bytes = vec![encoding as u8];
110
111		if let Some(ref mime_type) = self.mime_type {
112			bytes.extend(mime_type.as_bytes())
113		}
114
115		bytes.push(0);
116
117		let file_name = self.file_name.as_deref();
118		bytes.extend(&*encode_text(file_name.unwrap_or(""), encoding, true));
119
120		let descriptor = self.descriptor.as_deref();
121		bytes.extend(&*encode_text(descriptor.unwrap_or(""), encoding, true));
122
123		bytes.extend(&self.data);
124
125		bytes
126	}
127}
128
129#[cfg(test)]
130mod tests {
131	use crate::id3::v2::{FrameFlags, FrameHeader, GeneralEncapsulatedObject};
132	use crate::util::text::TextEncoding;
133
134	fn expected() -> GeneralEncapsulatedObject<'static> {
135		GeneralEncapsulatedObject {
136			header: FrameHeader::new(super::FRAME_ID, FrameFlags::default()),
137			encoding: TextEncoding::Latin1,
138			mime_type: Some(String::from("audio/mpeg")),
139			file_name: Some(String::from("a.mp3")),
140			descriptor: Some(String::from("Test Asset")),
141			data: crate::tag::utils::test_utils::read_path(
142				"tests/files/assets/minimal/full_test.mp3",
143			),
144		}
145	}
146
147	#[test_log::test]
148	fn geob_decode() {
149		let expected = expected();
150
151		let cont = crate::tag::utils::test_utils::read_path("tests/tags/assets/id3v2/test.geob");
152
153		let parsed_geob = GeneralEncapsulatedObject::parse(&cont, FrameFlags::default()).unwrap();
154
155		assert_eq!(parsed_geob, expected);
156	}
157
158	#[test_log::test]
159	fn geob_encode() {
160		let to_encode = expected();
161
162		let encoded = to_encode.as_bytes();
163
164		let expected_bytes =
165			crate::tag::utils::test_utils::read_path("tests/tags/assets/id3v2/test.geob");
166
167		assert_eq!(encoded, expected_bytes);
168	}
169}