Skip to main content

musefs_db/
models.rs

1use strum::{EnumIter, EnumString, IntoStaticStr};
2
3/// The DB text representation (the `tracks.format` column) is derived:
4/// `serialize_all = "lowercase"` lowercases the whole variant ident
5/// (`OggFlac` → `"oggflac"`). The strings are an external contract —
6/// beets/Picard write them — pinned by `tests::db_strings_are_pinned`.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, IntoStaticStr, EnumIter)]
8#[strum(serialize_all = "lowercase")]
9#[cfg_attr(feature = "mutants", derive(Default))]
10pub enum Format {
11    #[cfg_attr(feature = "mutants", default)]
12    Flac,
13    Mp3,
14    M4a,
15    Opus,
16    Vorbis,
17    OggFlac,
18    Wav,
19}
20
21impl Format {
22    pub fn as_str(self) -> &'static str {
23        self.into()
24    }
25}
26
27#[cfg(test)]
28mod tests {
29    use super::Format;
30    use strum::IntoEnumIterator;
31
32    #[test]
33    fn every_format_round_trips() {
34        for f in Format::iter() {
35            assert_eq!(f.as_str().parse::<Format>(), Ok(f));
36        }
37    }
38
39    /// The strings are a DB contract — external writers (beets/Picard) store
40    /// them. A variant rename must not silently change the stored string.
41    #[test]
42    fn db_strings_are_pinned() {
43        let expected = [
44            (Format::Flac, "flac"),
45            (Format::Mp3, "mp3"),
46            (Format::M4a, "m4a"),
47            (Format::Opus, "opus"),
48            (Format::Vorbis, "vorbis"),
49            (Format::OggFlac, "oggflac"),
50            (Format::Wav, "wav"),
51        ];
52        assert_eq!(expected.len(), Format::iter().count());
53        for (f, s) in expected {
54            assert_eq!(f.as_str(), s);
55        }
56    }
57}
58
59#[cfg(test)]
60mod binary_tag_models_tests {
61    #[test]
62    fn binary_tag_constructs() {
63        let bt = super::BinaryTag {
64            key: "PRIV".to_string(),
65            payload: vec![1, 2, 3],
66            ordinal: 0,
67        };
68        assert_eq!(bt.payload.len(), 3);
69        let row = super::BinaryTagRow {
70            rowid: 7,
71            key: "PRIV".to_string(),
72            byte_len: 3,
73        };
74        assert_eq!(row.rowid, 7);
75        let sb = super::StructuralBlock {
76            kind: "STREAMINFO".to_string(),
77            ordinal: 0,
78            body: vec![0u8; 34],
79        };
80        assert_eq!(sb.body.len(), 34);
81    }
82}
83
84/// Validated audio-region bounds for a track: `audio_offset + audio_length`
85/// is guaranteed to fit within `backing_size`, so the reader can splice the
86/// audio region without re-checking. Built at the `tracks` row reader.
87#[cfg_attr(feature = "mutants", derive(Default))]
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub struct TrackBounds {
90    audio_offset: u64,
91    audio_length: u64,
92}
93
94impl TrackBounds {
95    /// Err if `audio_offset + audio_length` overflows or exceeds `backing_size`.
96    pub fn new(
97        audio_offset: u64,
98        audio_length: u64,
99        backing_size: u64,
100    ) -> Result<TrackBounds, crate::DbError> {
101        let end = audio_offset
102            .checked_add(audio_length)
103            .filter(|&end| end <= backing_size)
104            .ok_or(crate::DbError::AudioBoundsOutOfRange {
105                audio_offset,
106                audio_length,
107                backing_size,
108            })?;
109        let _ = end;
110        Ok(TrackBounds {
111            audio_offset,
112            audio_length,
113        })
114    }
115
116    pub fn audio_offset(&self) -> u64 {
117        self.audio_offset
118    }
119
120    pub fn audio_length(&self) -> u64 {
121        self.audio_length
122    }
123}
124
125#[cfg_attr(feature = "mutants", derive(Default))]
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct Track {
128    pub id: i64,
129    pub backing_path: String,
130    pub format: Format,
131    pub bounds: TrackBounds,
132    pub backing_size: u64,
133    pub backing_mtime_ns: i64,
134    pub backing_ctime_ns: i64,
135    pub content_version: i64,
136    pub updated_at: i64,
137}
138
139#[derive(Debug, Clone)]
140pub struct NewTrack {
141    pub backing_path: String,
142    pub format: Format,
143    pub audio_offset: u64,
144    pub audio_length: u64,
145    pub backing_size: u64,
146    pub backing_mtime_ns: i64,
147    pub backing_ctime_ns: i64,
148}
149
150#[derive(Debug, Clone)]
151pub struct NewArt {
152    pub mime: String,
153    pub width: Option<u32>,
154    pub height: Option<u32>,
155    pub data: Vec<u8>,
156}
157
158#[cfg_attr(feature = "mutants", derive(Default))]
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct Tag {
161    pub key: String,
162    pub value: String,
163    pub ordinal: u64,
164}
165
166impl Tag {
167    pub fn new(key: &str, value: &str, ordinal: u64) -> Tag {
168        Tag {
169            key: key.to_string(),
170            value: value.to_string(),
171            ordinal,
172        }
173    }
174}
175
176#[cfg_attr(feature = "mutants", derive(Default))]
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct Art {
179    pub id: i64,
180    pub sha256: String,
181    pub mime: String,
182    pub width: Option<u32>,
183    pub height: Option<u32>,
184    pub byte_len: u64,
185    pub data: Vec<u8>,
186}
187
188#[cfg_attr(feature = "mutants", derive(Default))]
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct ArtMeta {
191    pub mime: String,
192    pub width: Option<u32>,
193    pub height: Option<u32>,
194    pub byte_len: u64,
195}
196
197#[cfg_attr(feature = "mutants", derive(Default))]
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub struct TrackArt {
200    pub art_id: i64,
201    pub picture_type: u32,
202    pub description: String,
203    pub ordinal: u64,
204}
205
206/// A binary tag payload to write (e.g. an opaque ID3 `PRIV` frame body). `key` is
207/// the format-private identifier (ID3 frame id, `APPLICATION`/`CUESHEET`,
208/// `----:<mean>:<name>`); `payload` is the post-header frame/block body.
209#[cfg_attr(feature = "mutants", derive(Default))]
210#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct BinaryTag {
212    pub key: String,
213    pub payload: Vec<u8>,
214    pub ordinal: u64,
215}
216
217/// A binary tag row read back for synthesis: the streaming handle (`rowid`), the
218/// key, and the payload length — the bytes themselves stream at read time.
219#[cfg_attr(feature = "mutants", derive(Default))]
220#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct BinaryTagRow {
222    pub rowid: i64,
223    pub key: String,
224    pub byte_len: u64,
225}
226
227/// A read-only structural metadata block derived from the backing file
228/// (FLAC `STREAMINFO`/`SEEKTABLE`). Stored outside the editable `tags` contract.
229#[cfg_attr(feature = "mutants", derive(Default))]
230#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct StructuralBlock {
232    pub kind: String,
233    pub ordinal: u64,
234    pub body: Vec<u8>,
235}
236
237#[cfg(test)]
238mod track_bounds_tests {
239    use super::TrackBounds;
240
241    #[test]
242    fn accepts_in_range() {
243        let b = TrackBounds::new(10, 20, 100).unwrap();
244        assert_eq!(b.audio_offset(), 10);
245        assert_eq!(b.audio_length(), 20);
246    }
247
248    #[test]
249    fn accepts_exact_fit() {
250        let b = TrackBounds::new(30, 70, 100).unwrap();
251        assert_eq!(b.audio_offset(), 30);
252        assert_eq!(b.audio_length(), 70);
253    }
254
255    #[test]
256    fn accepts_zero_length() {
257        // A zero-length audio run is valid (e.g. structure-only edge).
258        let b = TrackBounds::new(0, 0, 0).unwrap();
259        assert_eq!(b.audio_length(), 0);
260    }
261
262    #[test]
263    fn rejects_exceeding_backing_size() {
264        assert!(TrackBounds::new(50, 60, 100).is_err());
265    }
266
267    #[test]
268    fn rejects_offset_plus_length_overflow() {
269        assert!(TrackBounds::new(u64::MAX, 1, u64::MAX).is_err());
270    }
271}