Skip to main content

lofty/util/
io.rs

1//! Various traits for reading and writing to file-like objects
2
3use crate::error::{LoftyError, Result};
4use crate::util::math::F80;
5
6use std::collections::VecDeque;
7use std::fs::File;
8use std::io::{Cursor, Read, Seek, SeekFrom, Write};
9
10// TODO: https://github.com/rust-lang/rust/issues/59359
11pub(crate) trait SeekStreamLen: Seek {
12	fn stream_len_hack(&mut self) -> crate::error::Result<u64> {
13		use std::io::SeekFrom;
14
15		let current_pos = self.stream_position()?;
16		let len = self.seek(SeekFrom::End(0))?;
17
18		self.seek(SeekFrom::Start(current_pos))?;
19
20		Ok(len)
21	}
22}
23
24impl<T> SeekStreamLen for T where T: Seek {}
25
26/// Provides a method to truncate an object to the specified length
27///
28/// This is one component of the [`FileLike`] trait, which is used to provide implementors access to any
29/// file saving methods such as [`AudioFile::save_to`](crate::file::AudioFile::save_to).
30///
31/// Take great care in implementing this for downstream types, as Lofty will assume that the
32/// container has the new length specified. If this assumption were to be broken, files **will** become corrupted.
33///
34/// # Examples
35///
36/// ```rust
37/// use lofty::io::Truncate;
38///
39/// let mut data = vec![1, 2, 3, 4, 5];
40/// data.truncate(3);
41///
42/// assert_eq!(data, vec![1, 2, 3]);
43/// ```
44pub trait Truncate {
45	/// The error type of the truncation operation
46	type Error: Into<LoftyError>;
47
48	/// Truncate a storage object to the specified length
49	///
50	/// # Errors
51	///
52	/// Errors depend on the object being truncated, which may not always be fallible.
53	fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error>;
54}
55
56impl Truncate for File {
57	type Error = std::io::Error;
58
59	fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
60		self.set_len(new_len)
61	}
62}
63
64impl Truncate for Vec<u8> {
65	type Error = std::convert::Infallible;
66
67	fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
68		self.truncate(new_len as usize);
69		Ok(())
70	}
71}
72
73impl Truncate for VecDeque<u8> {
74	type Error = std::convert::Infallible;
75
76	fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
77		self.truncate(new_len as usize);
78		Ok(())
79	}
80}
81
82impl<T> Truncate for Cursor<T>
83where
84	T: Truncate,
85{
86	type Error = <T as Truncate>::Error;
87
88	fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
89		self.get_mut().truncate(new_len)
90	}
91}
92
93impl<T> Truncate for Box<T>
94where
95	T: Truncate,
96{
97	type Error = <T as Truncate>::Error;
98
99	fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
100		self.as_mut().truncate(new_len)
101	}
102}
103
104impl<T> Truncate for &mut T
105where
106	T: Truncate,
107{
108	type Error = <T as Truncate>::Error;
109
110	fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
111		(**self).truncate(new_len)
112	}
113}
114
115/// Provides a method to get the length of a storage object
116///
117/// This is one component of the [`FileLike`] trait, which is used to provide implementors access to any
118/// file saving methods such as [`AudioFile::save_to`](crate::file::AudioFile::save_to).
119///
120/// Take great care in implementing this for downstream types, as Lofty will assume that the
121/// container has the exact length specified. If this assumption were to be broken, files **may** become corrupted.
122///
123/// # Examples
124///
125/// ```rust
126/// use lofty::io::Length;
127///
128/// let data = vec![1, 2, 3, 4, 5];
129/// assert_eq!(data.len(), 5);
130/// ```
131pub trait Length {
132	/// The error type of the length operation
133	type Error: Into<LoftyError>;
134
135	/// Get the length of a storage object
136	///
137	/// # Errors
138	///
139	/// Errors depend on the object being read, which may not always be fallible.
140	fn len(&self) -> std::result::Result<u64, Self::Error>;
141}
142
143impl Length for File {
144	type Error = std::io::Error;
145
146	fn len(&self) -> std::result::Result<u64, Self::Error> {
147		self.metadata().map(|m| m.len())
148	}
149}
150
151impl Length for Vec<u8> {
152	type Error = std::convert::Infallible;
153
154	fn len(&self) -> std::result::Result<u64, Self::Error> {
155		Ok(self.len() as u64)
156	}
157}
158
159impl Length for VecDeque<u8> {
160	type Error = std::convert::Infallible;
161
162	fn len(&self) -> std::result::Result<u64, Self::Error> {
163		Ok(self.len() as u64)
164	}
165}
166
167impl<T> Length for Cursor<T>
168where
169	T: Length,
170{
171	type Error = <T as Length>::Error;
172
173	fn len(&self) -> std::result::Result<u64, Self::Error> {
174		Length::len(self.get_ref())
175	}
176}
177
178impl<T> Length for Box<T>
179where
180	T: Length,
181{
182	type Error = <T as Length>::Error;
183
184	fn len(&self) -> std::result::Result<u64, Self::Error> {
185		Length::len(self.as_ref())
186	}
187}
188
189impl<T> Length for &T
190where
191	T: Length,
192{
193	type Error = <T as Length>::Error;
194
195	fn len(&self) -> std::result::Result<u64, Self::Error> {
196		Length::len(*self)
197	}
198}
199
200impl<T> Length for &mut T
201where
202	T: Length,
203{
204	type Error = <T as Length>::Error;
205
206	fn len(&self) -> std::result::Result<u64, Self::Error> {
207		Length::len(*self)
208	}
209}
210
211/// Provides a set of methods to read and write to a file-like object
212///
213/// This is a combination of the [`Read`], [`Write`], [`Seek`], [`Truncate`], and [`Length`] traits.
214/// It is used to provide implementors access to any file saving methods such as [`AudioFile::save_to`](crate::file::AudioFile::save_to).
215///
216/// Take great care in implementing this for downstream types, as Lofty will assume that the
217/// trait implementations are correct. If this assumption were to be broken, files **may** become corrupted.
218pub trait FileLike: Read + Write + Seek + Truncate + Length
219where
220	<Self as Truncate>::Error: Into<LoftyError>,
221	<Self as Length>::Error: Into<LoftyError>,
222{
223}
224
225impl<T> FileLike for T
226where
227	T: Read + Write + Seek + Truncate + Length,
228	<T as Truncate>::Error: Into<LoftyError>,
229	<T as Length>::Error: Into<LoftyError>,
230{
231}
232
233pub(crate) trait ReadExt: Read {
234	/// Read a big-endian [`F80`] from the current position.
235	fn read_f80(&mut self) -> Result<F80>;
236}
237
238impl<R> ReadExt for R
239where
240	R: Read,
241{
242	fn read_f80(&mut self) -> Result<F80> {
243		let mut bytes = [0; 10];
244		self.read_exact(&mut bytes)?;
245
246		Ok(F80::from_be_bytes(bytes))
247	}
248}
249
250#[derive(Copy, Clone, Debug, Default, PartialEq)]
251pub(crate) enum RevSearchStart {
252	/// Start the search from the end of the stream
253	#[default]
254	FromEnd,
255	/// Start the search from the current position
256	FromCurrent,
257}
258
259#[derive(Copy, Clone, Debug, Default, PartialEq)]
260pub(crate) enum RevSearchEnd {
261	/// End the search at the start of the stream
262	StreamStart,
263	/// End the search at the current position
264	///
265	/// Collides with [`RevSearchStart::FromCurrent`]
266	#[default]
267	FromCurrent,
268	/// End the search at a specific position
269	Pos(u64),
270}
271
272pub(crate) struct RevPatternSearcher<'a, T> {
273	start: RevSearchStart,
274	end: RevSearchEnd,
275	buffer_size: u64,
276	pattern: &'a [u8],
277	reader: &'a mut T,
278}
279
280impl<T> RevPatternSearcher<'_, T>
281where
282	T: Read + Seek,
283{
284	pub(crate) fn buffer_size(&mut self, buffer_size: u64) -> &mut Self {
285		self.buffer_size = buffer_size;
286		self
287	}
288
289	pub(crate) fn start_pos(&mut self, start: RevSearchStart) -> &mut Self {
290		self.start = start;
291		self
292	}
293
294	pub(crate) fn end_pos(&mut self, end: RevSearchEnd) -> &mut Self {
295		self.end = end;
296		self
297	}
298
299	/// Search for `pattern` in the stream, from the end up to the current position.
300	///
301	/// If found, this will leave the reader at the start of the pattern. Otherwise, the reader will
302	/// be at the original position.
303	pub(crate) fn search(&mut self) -> std::io::Result<bool> {
304		if self.pattern.is_empty() {
305			return Ok(true);
306		}
307
308		let original_pos = self.reader.stream_position()?;
309		let pattern_len = self.pattern.len();
310
311		let start_pos = match self.start {
312			RevSearchStart::FromEnd => self.reader.seek(SeekFrom::End(0))?,
313			RevSearchStart::FromCurrent => original_pos,
314		};
315
316		let end_pos = match self.end {
317			RevSearchEnd::StreamStart => 0,
318			RevSearchEnd::FromCurrent => original_pos,
319			RevSearchEnd::Pos(p) => p,
320		};
321
322		if start_pos < end_pos
323			|| (start_pos - end_pos) < pattern_len as u64
324			|| self.buffer_size < pattern_len as u64
325		{
326			self.reader.seek(SeekFrom::Start(original_pos))?;
327			return Ok(false);
328		}
329
330		// To handle partial matches, the `current_pos` gets decremented in "steps". Which is the
331		// `buffer_size`, but with just enough room at the end for the pattern.
332		let overlap_step = self.buffer_size - ((pattern_len as u64) - 1);
333
334		let mut current_pos = start_pos;
335		let mut buf = vec![0; self.buffer_size as usize];
336
337		while current_pos > end_pos {
338			let window_size = current_pos - end_pos;
339			let read_size = std::cmp::min(self.buffer_size, window_size);
340
341			let read_start = current_pos - read_size;
342			self.reader.seek(SeekFrom::Start(read_start))?;
343
344			let window = &mut buf[..read_size as usize];
345			self.reader.read_exact(window)?;
346
347			if let Some(match_offset) = window
348				.windows(self.pattern.len())
349				.enumerate()
350				.rev()
351				.find_map(|(idx, window)| {
352					if window == self.pattern {
353						Some(idx)
354					} else {
355						None
356					}
357				}) {
358				self.reader
359					.seek(SeekFrom::Start(read_start + match_offset as u64))?;
360				return Ok(true);
361			}
362
363			current_pos -= std::cmp::min(read_size, overlap_step);
364		}
365
366		Ok(false)
367	}
368}
369
370pub(crate) trait ReadFindExt: Read + Seek + Sized {
371	/// Construct a [`RevPatternSearcher`]
372	fn rfind<'a>(&'a mut self, pattern: &'a [u8]) -> RevPatternSearcher<'a, Self> {
373		RevPatternSearcher {
374			start: RevSearchStart::default(),
375			end: RevSearchEnd::StreamStart,
376			buffer_size: 1024,
377			pattern,
378			reader: self,
379		}
380	}
381}
382
383impl<T> ReadFindExt for T where T: Read + Seek {}
384
385#[cfg(test)]
386mod tests {
387	use crate::config::{ParseOptions, WriteOptions};
388	use crate::file::AudioFile;
389	use crate::io::{ReadFindExt, RevSearchEnd, RevSearchStart};
390	use crate::mpeg::MpegFile;
391	use crate::tag::Accessor;
392
393	use std::io::{Cursor, Read, Seek, SeekFrom, Write};
394	use std::iter::repeat_n;
395	use std::ops::Neg;
396
397	const TEST_ASSET: &str = "tests/files/assets/minimal/full_test.mp3";
398
399	fn test_asset_contents() -> Vec<u8> {
400		std::fs::read(TEST_ASSET).unwrap()
401	}
402
403	fn file() -> MpegFile {
404		let file_contents = test_asset_contents();
405		let mut reader = Cursor::new(file_contents);
406		MpegFile::read_from(&mut reader, ParseOptions::new()).unwrap()
407	}
408
409	fn alter_tag(file: &mut MpegFile) {
410		let tag = file.id3v2_mut().unwrap();
411		tag.set_artist(String::from("Bar artist"));
412	}
413
414	fn revert_tag(file: &mut MpegFile) {
415		let tag = file.id3v2_mut().unwrap();
416		tag.set_artist(String::from("Foo artist"));
417	}
418
419	#[test_log::test]
420	fn io_save_to_file() {
421		// Read the file and change the artist
422		let mut file = file();
423		alter_tag(&mut file);
424
425		let mut temp_file = tempfile::tempfile().unwrap();
426		let file_content = std::fs::read(TEST_ASSET).unwrap();
427		temp_file.write_all(&file_content).unwrap();
428		temp_file.rewind().unwrap();
429
430		// Save the new artist
431		file.save_to(&mut temp_file, WriteOptions::new().preferred_padding(0))
432			.expect("Failed to save to file");
433
434		// Read the file again and change the artist back
435		temp_file.rewind().unwrap();
436		let mut file = MpegFile::read_from(&mut temp_file, ParseOptions::new()).unwrap();
437		revert_tag(&mut file);
438
439		temp_file.rewind().unwrap();
440		file.save_to(&mut temp_file, WriteOptions::new().preferred_padding(0))
441			.expect("Failed to save to file");
442
443		// The contents should be the same as the original file
444		temp_file.rewind().unwrap();
445		let mut current_file_contents = Vec::new();
446		temp_file.read_to_end(&mut current_file_contents).unwrap();
447
448		assert_eq!(current_file_contents, test_asset_contents());
449	}
450
451	#[test_log::test]
452	fn io_save_to_vec() {
453		// Same test as above, but using a Cursor<Vec<u8>> instead of a file
454		let mut file = file();
455		alter_tag(&mut file);
456
457		let file_content = std::fs::read(TEST_ASSET).unwrap();
458
459		let mut reader = Cursor::new(file_content);
460		file.save_to(&mut reader, WriteOptions::new().preferred_padding(0))
461			.expect("Failed to save to vec");
462
463		reader.rewind().unwrap();
464		let mut file = MpegFile::read_from(&mut reader, ParseOptions::new()).unwrap();
465		revert_tag(&mut file);
466
467		reader.rewind().unwrap();
468		file.save_to(&mut reader, WriteOptions::new().preferred_padding(0))
469			.expect("Failed to save to vec");
470
471		let current_file_contents = reader.into_inner();
472		assert_eq!(current_file_contents, test_asset_contents());
473	}
474
475	#[test_log::test]
476	fn io_save_using_references() {
477		struct File {
478			buf: Vec<u8>,
479		}
480
481		let mut f = File {
482			buf: std::fs::read(TEST_ASSET).unwrap(),
483		};
484
485		// Same test as above, but using references instead of owned values
486		let mut file = file();
487		alter_tag(&mut file);
488
489		{
490			let mut reader = Cursor::new(&mut f.buf);
491			file.save_to(&mut reader, WriteOptions::new().preferred_padding(0))
492				.expect("Failed to save to vec");
493		}
494
495		{
496			let mut reader = Cursor::new(&f.buf[..]);
497			file = MpegFile::read_from(&mut reader, ParseOptions::new()).unwrap();
498			revert_tag(&mut file);
499		}
500
501		{
502			let mut reader = Cursor::new(&mut f.buf);
503			file.save_to(&mut reader, WriteOptions::new().preferred_padding(0))
504				.expect("Failed to save to vec");
505		}
506
507		let current_file_contents = f.buf;
508		assert_eq!(current_file_contents, test_asset_contents());
509	}
510
511	#[test_log::test]
512	fn rev_search() {
513		// Basic search
514		const PAT: &[u8] = b"PATTERN";
515		let mut data1 = PAT.to_vec();
516		data1.extend(repeat_n(0, 5000));
517
518		let mut stream1 = Cursor::new(data1);
519		assert!(stream1.rfind(PAT).search().unwrap());
520
521		// Search across boundaries
522		let mut data2 = PAT.to_vec();
523		data2.extend(repeat_n(0, 1023));
524
525		let mut stream2 = Cursor::new(data2);
526		assert!(stream2.rfind(PAT).search().unwrap());
527
528		// Multiple occurrences, should find the last one
529		let mut data3 = PAT.to_vec();
530		let junk_len = 20;
531		data3.extend(repeat_n(0, junk_len));
532		data3.extend(PAT);
533		data3.extend(repeat_n(0, junk_len));
534		let last_occurence_offset = data3.len() - (junk_len + PAT.len());
535
536		let mut stream3 = Cursor::new(data3);
537		assert!(stream3.rfind(PAT).search().unwrap());
538		assert_eq!(stream3.position(), last_occurence_offset as u64);
539
540		// Multiple occurrences, search starts within a partial match
541		let mut data4 = PAT.to_vec();
542		data4.extend(repeat_n(0, junk_len));
543		data4.extend(PAT);
544		data4.extend(repeat_n(0, junk_len));
545		data4.extend(PAT);
546
547		let middle_match_offset = PAT.len() + junk_len;
548
549		let mut stream4 = Cursor::new(data4);
550		// Eat partially into the first match
551		stream4
552			.seek(SeekFrom::End(((PAT.len() - 3) as i64).neg()))
553			.unwrap();
554
555		assert!(
556			stream4
557				.rfind(PAT)
558				.start_pos(RevSearchStart::FromCurrent)
559				.end_pos(RevSearchEnd::StreamStart)
560				.search()
561				.unwrap()
562		);
563		assert_eq!(stream4.position(), middle_match_offset as u64);
564	}
565}