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 {
12 let mut s = String::with_capacity(64);
13 for b in Sha256::digest(data) {
14 use std::fmt::Write;
15 let _ = write!(s, "{b:02x}");
16 }
17 s
18}
19
20impl<M> Db<M> {
21 pub fn get_art(&self, id: i64) -> Result<Option<Art>> {
22 let mut stmt = self.conn.prepare(
23 "SELECT id, sha256, mime, width, height, byte_len, data FROM art WHERE id = ?1",
24 )?;
25 let mut rows = stmt.query(params![id])?;
26 match rows.next()? {
27 Some(r) => Ok(Some(Art {
28 id: r.get(0)?,
29 sha256: r.get(1)?,
30 mime: r.get(2)?,
31 width: r.get(3)?,
32 height: r.get(4)?,
33 byte_len: r.get(5)?,
34 data: r.get(6)?,
35 })),
36 None => Ok(None),
37 }
38 }
39
40 pub fn get_art_meta(&self, id: i64) -> Result<Option<ArtMeta>> {
43 let mut stmt = self
44 .conn
45 .prepare("SELECT length(mime), mime, width, height, byte_len FROM art WHERE id = ?1")?;
46 let mut rows = stmt.query(params![id])?;
47 match rows.next()? {
48 Some(r) => {
49 check_field_len("art", "mime", r.get(0)?, MAX_ART_MIME_LEN)?;
50 Ok(Some(ArtMeta {
51 mime: r.get(1)?,
52 width: r.get(2)?,
53 height: r.get(3)?,
54 byte_len: r.get(4)?,
55 }))
56 }
57 None => Ok(None),
58 }
59 }
60
61 pub fn read_art_chunk_into(&self, art_id: i64, offset: u64, buf: &mut [u8]) -> Result<()> {
66 let blob = self.conn.blob_open("main", "art", "data", art_id, true)?;
67 blob.read_at_exact(buf, crate::convert::usize_from(offset))?;
68 Ok(())
69 }
70
71 pub fn read_art_chunk(&self, art_id: i64, offset: u64, len: usize) -> Result<Vec<u8>> {
73 let mut buf = vec![0u8; len];
74 self.read_art_chunk_into(art_id, offset, &mut buf)?;
75 Ok(buf)
76 }
77
78 pub fn get_track_art(&self, track_id: i64) -> Result<Vec<TrackArt>> {
79 let mut stmt = self.conn.prepare(
80 "SELECT length(description), art_id, picture_type, description, ordinal
81 FROM track_art WHERE track_id = ?1 ORDER BY ordinal",
82 )?;
83 let mut rows = stmt.query(params![track_id])?;
84 let mut out = Vec::new();
85 while let Some(r) = rows.next()? {
86 check_field_len(
87 "track_art",
88 "description",
89 r.get(0)?,
90 MAX_ART_DESCRIPTION_LEN,
91 )?;
92 out.push(TrackArt {
93 art_id: r.get(1)?,
94 picture_type: r.get(2)?,
95 description: r.get(3)?,
96 ordinal: r.get(4)?,
97 });
98 check_art_count(track_id, out.len())?;
99 }
100 Ok(out)
101 }
102}
103
104impl Db<ReadWrite> {
105 pub fn upsert_art(&self, a: &NewArt) -> Result<i64> {
106 let sha = sha256_hex(&a.data);
107 self.conn.execute(
108 "INSERT INTO art (sha256, mime, width, height, byte_len, data)
109 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
110 ON CONFLICT(sha256) DO NOTHING",
111 params![sha, a.mime, a.width, a.height, a.data.len() as u64, a.data],
112 )?;
113 let id =
114 self.conn
115 .query_row("SELECT id FROM art WHERE sha256 = ?1", params![sha], |r| {
116 r.get(0)
117 })?;
118 Ok(id)
119 }
120
121 pub fn set_track_art(&self, track_id: i64, items: &[TrackArt]) -> Result<()> {
122 let tx = self.conn.unchecked_transaction()?;
123 tx.execute(
124 "DELETE FROM track_art WHERE track_id = ?1",
125 params![track_id],
126 )?;
127 {
128 let mut stmt = tx.prepare(
129 "INSERT INTO track_art (track_id, art_id, picture_type, description, ordinal)
130 VALUES (?1, ?2, ?3, ?4, ?5)",
131 )?;
132 for it in items {
133 stmt.execute(params![
134 track_id,
135 it.art_id,
136 it.picture_type,
137 it.description,
138 it.ordinal
139 ])?;
140 }
141 }
142 tx.commit()?;
143 Ok(())
144 }
145
146 pub fn gc_orphan_art(&self) -> Result<usize> {
149 let removed = self.conn.execute(
150 "DELETE FROM art WHERE id NOT IN (SELECT art_id FROM track_art)",
151 [],
152 )?;
153 Ok(removed)
154 }
155}
156
157#[cfg(test)]
158mod guard_tests {
159 use crate::error::DbError;
160 use crate::models::{NewArt, TrackArt};
161 use crate::{Db, Format, NewTrack};
162
163 fn db_track_art() -> (Db, i64, i64) {
164 let db = Db::open_in_memory().unwrap();
165 let track = db
166 .upsert_track(&NewTrack {
167 backing_path: "/a.flac".into(),
168 format: Format::Flac,
169 audio_offset: 0,
170 audio_length: 1,
171 backing_size: 1,
172 backing_mtime_ns: 0,
173 backing_ctime_ns: 0,
174 })
175 .unwrap();
176 let art = db
177 .upsert_art(&NewArt {
178 mime: "image/png".into(),
179 width: None,
180 height: None,
181 data: vec![0u8],
182 })
183 .unwrap();
184 (db, track, art)
185 }
186
187 #[test]
188 fn get_art_meta_rejects_oversize_mime() {
189 let (db, _t, _art) = db_track_art();
190 db.conn
191 .execute_batch("PRAGMA ignore_check_constraints=ON")
192 .unwrap();
193 let mime = "x".repeat(256);
198 db.conn
199 .execute(
200 "INSERT INTO art (sha256, mime, width, height, byte_len, data) \
201 VALUES (?1, ?2, NULL, NULL, 1, X'00')",
202 rusqlite::params!["b".repeat(64), mime],
203 )
204 .unwrap();
205 let bad = db.conn.last_insert_rowid();
206 let err = db.get_art_meta(bad).unwrap_err();
207 assert!(
208 matches!(
209 err,
210 DbError::FieldTooLarge {
211 table: "art",
212 field: "mime",
213 ..
214 }
215 ),
216 "{err:?}"
217 );
218 }
219
220 #[test]
221 fn get_track_art_rejects_oversize_description() {
222 let (db, track, art) = db_track_art();
223 db.conn
224 .execute_batch("PRAGMA ignore_check_constraints=ON")
225 .unwrap();
226 let desc = "d".repeat(1025);
227 db.set_track_art(
228 track,
229 &[TrackArt {
230 art_id: art,
231 picture_type: 3,
232 description: desc,
233 ordinal: 0,
234 }],
235 )
236 .unwrap();
237 let err = db.get_track_art(track).unwrap_err();
238 assert!(
239 matches!(
240 err,
241 DbError::FieldTooLarge {
242 table: "track_art",
243 field: "description",
244 ..
245 }
246 ),
247 "{err:?}"
248 );
249 }
250
251 #[test]
252 fn get_track_art_accepts_description_at_cap() {
253 let (db, track, art) = db_track_art();
254 let desc = "d".repeat(1024);
255 db.set_track_art(
256 track,
257 &[TrackArt {
258 art_id: art,
259 picture_type: 3,
260 description: desc,
261 ordinal: 0,
262 }],
263 )
264 .unwrap();
265 assert_eq!(db.get_track_art(track).unwrap()[0].description.len(), 1024);
266 }
267
268 #[test]
269 fn get_track_art_rejects_excess_rows() {
270 let (db, track, art) = db_track_art();
271 let tx = db.conn.unchecked_transaction().unwrap();
275 let mut stmt = tx
276 .prepare(
277 "INSERT INTO track_art (track_id, art_id, picture_type, description, ordinal) \
278 VALUES (?1, ?2, 3, '', ?3)",
279 )
280 .unwrap();
281 for i in 0..4097 {
282 stmt.execute(rusqlite::params![track, art, i]).unwrap();
283 }
284 drop(stmt);
285 tx.commit().unwrap();
286 let err = db.get_track_art(track).unwrap_err();
287 assert!(matches!(err, DbError::TooManyArtRows { .. }), "{err:?}");
288 }
289
290 #[test]
291 fn get_track_art_accepts_rows_at_cap() {
292 let (db, track, art) = db_track_art();
293 let tx = db.conn.unchecked_transaction().unwrap();
294 let mut stmt = tx
295 .prepare(
296 "INSERT INTO track_art (track_id, art_id, picture_type, description, ordinal) \
297 VALUES (?1, ?2, 3, '', ?3)",
298 )
299 .unwrap();
300 for i in 0..4096 {
301 stmt.execute(rusqlite::params![track, art, i]).unwrap();
302 }
303 drop(stmt);
304 tx.commit().unwrap();
305 assert_eq!(db.get_track_art(track).unwrap().len(), 4096);
306 }
307}