use std::path::Path;
use sha2::{Digest, Sha256};
use super::segment::Index;
use concinnity_core::blob::CacheEntryKind;
const THUMBNAIL: CacheEntryKind = CacheEntryKind::Thumbnail;
const NAMES_KEY: &str = "names";
pub struct Thumbnails {
index: Index,
names: Vec<(String, String)>,
revision: u64,
}
impl Thumbnails {
pub fn open() -> Option<Self> {
Self::open_at(&super::anchored_path()?, super::identity::token()?)
}
fn open_at(path: &Path, token: u32) -> Option<Self> {
let index = Index::read(path, token);
let bytes = index.get(THUMBNAIL, NAMES_KEY)?;
Some(Self {
names: decode_names(&bytes)?,
revision: revision_of(&bytes),
index,
})
}
pub fn names(&self) -> &[(String, String)] {
&self.names
}
pub fn revision(&self) -> u64 {
self.revision
}
pub fn png(&self, key: &str) -> Option<Vec<u8>> {
self.index.get(THUMBNAIL, key)
}
}
pub(crate) fn hold(images: &[(String, Vec<u8>)], names: &[(String, String)]) {
for (key, png) in images {
super::store(THUMBNAIL, key, png);
}
let encoded = encode_names(names);
if super::load(THUMBNAIL, NAMES_KEY).as_deref() != Some(encoded.as_slice()) {
super::store(THUMBNAIL, NAMES_KEY, &encoded);
}
}
pub(crate) fn holds(key: &str) -> bool {
super::contains(THUMBNAIL, key)
}
fn encode_names(names: &[(String, String)]) -> Vec<u8> {
postcard::to_allocvec(names).unwrap_or_default()
}
fn decode_names(bytes: &[u8]) -> Option<Vec<(String, String)>> {
postcard::from_bytes(bytes).ok()
}
fn revision_of(names: &[u8]) -> u64 {
let digest: [u8; 32] = Sha256::digest(names).into();
u64::from_le_bytes(digest[..8].try_into().expect("eight bytes of a digest"))
}
#[cfg(test)]
mod tests {
use super::*;
const TOKEN: u32 = 0xB0BA;
fn names(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
pairs
.iter()
.map(|(n, k)| ((*n).to_string(), (*k).to_string()))
.collect()
}
fn write_set(path: &Path, images: &[(&str, &[u8])], pairs: &[(&str, &str)]) {
let encoded = encode_names(&names(pairs));
let mut items: Vec<(CacheEntryKind, &str, &[u8])> = images
.iter()
.map(|(key, png)| (THUMBNAIL, *key, *png))
.collect();
items.push((THUMBNAIL, NAMES_KEY, &encoded));
let index = Index::read(path, TOKEN);
assert!(super::super::segment::write(path, &index, &items, TOKEN));
}
#[test]
fn a_set_round_trips_through_a_segment() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("cache").join("1");
write_set(
&path,
&[("aa", &[1, 2, 3]), ("bb", &[4])],
&[("red_tex", "aa"), ("box_mesh", "bb")],
);
let thumbs = Thumbnails::open_at(&path, TOKEN).expect("a set");
assert_eq!(
thumbs.names(),
names(&[("red_tex", "aa"), ("box_mesh", "bb")])
);
assert_eq!(thumbs.png("aa"), Some(vec![1, 2, 3]));
assert_eq!(thumbs.png("nope"), None);
}
#[test]
fn two_names_may_address_one_image() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("1");
write_set(&path, &[("aa", &[9])], &[("one", "aa"), ("two", "aa")]);
let thumbs = Thumbnails::open_at(&path, TOKEN).expect("a set");
assert_eq!(thumbs.names().len(), 2);
assert_eq!(thumbs.png("aa"), Some(vec![9]));
}
#[test]
fn the_revision_follows_the_name_map() {
let dir = tempfile::tempdir().unwrap();
let one = dir.path().join("one");
let two = dir.path().join("two");
let three = dir.path().join("three");
let four = dir.path().join("four");
write_set(&one, &[("aa", &[1])], &[("red_tex", "aa")]);
write_set(&two, &[("aa", &[1])], &[("red_tex", "aa")]);
write_set(&three, &[("aa", &[1])], &[("blue_tex", "aa")]);
write_set(&four, &[("bb", &[1])], &[("red_tex", "bb")]);
let revision = |p: &Path| Thumbnails::open_at(p, TOKEN).expect("a set").revision();
assert_eq!(revision(&one), revision(&two), "an unchanged bake holds");
assert_ne!(revision(&one), revision(&three), "a rename shows");
assert_ne!(revision(&one), revision(&four), "a content change shows");
}
#[test]
fn an_absent_or_foreign_segment_opens_to_nothing() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("1");
assert!(Thumbnails::open_at(&path, TOKEN).is_none());
write_set(&path, &[("aa", &[1])], &[("red_tex", "aa")]);
assert!(Thumbnails::open_at(&path, TOKEN).is_some());
assert!(Thumbnails::open_at(&path, TOKEN + 1).is_none());
std::fs::write(&path, b"not a segment").unwrap();
assert!(Thumbnails::open_at(&path, TOKEN).is_none());
}
#[test]
fn a_segment_without_a_name_map_opens_to_nothing() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("1");
let index = Index::read(&path, TOKEN);
let payload: &[(CacheEntryKind, &str, &[u8])] =
&[(CacheEntryKind::Payload, "cafe", &[1, 2, 3])];
assert!(super::super::segment::write(&path, &index, payload, TOKEN));
assert!(Thumbnails::open_at(&path, TOKEN).is_none());
}
}