Skip to main content

musefs_db/
structural.rs

1use crate::error::DbError;
2use crate::models::StructuralBlock;
3use crate::{Db, ReadWrite, Result};
4use rusqlite::params;
5
6impl<M> Db<M> {
7    /// Track ids that have at least one structural block row. Used by `revalidate`
8    /// to detect legacy FLAC tracks (scanned under V1) that still need a backfill.
9    pub fn track_ids_with_structural_blocks(&self) -> Result<std::collections::HashSet<i64>> {
10        let mut stmt = self
11            .conn
12            .prepare("SELECT DISTINCT track_id FROM structural_blocks")?;
13        let rows = stmt.query_map([], |r| r.get::<_, i64>(0))?;
14        Ok(rows.collect::<rusqlite::Result<std::collections::HashSet<i64>>>()?)
15    }
16
17    /// Structural blocks for a track, ordered by (kind, ordinal). Empty when a
18    /// FLAC track has not been (re)scanned under V2 — callers fall back to a
19    /// front read in that case.
20    pub fn get_structural_blocks(&self, track_id: i64) -> Result<Vec<StructuralBlock>> {
21        let mut stmt = self.conn.prepare(
22            "SELECT kind, ordinal, length(body), body FROM structural_blocks \
23             WHERE track_id = ?1 ORDER BY kind, ordinal",
24        )?;
25        let mut rows = stmt.query(params![track_id])?;
26        let mut out = Vec::new();
27        while let Some(r) = rows.next()? {
28            let kind: String = r.get(0)?;
29            let ordinal: i64 = r.get(1)?;
30            let body_len: i64 = r.get(2)?;
31            if !crate::limits::STRUCTURAL_KINDS.contains(&kind.as_str()) {
32                return Err(DbError::InvalidStructuralBlock {
33                    track_id,
34                    detail: format!("unknown kind {kind:?}"),
35                });
36            }
37            if ordinal < 0 {
38                return Err(DbError::InvalidStructuralBlock {
39                    track_id,
40                    detail: format!("negative ordinal {ordinal}"),
41                });
42            }
43            crate::error::check_field_len(
44                "structural_blocks",
45                "body",
46                body_len,
47                crate::limits::MAX_STRUCTURAL_BODY_LEN,
48            )?;
49            out.push(StructuralBlock {
50                kind,
51                ordinal: u64::try_from(ordinal).expect("ordinal guarded >= 0 above"),
52                body: r.get(3)?,
53            });
54        }
55        Ok(out)
56    }
57}
58
59impl Db<ReadWrite> {
60    /// Replace the track's structural blocks (FLAC STREAMINFO/SEEKTABLE).
61    pub fn set_structural_blocks(&self, track_id: i64, blocks: &[StructuralBlock]) -> Result<()> {
62        let tx = self.conn.unchecked_transaction()?;
63        tx.execute(
64            "DELETE FROM structural_blocks WHERE track_id = ?1",
65            params![track_id],
66        )?;
67        {
68            let mut stmt = tx.prepare(
69                "INSERT INTO structural_blocks (track_id, kind, ordinal, body) \
70                 VALUES (?1, ?2, ?3, ?4)",
71            )?;
72            for b in blocks {
73                stmt.execute(params![track_id, b.kind, b.ordinal, b.body])?;
74            }
75        }
76        tx.commit()?;
77        Ok(())
78    }
79}
80
81#[cfg(test)]
82mod guard_tests {
83    use crate::error::DbError;
84    use crate::{Db, Format, NewTrack};
85
86    fn db_with_track() -> (Db, i64) {
87        let db = Db::open_in_memory().unwrap();
88        let id = db
89            .upsert_track(&NewTrack {
90                backing_path: "/a.flac".into(),
91                format: Format::Flac,
92                audio_offset: 0,
93                audio_length: 1,
94                backing_size: 1,
95                backing_mtime_ns: 0,
96                backing_ctime_ns: 0,
97            })
98            .unwrap();
99        (db, id)
100    }
101
102    #[test]
103    fn rejects_oversize_body() {
104        let (db, id) = db_with_track();
105        db.conn
106            .execute_batch("PRAGMA ignore_check_constraints=ON")
107            .unwrap();
108        db.conn
109            .execute(
110                "INSERT INTO structural_blocks (track_id, kind, ordinal, body) \
111                 VALUES (?1, 'STREAMINFO', 0, zeroblob(16777216))",
112                rusqlite::params![id],
113            )
114            .unwrap();
115        let err = db.get_structural_blocks(id).unwrap_err();
116        assert!(
117            matches!(err, DbError::FieldTooLarge { field: "body", .. }),
118            "{err:?}"
119        );
120    }
121
122    #[test]
123    fn accepts_body_at_cap() {
124        let (db, id) = db_with_track();
125        db.conn
126            .execute(
127                "INSERT INTO structural_blocks (track_id, kind, ordinal, body) \
128                 VALUES (?1, 'STREAMINFO', 0, zeroblob(16777215))",
129                rusqlite::params![id],
130            )
131            .unwrap();
132        let rows = db.get_structural_blocks(id).unwrap();
133        assert_eq!(rows.len(), 1);
134        assert_eq!(rows[0].body.len(), 16_777_215);
135    }
136
137    #[test]
138    fn rejects_unknown_kind() {
139        let (db, id) = db_with_track();
140        db.conn
141            .execute_batch("PRAGMA ignore_check_constraints=ON")
142            .unwrap();
143        db.conn
144            .execute(
145                "INSERT INTO structural_blocks (track_id, kind, ordinal, body) \
146                 VALUES (?1, 'APPLICATION', 0, X'00')",
147                rusqlite::params![id],
148            )
149            .unwrap();
150        let err = db.get_structural_blocks(id).unwrap_err();
151        assert!(
152            matches!(err, DbError::InvalidStructuralBlock { .. }),
153            "{err:?}"
154        );
155    }
156
157    #[test]
158    fn rejects_negative_ordinal() {
159        let (db, id) = db_with_track();
160        db.conn
161            .execute_batch("PRAGMA ignore_check_constraints=ON")
162            .unwrap();
163        db.conn
164            .execute(
165                "INSERT INTO structural_blocks (track_id, kind, ordinal, body) \
166                 VALUES (?1, 'STREAMINFO', -1, X'00')",
167                rusqlite::params![id],
168            )
169            .unwrap();
170        let err = db.get_structural_blocks(id).unwrap_err();
171        assert!(
172            matches!(err, DbError::InvalidStructuralBlock { .. }),
173            "{err:?}"
174        );
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use crate::{Db, Format, NewTrack, StructuralBlock};
181
182    #[test]
183    fn structural_blocks_round_trip_and_replace() {
184        let db = Db::open_in_memory().unwrap();
185        let id = db
186            .upsert_track(&NewTrack {
187                backing_path: "/a.flac".into(),
188                format: Format::Flac,
189                audio_offset: 0,
190                audio_length: 1,
191                backing_size: 1,
192                backing_mtime_ns: 0,
193                backing_ctime_ns: 0,
194            })
195            .unwrap();
196        db.set_structural_blocks(
197            id,
198            &[
199                StructuralBlock {
200                    kind: "STREAMINFO".into(),
201                    ordinal: 0,
202                    body: vec![1, 2],
203                },
204                StructuralBlock {
205                    kind: "SEEKTABLE".into(),
206                    ordinal: 0,
207                    body: vec![3],
208                },
209            ],
210        )
211        .unwrap();
212        let got = db.get_structural_blocks(id).unwrap();
213        assert_eq!(got.len(), 2);
214        // ordered by kind: SEEKTABLE before STREAMINFO
215        assert_eq!(got[0].kind, "SEEKTABLE");
216        assert_eq!(got[1].body, vec![1, 2]);
217
218        db.set_structural_blocks(id, &[]).unwrap();
219        assert!(db.get_structural_blocks(id).unwrap().is_empty());
220    }
221}