1use std::fs::{File, OpenOptions};
2use std::io::{BufReader, Read, Seek, SeekFrom};
3use std::path::Path;
4
5use id3::{ErrorKind, Tag, Version};
6
7use crate::edit::{TagEdit, WriteOptions};
8use crate::header::DsfHeader;
9use crate::storage::Storage;
10use crate::{Error, Result};
11
12fn load<R: Read + Seek>(r: &mut R) -> Result<(DsfHeader, Option<Tag>)> {
15 r.seek(SeekFrom::Start(0))?;
16 let header = DsfHeader::read(&mut *r)?;
17 let file_len = r.seek(SeekFrom::End(0))?;
18
19 if header.audio_end() > file_len {
20 return Err(Error::Malformed(
21 "file is shorter than its declared audio data",
22 ));
23 }
24 let offset = header.metadata_offset;
25 if offset == 0 || offset == file_len {
26 return Ok((header, None));
27 }
28 if offset < header.audio_end() || offset > file_len {
29 return Err(Error::Malformed("metadata pointer outside the file tail"));
30 }
31
32 r.seek(SeekFrom::Start(offset))?;
33 match Tag::read_from2(&mut *r) {
34 Ok(tag) => Ok((header, Some(tag))),
35 Err(e) if matches!(e.kind, ErrorKind::NoTag) => Ok((header, None)),
36 Err(e) => Err(e.into()),
38 }
39}
40
41fn commit<S: Storage>(s: &mut S, header: &DsfHeader, tag_bytes: Option<&[u8]>) -> Result<()> {
43 let audio_end = header.audio_end();
44 let (offset, new_len) = match tag_bytes {
45 Some(bytes) => {
46 let len = u64::try_from(bytes.len()).map_err(|_| Error::Malformed("tag too large"))?;
47 let end = audio_end
48 .checked_add(len)
49 .ok_or(Error::Malformed("tag too large"))?;
50 s.seek(SeekFrom::Start(audio_end))?;
51 s.write_all(bytes)?;
52 (audio_end, end)
53 }
54 None => (0, audio_end),
55 };
56 s.set_len(new_len)?;
57
58 let mut fields = [0u8; 16];
60 fields[..8].copy_from_slice(&new_len.to_le_bytes());
61 fields[8..].copy_from_slice(&offset.to_le_bytes());
62 s.seek(SeekFrom::Start(12))?;
63 s.write_all(&fields)?;
64 s.sync()?;
65 Ok(())
66}
67
68fn encode(tag: &Tag, version: Version) -> Result<Vec<u8>> {
69 let mut buf = Vec::new();
70 tag.write_to(&mut buf, version)?;
71 Ok(buf)
72}
73
74pub fn read_tag<R: Read + Seek>(r: &mut R) -> Result<Option<Tag>> {
76 Ok(load(r)?.1)
77}
78
79pub fn write_tag<S: Storage>(s: &mut S, tag: &Tag, version: Version) -> Result<()> {
81 let (header, _) = load(s)?;
82 commit(s, &header, Some(&encode(tag, version)?))
83}
84
85pub fn update_tag<S: Storage>(s: &mut S, edit: &TagEdit, opts: &WriteOptions) -> Result<()> {
87 let (header, existing) = load(s)?;
88 let mut tag = existing.unwrap_or_else(Tag::new);
89 edit.apply(&mut tag);
90 let version = opts.version.unwrap_or_else(|| tag.version());
91 commit(s, &header, Some(&encode(&tag, version)?))
92}
93
94pub fn remove_tag<S: Storage>(s: &mut S) -> Result<()> {
96 let (header, _) = load(s)?;
97 commit(s, &header, None)
98}
99
100pub fn read_path(path: impl AsRef<Path>) -> Result<Option<Tag>> {
102 read_tag(&mut BufReader::new(File::open(path)?))
103}
104
105pub fn update_path(path: impl AsRef<Path>, edit: &TagEdit, opts: &WriteOptions) -> Result<()> {
107 let mut file = OpenOptions::new().read(true).write(true).open(path)?;
108 update_tag(&mut file, edit, opts)
109}
110
111#[cfg(test)]
112mod tests {
113 use std::io::Cursor;
114
115 use id3::TagLike;
116 use id3::frame::Comment;
117
118 use super::*;
119 use crate::edit::FrameId;
120
121 type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
122 const AUDIO: usize = 1024;
123 const AUDIO_END: usize = 92 + AUDIO;
124
125 fn synth() -> std::result::Result<Vec<u8>, Box<dyn std::error::Error>> {
127 let header = DsfHeader {
128 total_file_size: AUDIO_END as u64,
129 metadata_offset: 0,
130 channel_type: 2,
131 channel_count: 2,
132 sample_rate: 2_822_400,
133 bits_per_sample: 1,
134 sample_count: 4096,
135 block_size: 4096,
136 data_chunk_size: 12 + AUDIO as u64,
137 };
138 let mut v = Vec::new();
139 header.write(&mut v)?;
140 v.extend(std::iter::repeat_n(0xA5u8, AUDIO));
141 Ok(v)
142 }
143
144 #[test]
145 fn creates_tag_and_keeps_header_consistent() -> TestResult {
146 let mut f = Cursor::new(synth()?);
147 let edit = TagEdit::new()
148 .set(FrameId::TITLE, "Allemande")
149 .set(FrameId::COMPOSER, "J.S. Bach");
150 update_tag(&mut f, &edit, &WriteOptions::default())?;
151
152 let tag = read_tag(&mut f)?.ok_or("tag missing")?;
153 assert_eq!(tag.title(), Some("Allemande"));
154
155 let bytes = f.into_inner();
156 let header = DsfHeader::read(&bytes[..])?;
157 assert_eq!(header.metadata_offset, AUDIO_END as u64);
158 assert_eq!(header.total_file_size, bytes.len() as u64);
159 assert!(
160 bytes[92..AUDIO_END].iter().all(|b| *b == 0xA5),
161 "audio must be untouched"
162 );
163 Ok(())
164 }
165
166 #[test]
167 fn shrinking_a_tag_truncates_the_file() -> TestResult {
168 let mut f = Cursor::new(synth()?);
169 let long = "x".repeat(2000);
170 update_tag(
171 &mut f,
172 &TagEdit::new().set(FrameId::TITLE, long),
173 &WriteOptions::default(),
174 )?;
175 let big = f.get_ref().len();
176
177 update_tag(
178 &mut f,
179 &TagEdit::new().set(FrameId::TITLE, "short"),
180 &WriteOptions::default(),
181 )?;
182 let bytes = f.into_inner();
183 assert!(bytes.len() < big);
184 let header = DsfHeader::read(&bytes[..])?;
185 assert_eq!(header.total_file_size, bytes.len() as u64);
186 Ok(())
187 }
188
189 #[test]
190 fn unrelated_frames_survive_an_edit() -> TestResult {
191 let mut f = Cursor::new(synth()?);
192 let mut tag = Tag::new();
193 tag.add_frame(Comment {
194 lang: "eng".into(),
195 description: String::new(),
196 text: "keep me".into(),
197 });
198 write_tag(&mut f, &tag, Version::Id3v24)?;
199
200 update_tag(
201 &mut f,
202 &TagEdit::new().set(FrameId::TITLE, "T"),
203 &WriteOptions::default(),
204 )?;
205 let tag = read_tag(&mut f)?.ok_or("tag missing")?;
206 assert_eq!(tag.comments().count(), 1);
207 assert_eq!(tag.title(), Some("T"));
208 Ok(())
209 }
210
211 #[test]
212 fn user_text_comment_and_cover_round_trip() -> TestResult {
213 let mut f = Cursor::new(synth()?);
214 let cover = crate::edit::Picture {
215 mime_type: "image/jpeg".to_owned(),
216 data: vec![0xFF, 0xD8, 0xFF, 0xE0, 1, 2, 3],
217 };
218 let edit = TagEdit::new()
219 .set_user_text("MOOD", "calm")
220 .set_user_text("LABEL", "Test Records")
221 .set_comment("remastered")
222 .set_cover(cover.clone());
223 update_tag(&mut f, &edit, &WriteOptions::default())?;
224
225 let tag = read_tag(&mut f)?.ok_or("tag missing")?;
226 let mut texts: Vec<(&str, &str)> = tag
227 .extended_texts()
228 .map(|t| (t.description.as_str(), t.value.as_str()))
229 .collect();
230 texts.sort_unstable();
231 assert_eq!(texts, [("LABEL", "Test Records"), ("MOOD", "calm")]);
232 assert_eq!(
233 tag.comments().map(|c| c.text.as_str()).collect::<Vec<_>>(),
234 ["remastered"]
235 );
236 let picture = tag.pictures().next().ok_or("cover missing")?;
237 assert_eq!(picture.data, cover.data);
238 assert_eq!(picture.mime_type, cover.mime_type);
239 assert_eq!(picture.picture_type, id3::frame::PictureType::CoverFront);
240 Ok(())
241 }
242
243 #[test]
244 fn replacing_and_clearing_extras_leaves_no_duplicates() -> TestResult {
245 let mut f = Cursor::new(synth()?);
246 let first = TagEdit::new()
247 .set_user_text("MOOD", "calm")
248 .set_comment("one")
249 .set_cover(crate::edit::Picture {
250 mime_type: "image/png".to_owned(),
251 data: vec![1],
252 });
253 update_tag(&mut f, &first, &WriteOptions::default())?;
254 let second = TagEdit::new()
255 .set_user_text("MOOD", "tense")
256 .set_comment("two")
257 .set_cover(crate::edit::Picture {
258 mime_type: "image/png".to_owned(),
259 data: vec![2],
260 });
261 update_tag(&mut f, &second, &WriteOptions::default())?;
262
263 let tag = read_tag(&mut f)?.ok_or("tag missing")?;
264 assert_eq!(tag.extended_texts().count(), 1);
265 assert_eq!(tag.comments().count(), 1);
266 assert_eq!(
267 tag.pictures().map(|p| p.data.clone()).collect::<Vec<_>>(),
268 [vec![2]]
269 );
270
271 let clear = TagEdit::new()
272 .clear_user_text("MOOD")
273 .set_comment(" ")
274 .clear_cover();
275 update_tag(&mut f, &clear, &WriteOptions::default())?;
276 let tag = read_tag(&mut f)?.ok_or("tag missing")?;
277 assert_eq!(tag.extended_texts().count(), 0);
278 assert_eq!(tag.comments().count(), 0);
279 assert_eq!(tag.pictures().count(), 0);
280 Ok(())
281 }
282
283 #[test]
284 fn other_user_text_and_comments_survive() -> TestResult {
285 let mut f = Cursor::new(synth()?);
286 let mut tag = Tag::new();
287 tag.add_frame(id3::frame::ExtendedText {
288 description: "KEEP".into(),
289 value: "me".into(),
290 });
291 tag.add_frame(Comment {
292 lang: "eng".into(),
293 description: "other".into(),
294 text: "described comment".into(),
295 });
296 write_tag(&mut f, &tag, Version::Id3v24)?;
297
298 let edit = TagEdit::new()
299 .set_user_text("MOOD", "x")
300 .set_comment("plain");
301 update_tag(&mut f, &edit, &WriteOptions::default())?;
302 let tag = read_tag(&mut f)?.ok_or("tag missing")?;
303 assert_eq!(tag.extended_texts().count(), 2);
304 assert_eq!(tag.comments().count(), 2);
305 Ok(())
306 }
307
308 #[test]
309 fn extras_survive_id3v23() -> TestResult {
310 let mut f = Cursor::new(synth()?);
311 let edit = TagEdit::new()
312 .set_user_text("MOOD", "calm")
313 .set_comment("hello")
314 .set_cover(crate::edit::Picture {
315 mime_type: "image/jpeg".to_owned(),
316 data: vec![9, 9],
317 });
318 let opts = WriteOptions::default().with_version(Version::Id3v23);
319 update_tag(&mut f, &edit, &opts)?;
320 let tag = read_tag(&mut f)?.ok_or("tag missing")?;
321 assert_eq!(tag.version(), Version::Id3v23);
322 assert_eq!(tag.extended_texts().count(), 1);
323 assert_eq!(tag.comments().count(), 1);
324 assert_eq!(tag.pictures().count(), 1);
325 Ok(())
326 }
327
328 #[test]
329 fn remove_tag_restores_a_tagless_file() -> TestResult {
330 let original = synth()?;
331 let mut f = Cursor::new(original.clone());
332 update_tag(
333 &mut f,
334 &TagEdit::new().set(FrameId::TITLE, "T"),
335 &WriteOptions::default(),
336 )?;
337 remove_tag(&mut f)?;
338 assert_eq!(f.into_inner(), original);
339 Ok(())
340 }
341
342 #[test]
343 fn rejects_foreign_and_truncated_input() {
344 let riff = read_tag(&mut Cursor::new(b"RIFF....".to_vec()));
345 assert!(matches!(riff, Err(Error::BadSignature(_))));
346 let short = read_tag(&mut Cursor::new(b"DSD ".to_vec()));
347 assert!(matches!(short, Err(Error::Malformed(_))));
348 }
349
350 #[test]
351 fn frame_id_validation() {
352 assert!(FrameId::new("TIT2").is_ok());
353 assert!(FrameId::new("tit2").is_err());
354 assert!(FrameId::new("TXXX").is_err());
355 assert!(FrameId::new("APIC").is_err());
356 assert!(FrameId::new("TI").is_err());
357 }
358}