1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
//! Abstractions that expose a simple interface for reading and storing tags according to some
//! underlying file format.
//!
//! The need for this abstraction arises from the differences that audiofiles have when storing
//! metadata. For example, MP3 uses a header for ID3v2, a trailer for ID3v1 while WAV has a special
//! "RIFF-chunk" which stores an ID3 tag.

use std::fs;
use std::io;

pub mod plain;

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Format {
    /// ID3 is typically written as a header that precedes any audio content. For MPEG files, it is
    /// always a header. However, other formats where it is possible for the tag to be located
    /// elsewhere may also have an ID3 header.
    Header,

    /// Aiff is a chunk-y format format like WAV. Here, the file is built up in chunks where every
    /// chunk is a size header and some binary data. Audio is a chunk, but an ID3 tag may also be
    /// written to a chunk.
    Aiff,

    /// Similar to Aiff.
    Wav,
}

impl Format {
    pub fn magic(probe: impl AsRef<[u8]>) -> Option<Self> {
        let probe = probe.as_ref();
        if probe.len() < 12 {
            return None;
        }
        match (&probe[..3], &probe[..4], &probe[8..12]) {
            (b"ID3", _, _) => Some(Format::Header),
            (_, b"FORM", _) => Some(Format::Aiff),
            (_, b"RIFF", b"WAVE") => Some(Format::Wav),
            _ => None,
        }
    }
}

/// Refer to the module documentation.
pub trait Storage<'a> {
    type Reader: io::Read + io::Seek + 'a;
    type Writer: io::Write + io::Seek + 'a;

    /// Opens the storage for reading.
    fn reader(&'a mut self) -> io::Result<Self::Reader>;

    /// Opens the storage for writing.
    ///
    /// The written data is comitted to persistent storage when the
    /// writer is dropped, altough this will ignore any errors. The caller must manually commit by
    /// using `io::Write::flush` to check for errors.
    fn writer(&'a mut self) -> io::Result<Self::Writer>;
}

/// This trait is the combination of the [`std::io`] stream traits with an additional method to resize the
/// file.
pub trait StorageFile: io::Read + io::Write + io::Seek + private::Sealed {
    /// Performs the resize. Assumes the same behaviour as [`std::fs::File::set_len`].
    fn set_len(&mut self, new_len: u64) -> io::Result<()>;
}

impl<'a, T: StorageFile> StorageFile for &'a mut T {
    fn set_len(&mut self, new_len: u64) -> io::Result<()> {
        (*self).set_len(new_len)
    }
}

impl StorageFile for fs::File {
    fn set_len(&mut self, new_len: u64) -> io::Result<()> {
        fs::File::set_len(self, new_len)
    }
}

impl StorageFile for io::Cursor<Vec<u8>> {
    fn set_len(&mut self, new_len: u64) -> io::Result<()> {
        self.get_mut().resize(new_len as usize, 0);
        Ok(())
    }
}

// https://rust-lang.github.io/api-guidelines/future-proofing.html#c-sealed
mod private {
    pub trait Sealed {}

    impl<'a, T: Sealed> Sealed for &'a mut T {}
    impl Sealed for std::fs::File {}
    impl Sealed for std::io::Cursor<Vec<u8>> {}
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Read;
    use std::path::Path;

    fn probe(path: impl AsRef<Path>) -> [u8; 12] {
        let mut f = fs::File::open(path).unwrap();
        let mut b = [0u8; 12];
        f.read(&mut b[..]).unwrap();
        b
    }

    #[test]
    fn test_format_magic() {
        assert_eq!(
            Format::magic(probe("testdata/aiff/padding.aiff")),
            Some(Format::Aiff)
        );
        assert_eq!(
            Format::magic(probe("testdata/aiff/quiet.aiff")),
            Some(Format::Aiff)
        );
        assert_eq!(
            Format::magic(probe("testdata/wav/tagged-end.wav")),
            Some(Format::Wav)
        );
        assert_eq!(
            Format::magic(probe("testdata/wav/tagless.wav")),
            Some(Format::Wav)
        );
        assert_eq!(
            Format::magic(probe("testdata/id3v22.id3")),
            Some(Format::Header)
        );
        assert_eq!(Format::magic(probe("testdata/mpeg-header")), None);
    }
}