1use crate::error::DbError;
2use crate::models::StructuralBlock;
3use crate::{Db, ReadWrite, Result};
4use rusqlite::params;
5
6impl<M> Db<M> {
7 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 pub fn get_structural_blocks(&self, track_id: i64) -> Result<Vec<StructuralBlock>> {
21 let mut stmt = self.conn.prepare_cached(
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
59pub(crate) fn set_structural_blocks_in(
62 conn: &rusqlite::Connection,
63 track_id: i64,
64 blocks: &[StructuralBlock],
65) -> Result<()> {
66 conn.execute(
67 "DELETE FROM structural_blocks WHERE track_id = ?1",
68 params![track_id],
69 )?;
70 let mut stmt = conn.prepare_cached(
71 "INSERT INTO structural_blocks (track_id, kind, ordinal, body) \
72 VALUES (?1, ?2, ?3, ?4)",
73 )?;
74 for b in blocks {
75 stmt.execute(params![track_id, b.kind, b.ordinal, b.body])?;
76 }
77 Ok(())
78}
79
80impl Db<ReadWrite> {
81 pub fn set_structural_blocks(&self, track_id: i64, blocks: &[StructuralBlock]) -> Result<()> {
83 let tx = self.conn.unchecked_transaction()?;
84 set_structural_blocks_in(&tx, track_id, blocks)?;
85 tx.commit()?;
86 Ok(())
87 }
88}
89
90#[cfg(test)]
91mod guard_tests {
92 use crate::error::DbError;
93 use crate::{Db, Format, NewTrack};
94
95 fn db_with_track() -> (Db, i64) {
96 let db = Db::open_in_memory().unwrap();
97 let id = db
98 .upsert_track(&NewTrack {
99 backing_path: "/a.flac".into(),
100 format: Format::Flac,
101 audio_offset: 0,
102 audio_length: 1,
103 backing_size: 1,
104 backing_mtime_ns: 0,
105 backing_ctime_ns: 0,
106 })
107 .unwrap();
108 (db, id)
109 }
110
111 #[test]
112 fn rejects_oversize_body() {
113 let (db, id) = db_with_track();
114 db.conn
115 .execute_batch("PRAGMA ignore_check_constraints=ON")
116 .unwrap();
117 db.conn
118 .execute(
119 "INSERT INTO structural_blocks (track_id, kind, ordinal, body) \
120 VALUES (?1, 'STREAMINFO', 0, zeroblob(16777216))",
121 rusqlite::params![id],
122 )
123 .unwrap();
124 let err = db.get_structural_blocks(id).unwrap_err();
125 assert!(
126 matches!(err, DbError::FieldTooLarge { field: "body", .. }),
127 "{err:?}"
128 );
129 }
130
131 #[test]
132 fn accepts_body_at_cap() {
133 let (db, id) = db_with_track();
134 db.conn
135 .execute(
136 "INSERT INTO structural_blocks (track_id, kind, ordinal, body) \
137 VALUES (?1, 'STREAMINFO', 0, zeroblob(16777215))",
138 rusqlite::params![id],
139 )
140 .unwrap();
141 let rows = db.get_structural_blocks(id).unwrap();
142 assert_eq!(rows.len(), 1);
143 assert_eq!(rows[0].body.len(), 16_777_215);
144 }
145
146 #[test]
147 fn rejects_unknown_kind() {
148 let (db, id) = db_with_track();
149 db.conn
150 .execute_batch("PRAGMA ignore_check_constraints=ON")
151 .unwrap();
152 db.conn
153 .execute(
154 "INSERT INTO structural_blocks (track_id, kind, ordinal, body) \
155 VALUES (?1, 'APPLICATION', 0, X'00')",
156 rusqlite::params![id],
157 )
158 .unwrap();
159 let err = db.get_structural_blocks(id).unwrap_err();
160 assert!(
161 matches!(err, DbError::InvalidStructuralBlock { .. }),
162 "{err:?}"
163 );
164 }
165
166 #[test]
167 fn rejects_negative_ordinal() {
168 let (db, id) = db_with_track();
169 db.conn
170 .execute_batch("PRAGMA ignore_check_constraints=ON")
171 .unwrap();
172 db.conn
173 .execute(
174 "INSERT INTO structural_blocks (track_id, kind, ordinal, body) \
175 VALUES (?1, 'STREAMINFO', -1, X'00')",
176 rusqlite::params![id],
177 )
178 .unwrap();
179 let err = db.get_structural_blocks(id).unwrap_err();
180 assert!(
181 matches!(err, DbError::InvalidStructuralBlock { .. }),
182 "{err:?}"
183 );
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use crate::{Db, Format, NewTrack, StructuralBlock};
190
191 #[test]
192 fn structural_blocks_round_trip_and_replace() {
193 let db = Db::open_in_memory().unwrap();
194 let id = db
195 .upsert_track(&NewTrack {
196 backing_path: "/a.flac".into(),
197 format: Format::Flac,
198 audio_offset: 0,
199 audio_length: 1,
200 backing_size: 1,
201 backing_mtime_ns: 0,
202 backing_ctime_ns: 0,
203 })
204 .unwrap();
205 db.set_structural_blocks(
206 id,
207 &[
208 StructuralBlock {
209 kind: "STREAMINFO".into(),
210 ordinal: 0,
211 body: vec![1, 2],
212 },
213 StructuralBlock {
214 kind: "SEEKTABLE".into(),
215 ordinal: 0,
216 body: vec![3],
217 },
218 ],
219 )
220 .unwrap();
221 let got = db.get_structural_blocks(id).unwrap();
222 assert_eq!(got.len(), 2);
223 assert_eq!(got[0].kind, "SEEKTABLE");
225 assert_eq!(got[1].body, vec![1, 2]);
226
227 db.set_structural_blocks(id, &[]).unwrap();
228 assert!(db.get_structural_blocks(id).unwrap().is_empty());
229 }
230}