1use crate::error::{check_art_count, check_field_len};
2use crate::limits::{MAX_ART_DESCRIPTION_LEN, MAX_ART_MIME_LEN};
3use crate::models::{Art, ArtMeta, NewArt, TrackArt};
4use crate::{Db, ReadWrite, Result};
5use rusqlite::params;
6use sha2::{Digest, Sha256};
7
8pub(crate) fn sha256_hex(data: &[u8]) -> String {
9 format!("{:x}", base16ct::HexDisplay(&Sha256::digest(data)))
10}
11
12impl<M> Db<M> {
13 pub fn get_art(&self, id: i64) -> Result<Option<Art>> {
14 crate::query_optional(
15 &self.conn,
16 "SELECT id, sha256, mime, width, height, byte_len, data FROM art WHERE id = ?1",
17 params![id],
18 |r| {
19 Ok(Art {
20 id: r.get(0)?,
21 sha256: r.get(1)?,
22 mime: r.get(2)?,
23 width: r.get(3)?,
24 height: r.get(4)?,
25 byte_len: r.get(5)?,
26 data: r.get(6)?,
27 })
28 },
29 )
30 }
31
32 pub fn get_art_meta(&self, id: i64) -> Result<Option<ArtMeta>> {
35 crate::query_optional(
36 &self.conn,
37 "SELECT length(mime), mime, width, height, byte_len FROM art WHERE id = ?1",
38 params![id],
39 |r| {
40 check_field_len("art", "mime", r.get(0)?, MAX_ART_MIME_LEN)?;
41 Ok(ArtMeta {
42 mime: r.get(1)?,
43 width: r.get(2)?,
44 height: r.get(3)?,
45 byte_len: r.get(4)?,
46 })
47 },
48 )
49 }
50
51 pub fn read_art_chunk_into(&self, art_id: i64, offset: u64, buf: &mut [u8]) -> Result<()> {
56 let blob = self.conn.blob_open("main", "art", "data", art_id, true)?;
57 blob.read_at_exact(buf, crate::convert::usize_from(offset))?;
58 Ok(())
59 }
60
61 pub fn read_art_chunk(&self, art_id: i64, offset: u64, len: usize) -> Result<Vec<u8>> {
63 let mut buf = vec![0u8; len];
64 self.read_art_chunk_into(art_id, offset, &mut buf)?;
65 Ok(buf)
66 }
67
68 pub fn get_track_art(&self, track_id: i64) -> Result<Vec<TrackArt>> {
69 let mut stmt = self.conn.prepare_cached(
70 "SELECT length(description), art_id, picture_type, description, ordinal
71 FROM track_art WHERE track_id = ?1 ORDER BY ordinal",
72 )?;
73 let mut rows = stmt.query(params![track_id])?;
74 let mut out = Vec::new();
75 while let Some(r) = rows.next()? {
76 check_field_len(
77 "track_art",
78 "description",
79 r.get(0)?,
80 MAX_ART_DESCRIPTION_LEN,
81 )?;
82 out.push(TrackArt {
83 art_id: r.get(1)?,
84 picture_type: r.get(2)?,
85 description: r.get(3)?,
86 ordinal: r.get(4)?,
87 });
88 check_art_count(track_id, out.len())?;
89 }
90 Ok(out)
91 }
92
93 pub fn get_track_art_with_meta(
100 &self,
101 track_id: i64,
102 ) -> Result<Vec<(TrackArt, Option<ArtMeta>)>> {
103 let mut stmt = self.conn.prepare_cached(
104 "SELECT length(ta.description), ta.art_id, ta.picture_type, ta.description, ta.ordinal, \
105 length(a.mime), a.mime, a.width, a.height, a.byte_len \
106 FROM track_art ta LEFT JOIN art a ON a.id = ta.art_id \
107 WHERE ta.track_id = ?1 ORDER BY ta.ordinal",
108 )?;
109 let mut rows = stmt.query(params![track_id])?;
110 let mut out = Vec::new();
111 while let Some(r) = rows.next()? {
112 check_field_len(
113 "track_art",
114 "description",
115 r.get(0)?,
116 MAX_ART_DESCRIPTION_LEN,
117 )?;
118 let track_art = TrackArt {
119 art_id: r.get(1)?,
120 picture_type: r.get(2)?,
121 description: r.get(3)?,
122 ordinal: r.get(4)?,
123 };
124 let meta = match r.get::<_, Option<i64>>(5)? {
130 Some(mime_len) => {
131 check_field_len("art", "mime", mime_len, MAX_ART_MIME_LEN)?;
132 Some(ArtMeta {
133 mime: r.get(6)?,
134 width: r.get(7)?,
135 height: r.get(8)?,
136 byte_len: r.get(9)?,
137 })
138 }
139 None => None,
140 };
141 out.push((track_art, meta));
142 check_art_count(track_id, out.len())?;
143 }
144 Ok(out)
145 }
146}
147
148pub(crate) fn upsert_art_in(conn: &rusqlite::Connection, a: &NewArt) -> Result<i64> {
151 let sha = sha256_hex(&a.data);
152 conn.execute(
153 "INSERT INTO art (sha256, mime, width, height, byte_len, data)
154 VALUES (?1, ?2, ?3, ?4, ?5, ?6) ON CONFLICT(sha256) DO NOTHING",
155 params![sha, a.mime, a.width, a.height, a.data.len() as u64, a.data],
156 )?;
157 Ok(
158 conn.query_row("SELECT id FROM art WHERE sha256 = ?1", params![sha], |r| {
159 r.get(0)
160 })?,
161 )
162}
163
164pub(crate) fn set_track_art_in(
167 conn: &rusqlite::Connection,
168 track_id: i64,
169 items: &[TrackArt],
170) -> Result<()> {
171 conn.execute(
172 "DELETE FROM track_art WHERE track_id = ?1",
173 params![track_id],
174 )?;
175 let mut stmt = conn.prepare_cached(
176 "INSERT INTO track_art (track_id, art_id, picture_type, description, ordinal)
177 VALUES (?1, ?2, ?3, ?4, ?5)",
178 )?;
179 for it in items {
180 stmt.execute(params![
181 track_id,
182 it.art_id,
183 it.picture_type,
184 it.description,
185 it.ordinal
186 ])?;
187 }
188 Ok(())
189}
190
191impl Db<ReadWrite> {
192 pub fn upsert_art(&self, a: &NewArt) -> Result<i64> {
193 upsert_art_in(&self.conn, a)
194 }
195
196 pub fn set_track_art(&self, track_id: i64, items: &[TrackArt]) -> Result<()> {
197 let tx = self.conn.unchecked_transaction()?;
198 set_track_art_in(&tx, track_id, items)?;
199 tx.commit()?;
200 Ok(())
201 }
202
203 pub fn gc_orphan_art(&self) -> Result<usize> {
211 let removed = self.conn.execute(
212 "DELETE FROM art WHERE NOT EXISTS \
213 (SELECT 1 FROM track_art WHERE track_art.art_id = art.id)",
214 [],
215 )?;
216 Ok(removed)
217 }
218}
219
220#[cfg(test)]
221mod guard_tests {
222 use crate::error::DbError;
223 use crate::models::{NewArt, TrackArt};
224 use crate::{Db, Format, NewTrack};
225
226 fn db_track_art() -> (Db, i64, i64) {
227 let db = Db::open_in_memory().unwrap();
228 let track = db
229 .upsert_track(&NewTrack {
230 backing_path: "/a.flac".into(),
231 format: Format::Flac,
232 audio_offset: 0,
233 audio_length: 1,
234 backing_size: 1,
235 backing_mtime_ns: 0,
236 backing_ctime_ns: 0,
237 })
238 .unwrap();
239 let art = db
240 .upsert_art(&NewArt {
241 mime: "image/png".into(),
242 width: None,
243 height: None,
244 data: vec![0u8],
245 })
246 .unwrap();
247 (db, track, art)
248 }
249
250 #[test]
251 fn get_art_meta_rejects_oversize_mime() {
252 let (db, _t, _art) = db_track_art();
253 db.conn
254 .execute_batch("PRAGMA ignore_check_constraints=ON")
255 .unwrap();
256 let mime = "x".repeat(256);
261 db.conn
262 .execute(
263 "INSERT INTO art (sha256, mime, width, height, byte_len, data) \
264 VALUES (?1, ?2, NULL, NULL, 1, X'00')",
265 rusqlite::params!["b".repeat(64), mime],
266 )
267 .unwrap();
268 let bad = db.conn.last_insert_rowid();
269 let err = db.get_art_meta(bad).unwrap_err();
270 assert!(
271 matches!(
272 err,
273 DbError::FieldTooLarge {
274 table: "art",
275 field: "mime",
276 ..
277 }
278 ),
279 "{err:?}"
280 );
281 }
282
283 #[test]
284 fn get_track_art_rejects_oversize_description() {
285 let (db, track, art) = db_track_art();
286 db.conn
287 .execute_batch("PRAGMA ignore_check_constraints=ON")
288 .unwrap();
289 let desc = "d".repeat(1025);
290 db.set_track_art(
291 track,
292 &[TrackArt {
293 art_id: art,
294 picture_type: 3,
295 description: desc,
296 ordinal: 0,
297 }],
298 )
299 .unwrap();
300 let err = db.get_track_art(track).unwrap_err();
301 assert!(
302 matches!(
303 err,
304 DbError::FieldTooLarge {
305 table: "track_art",
306 field: "description",
307 ..
308 }
309 ),
310 "{err:?}"
311 );
312 }
313
314 #[test]
315 fn get_track_art_accepts_description_at_cap() {
316 let (db, track, art) = db_track_art();
317 let desc = "d".repeat(1024);
318 db.set_track_art(
319 track,
320 &[TrackArt {
321 art_id: art,
322 picture_type: 3,
323 description: desc,
324 ordinal: 0,
325 }],
326 )
327 .unwrap();
328 assert_eq!(db.get_track_art(track).unwrap()[0].description.len(), 1024);
329 }
330
331 #[test]
332 fn get_track_art_rejects_excess_rows() {
333 let (db, track, art) = db_track_art();
334 let tx = db.conn.unchecked_transaction().unwrap();
338 let mut stmt = tx
339 .prepare(
340 "INSERT INTO track_art (track_id, art_id, picture_type, description, ordinal) \
341 VALUES (?1, ?2, 3, '', ?3)",
342 )
343 .unwrap();
344 for i in 0..4097 {
345 stmt.execute(rusqlite::params![track, art, i]).unwrap();
346 }
347 drop(stmt);
348 tx.commit().unwrap();
349 let err = db.get_track_art(track).unwrap_err();
350 assert!(matches!(err, DbError::TooManyArtRows { .. }), "{err:?}");
351 }
352
353 #[test]
354 fn get_track_art_accepts_rows_at_cap() {
355 let (db, track, art) = db_track_art();
356 let tx = db.conn.unchecked_transaction().unwrap();
357 let mut stmt = tx
358 .prepare(
359 "INSERT INTO track_art (track_id, art_id, picture_type, description, ordinal) \
360 VALUES (?1, ?2, 3, '', ?3)",
361 )
362 .unwrap();
363 for i in 0..4096 {
364 stmt.execute(rusqlite::params![track, art, i]).unwrap();
365 }
366 drop(stmt);
367 tx.commit().unwrap();
368 assert_eq!(db.get_track_art(track).unwrap().len(), 4096);
369 }
370
371 #[test]
372 fn sha256_hex_matches_known_digest() {
373 assert_eq!(
375 super::sha256_hex(b"abc"),
376 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
377 );
378 }
379
380 #[test]
381 fn gc_orphan_art_not_exists_is_null_safe() {
382 let conn = rusqlite::Connection::open_in_memory().unwrap();
389 conn.execute_batch(
390 "CREATE TABLE art (id INTEGER PRIMARY KEY);
391 CREATE TABLE track_art (art_id INTEGER);
392 INSERT INTO art (id) VALUES (1), (2);
393 INSERT INTO track_art (art_id) VALUES (1), (NULL);",
394 )
395 .unwrap();
396
397 let not_in = conn
399 .execute(
400 "DELETE FROM art WHERE id NOT IN (SELECT art_id FROM track_art)",
401 [],
402 )
403 .unwrap();
404 assert_eq!(not_in, 0, "NOT IN deletes nothing when a NULL is present");
405
406 let not_exists = conn
408 .execute(
409 "DELETE FROM art WHERE NOT EXISTS \
410 (SELECT 1 FROM track_art WHERE track_art.art_id = art.id)",
411 [],
412 )
413 .unwrap();
414 assert_eq!(
415 not_exists, 1,
416 "NOT EXISTS removes the orphan despite the NULL"
417 );
418 }
419}