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(
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 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 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}