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("{table}.{field} length {len} exceeds the {max} cap (crafted or corrupt DB)")]
21    FieldTooLarge {
22        table: &'static str,
23        field: &'static str,
24        len: i64,
25        max: i64,
26    },
27    #[error("structural block for track {track_id} is invalid: {detail} (crafted or corrupt DB)")]
28    InvalidStructuralBlock { track_id: i64, detail: String },
29    #[error(
30        "track {track_id} has {count} tag rows, exceeds the {max}-row cap (crafted or corrupt DB)"
31    )]
32    TooManyValues {
33        track_id: i64,
34        count: usize,
35        max: usize,
36    },
37    #[error(
38        "track {track_id} has {count} track_art rows, exceeds the {max}-row cap (crafted or corrupt DB)"
39    )]
40    TooManyArtRows {
41        track_id: i64,
42        count: usize,
43        max: usize,
44    },
45}
46
47pub type Result<T> = std::result::Result<T, DbError>;
48
49/// Reject a field whose SQL-computed `length()` exceeds `max`, before the value
50/// is ever materialized. Takes only the length, so by construction it cannot
51/// touch the (potentially huge) payload — the allocation-free guarantee the
52/// reader guards rely on (spec N13).
53pub(crate) fn check_field_len(
54    table: &'static str,
55    field: &'static str,
56    len: i64,
57    max: i64,
58) -> Result<()> {
59    if len > max {
60        return Err(DbError::FieldTooLarge {
61            table,
62            field,
63            len,
64            max,
65        });
66    }
67    Ok(())
68}
69
70/// Reject a track whose materialized tag-row count exceeds the per-track cap.
71/// Centralizing the comparison keeps a single boundary site (one mutation
72/// target) shared by every tag reader, instead of one per reader.
73pub(crate) fn check_tag_count(track_id: i64, count: usize) -> Result<()> {
74    if count > crate::limits::MAX_TAGS_PER_TRACK {
75        return Err(DbError::TooManyValues {
76            track_id,
77            count,
78            max: crate::limits::MAX_TAGS_PER_TRACK,
79        });
80    }
81    Ok(())
82}
83
84/// Reject a track whose materialized `track_art` row count exceeds the per-track
85/// cap. There is a single art reader (`get_track_art`), so this helper is not
86/// about sharing across callers the way `check_tag_count` is; it exists for
87/// fidelity with that pattern and to keep the single `>` comparison as one
88/// mutation-gate target.
89pub(crate) fn check_art_count(track_id: i64, count: usize) -> Result<()> {
90    if count > crate::limits::MAX_ART_ROWS_PER_TRACK {
91        return Err(DbError::TooManyArtRows {
92            track_id,
93            count,
94            max: crate::limits::MAX_ART_ROWS_PER_TRACK,
95        });
96    }
97    Ok(())
98}
99
100#[cfg(test)]
101mod guard_helper_tests {
102    use super::check_field_len;
103
104    #[test]
105    fn rejects_on_length_only_inclusive_boundary() {
106        // The decision is a pure function of length — the value is never passed
107        // in, so an over-cap row provably cannot be materialized to reject it.
108        assert!(check_field_len("tags", "value", 262_145, 262_144).is_err());
109        assert!(check_field_len("tags", "value", 262_144, 262_144).is_ok());
110    }
111
112    #[test]
113    fn tag_count_accepts_at_cap_rejects_above() {
114        use crate::limits::MAX_TAGS_PER_TRACK;
115        // Boundary is inclusive: exactly the cap is accepted, one over rejected.
116        // Pins the single `>` site so a `>`→`>=`/`==` mutant cannot survive.
117        assert!(super::check_tag_count(1, MAX_TAGS_PER_TRACK).is_ok());
118        assert!(super::check_tag_count(1, MAX_TAGS_PER_TRACK + 1).is_err());
119    }
120
121    #[test]
122    fn art_count_accepts_at_cap_rejects_above() {
123        use crate::limits::MAX_ART_ROWS_PER_TRACK;
124        // Boundary is inclusive: exactly the cap is accepted, one over rejected.
125        // Pins the single `>` site so a `>`→`>=`/`==` mutant cannot survive.
126        assert!(super::check_art_count(1, MAX_ART_ROWS_PER_TRACK).is_ok());
127        assert!(super::check_art_count(1, MAX_ART_ROWS_PER_TRACK + 1).is_err());
128    }
129}