Skip to main content

musefs_db/
error.rs

1use thiserror::Error;
2
3#[derive(Debug, Error)]
4pub enum DbError {
5    #[error(transparent)]
6    Sqlite(#[from] rusqlite::Error),
7    #[error(
8        "audio bounds out of range: offset {audio_offset} + length {audio_length} exceeds backing_size {backing_size}"
9    )]
10    AudioBoundsOutOfRange {
11        audio_offset: u64,
12        audio_length: u64,
13        backing_size: u64,
14    },
15    #[error(
16        "database schema does not match the version musefs expects (mismatch at {object}); \
17         regenerate the store by running `musefs scan` against the library"
18    )]
19    SchemaMismatch { object: String },
20    #[error(
21        "store schema version {found} is newer than this musefs build supports \
22         (max {supported}); upgrade musefs to read this store"
23    )]
24    StoreTooNew { found: i64, supported: i64 },
25    #[error("the store is in use — unmount the filesystem or stop any scan before vacuuming")]
26    StoreInUse(#[source] rusqlite::Error),
27    #[error("{table}.{field} length {len} exceeds the {max} cap (crafted or corrupt DB)")]
28    FieldTooLarge {
29        table: &'static str,
30        field: &'static str,
31        len: i64,
32        max: i64,
33    },
34    #[error("structural block for track {track_id} is invalid: {detail} (crafted or corrupt DB)")]
35    InvalidStructuralBlock { track_id: i64, detail: String },
36    #[error(
37        "track {track_id} has {count} tag rows, exceeds the {max}-row cap (crafted or corrupt DB)"
38    )]
39    TooManyValues {
40        track_id: i64,
41        count: usize,
42        max: usize,
43    },
44    #[error(
45        "track {track_id} has {count} track_art rows, exceeds the {max}-row cap (crafted or corrupt DB)"
46    )]
47    TooManyArtRows {
48        track_id: i64,
49        count: usize,
50        max: usize,
51    },
52}
53
54pub type Result<T> = std::result::Result<T, DbError>;
55
56/// Reject a field whose SQL-computed `length()` exceeds `max`, before the value
57/// is ever materialized. Takes only the length, so by construction it cannot
58/// touch the (potentially huge) payload — the allocation-free guarantee the
59/// reader guards rely on (spec N13).
60pub(crate) fn check_field_len(
61    table: &'static str,
62    field: &'static str,
63    len: i64,
64    max: i64,
65) -> Result<()> {
66    if len > max {
67        return Err(DbError::FieldTooLarge {
68            table,
69            field,
70            len,
71            max,
72        });
73    }
74    Ok(())
75}
76
77/// Reject a track whose materialized tag-row count exceeds the per-track cap.
78/// Centralizing the comparison keeps a single boundary site (one mutation
79/// target) shared by every tag reader, instead of one per reader.
80pub(crate) fn check_tag_count(track_id: i64, count: usize) -> Result<()> {
81    if count > crate::limits::MAX_TAGS_PER_TRACK {
82        return Err(DbError::TooManyValues {
83            track_id,
84            count,
85            max: crate::limits::MAX_TAGS_PER_TRACK,
86        });
87    }
88    Ok(())
89}
90
91/// Reject a track whose materialized `track_art` row count exceeds the per-track
92/// cap. There is a single art reader (`get_track_art`), so this helper is not
93/// about sharing across callers the way `check_tag_count` is; it exists for
94/// fidelity with that pattern and to keep the single `>` comparison as one
95/// mutation-gate target.
96pub(crate) fn check_art_count(track_id: i64, count: usize) -> Result<()> {
97    if count > crate::limits::MAX_ART_ROWS_PER_TRACK {
98        return Err(DbError::TooManyArtRows {
99            track_id,
100            count,
101            max: crate::limits::MAX_ART_ROWS_PER_TRACK,
102        });
103    }
104    Ok(())
105}
106
107#[cfg(test)]
108mod guard_helper_tests {
109    use super::check_field_len;
110
111    #[test]
112    fn rejects_on_length_only_inclusive_boundary() {
113        // The decision is a pure function of length — the value is never passed
114        // in, so an over-cap row provably cannot be materialized to reject it.
115        assert!(check_field_len("tags", "value", 262_145, 262_144).is_err());
116        assert!(check_field_len("tags", "value", 262_144, 262_144).is_ok());
117    }
118
119    #[test]
120    fn tag_count_accepts_at_cap_rejects_above() {
121        use crate::limits::MAX_TAGS_PER_TRACK;
122        // Boundary is inclusive: exactly the cap is accepted, one over rejected.
123        // Pins the single `>` site so a `>`→`>=`/`==` mutant cannot survive.
124        assert!(super::check_tag_count(1, MAX_TAGS_PER_TRACK).is_ok());
125        assert!(super::check_tag_count(1, MAX_TAGS_PER_TRACK + 1).is_err());
126    }
127
128    #[test]
129    fn art_count_accepts_at_cap_rejects_above() {
130        use crate::limits::MAX_ART_ROWS_PER_TRACK;
131        // Boundary is inclusive: exactly the cap is accepted, one over rejected.
132        // Pins the single `>` site so a `>`→`>=`/`==` mutant cannot survive.
133        assert!(super::check_art_count(1, MAX_ART_ROWS_PER_TRACK).is_ok());
134        assert!(super::check_art_count(1, MAX_ART_ROWS_PER_TRACK + 1).is_err());
135    }
136}