1use std::path::Path;
18
19use sha2::{Digest, Sha256};
20
21use super::segment::Index;
22use concinnity_core::blob::CacheEntryKind;
23
24const THUMBNAIL: CacheEntryKind = CacheEntryKind::Thumbnail;
25
26const NAMES_KEY: &str = "names";
29
30pub struct Thumbnails {
37 index: Index,
38 names: Vec<(String, String)>,
39 revision: u64,
40}
41
42impl Thumbnails {
43 pub fn open() -> Option<Self> {
47 Self::open_at(&super::anchored_path()?, super::identity::token()?)
48 }
49
50 fn open_at(path: &Path, token: u32) -> Option<Self> {
51 let index = Index::read(path, token);
52 let bytes = index.get(THUMBNAIL, NAMES_KEY)?;
53 Some(Self {
54 names: decode_names(&bytes)?,
55 revision: revision_of(&bytes),
56 index,
57 })
58 }
59
60 pub fn names(&self) -> &[(String, String)] {
62 &self.names
63 }
64
65 pub fn revision(&self) -> u64 {
70 self.revision
71 }
72
73 pub fn png(&self, key: &str) -> Option<Vec<u8>> {
78 self.index.get(THUMBNAIL, key)
79 }
80}
81
82pub(crate) fn hold(images: &[(String, Vec<u8>)], names: &[(String, String)]) {
88 for (key, png) in images {
89 super::store(THUMBNAIL, key, png);
90 }
91 let encoded = encode_names(names);
92 if super::load(THUMBNAIL, NAMES_KEY).as_deref() != Some(encoded.as_slice()) {
93 super::store(THUMBNAIL, NAMES_KEY, &encoded);
94 }
95}
96
97pub(crate) fn holds(key: &str) -> bool {
99 super::contains(THUMBNAIL, key)
100}
101
102fn encode_names(names: &[(String, String)]) -> Vec<u8> {
103 postcard::to_allocvec(names).unwrap_or_default()
104}
105
106fn decode_names(bytes: &[u8]) -> Option<Vec<(String, String)>> {
107 postcard::from_bytes(bytes).ok()
108}
109
110fn revision_of(names: &[u8]) -> u64 {
113 let digest: [u8; 32] = Sha256::digest(names).into();
114 u64::from_le_bytes(digest[..8].try_into().expect("eight bytes of a digest"))
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 const TOKEN: u32 = 0xB0BA;
122
123 fn names(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
124 pairs
125 .iter()
126 .map(|(n, k)| ((*n).to_string(), (*k).to_string()))
127 .collect()
128 }
129
130 fn write_set(path: &Path, images: &[(&str, &[u8])], pairs: &[(&str, &str)]) {
133 let encoded = encode_names(&names(pairs));
134 let mut items: Vec<(CacheEntryKind, &str, &[u8])> = images
135 .iter()
136 .map(|(key, png)| (THUMBNAIL, *key, *png))
137 .collect();
138 items.push((THUMBNAIL, NAMES_KEY, &encoded));
139 let index = Index::read(path, TOKEN);
140 assert!(super::super::segment::write(path, &index, &items, TOKEN));
141 }
142
143 #[test]
144 fn a_set_round_trips_through_a_segment() {
145 let dir = tempfile::tempdir().unwrap();
146 let path = dir.path().join("segment");
147 write_set(
148 &path,
149 &[("aa", &[1, 2, 3]), ("bb", &[4])],
150 &[("red_tex", "aa"), ("box_mesh", "bb")],
151 );
152
153 let thumbs = Thumbnails::open_at(&path, TOKEN).expect("a set");
154 assert_eq!(
155 thumbs.names(),
156 names(&[("red_tex", "aa"), ("box_mesh", "bb")])
157 );
158 assert_eq!(thumbs.png("aa"), Some(vec![1, 2, 3]));
159 assert_eq!(thumbs.png("nope"), None);
160 }
161
162 #[test]
165 fn two_names_may_address_one_image() {
166 let dir = tempfile::tempdir().unwrap();
167 let path = dir.path().join("1");
168 write_set(&path, &[("aa", &[9])], &[("one", "aa"), ("two", "aa")]);
169
170 let thumbs = Thumbnails::open_at(&path, TOKEN).expect("a set");
171 assert_eq!(thumbs.names().len(), 2);
172 assert_eq!(thumbs.png("aa"), Some(vec![9]));
173 }
174
175 #[test]
179 fn the_revision_follows_the_name_map() {
180 let dir = tempfile::tempdir().unwrap();
181 let one = dir.path().join("one");
182 let two = dir.path().join("two");
183 let three = dir.path().join("three");
184 let four = dir.path().join("four");
185 write_set(&one, &[("aa", &[1])], &[("red_tex", "aa")]);
186 write_set(&two, &[("aa", &[1])], &[("red_tex", "aa")]);
187 write_set(&three, &[("aa", &[1])], &[("blue_tex", "aa")]);
188 write_set(&four, &[("bb", &[1])], &[("red_tex", "bb")]);
189
190 let revision = |p: &Path| Thumbnails::open_at(p, TOKEN).expect("a set").revision();
191 assert_eq!(revision(&one), revision(&two), "an unchanged bake holds");
192 assert_ne!(revision(&one), revision(&three), "a rename shows");
193 assert_ne!(revision(&one), revision(&four), "a content change shows");
194 }
195
196 #[test]
199 fn an_absent_or_foreign_segment_opens_to_nothing() {
200 let dir = tempfile::tempdir().unwrap();
201 let path = dir.path().join("1");
202 assert!(Thumbnails::open_at(&path, TOKEN).is_none());
203
204 write_set(&path, &[("aa", &[1])], &[("red_tex", "aa")]);
205 assert!(Thumbnails::open_at(&path, TOKEN).is_some());
206 assert!(Thumbnails::open_at(&path, TOKEN + 1).is_none());
207
208 std::fs::write(&path, b"not a segment").unwrap();
209 assert!(Thumbnails::open_at(&path, TOKEN).is_none());
210 }
211
212 #[test]
215 fn a_segment_without_a_name_map_opens_to_nothing() {
216 let dir = tempfile::tempdir().unwrap();
217 let path = dir.path().join("1");
218 let index = Index::read(&path, TOKEN);
219 let payload: &[(CacheEntryKind, &str, &[u8])] =
220 &[(CacheEntryKind::Payload, "cafe", &[1, 2, 3])];
221 assert!(super::super::segment::write(&path, &index, payload, TOKEN));
222
223 assert!(Thumbnails::open_at(&path, TOKEN).is_none());
224 }
225}