wallust/
cache.rs

1//! Cache functions, serde + serde_json
2use std::fmt;
3use std::fs;
4use std::fs::File;
5use std::io::BufReader;
6use std::io::Write;
7use std::path::Path;
8use std::path::PathBuf;
9use palette::Srgb;
10
11use crate::colors::Colors;
12use crate::config::Config;
13
14use anyhow::{Result, Context};
15
16/// Cache versioning, to avoid breaks and missreadings.
17/// For example, when there is an internal change in how the
18/// scheme is generated, the cache format won't change, however,
19/// there is a need for a regeneration, so we bump up the version.
20pub const CACHE_VER: &str = "1.7";
21
22/// Used to manage cache, rather than passing arguments in main() a lot
23#[derive(Debug, Default)]
24pub struct Cache {
25    /// Path of the cache, this is the path read.
26    pub path: PathBuf,
27    /// backend file, doesn't include de thereshold since it doesn't affects it
28    pub back: PathBuf,
29    /// colorscace file + threshold
30    pub cs: PathBuf,
31    /// palette file + threshold
32    pub palette: PathBuf,
33
34    /// Path name
35    pub name: PathBuf,
36}
37
38/// Simply print the path when trying to display the [`Cache`] struct
39impl fmt::Display for Cache {
40    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
41        write!(f, "{}", self.path.display())
42    }
43}
44
45/// Simple shadow for colorscheme return type
46type CSret = (Vec<Srgb>, Vec<Srgb>, bool);
47
48/// Cache order
49#[derive(Debug)]
50pub enum IsCached {
51    None,
52    Backend,
53    BackendnCS,
54    BackendnCSnPalette,
55}
56
57impl Cache {
58    /// # Filename structure, magic numbers (cachefmt) after this impl block:
59    /// *Each hash image has it's own dir*, inside there is multiple files:
60    /// 1. Backend file, with the full name, maybe reductant with the `full` backend.
61    /// 2. ColorSpace + threshold, since it depends on the threshold
62    /// 3. Scheme + ColorSpace + threshold, since the palette depends on the colorspace, and the colorspace on the threshold
63    ///    This new structure allows you to reuse some parts, when configuring, avoding more time waiting.
64    pub fn new(file: &Path, c: &Config, cache_path: &Path) -> Result<Self> {
65        // create cache (e.g. `~/.cache/wallust`)
66        let cachepath = cache_path.join("wallust");
67
68        // hash value for the file, since you can duplicate it, but the contents are the same.
69        let hash  = base36(fnv1a(&std::fs::read(file)?));
70
71        let name = cachepath.join(format!("{hash}_{CACHE_VER}"));
72        // Create cache dir (with all of it's parents)
73        fs::create_dir_all(&name).with_context(|| format!("Failed to create {}", cachepath.display()))?;
74
75        let th    = if c.true_th == 0 { "auto" } else { &c.true_th.to_string() };
76        // wallust/image_1.0/
77        let base = cachepath.join(format!("{hash}_{CACHE_VER}"));
78
79        let back = c.backend.to_string();
80        let cs  = c.color_space.to_string();
81        let palet = c.palette.to_string();
82
83        Ok(Self {
84            path: cachepath,
85            name,
86            back: base.join(&back),
87            cs: base.join(format!("{back}_{cs}_{th}")),
88            palette: base.join(format!("{back}_{cs}_{th}_{palet}")),
89        })
90    }
91
92    pub fn read_backend(&self) -> Result<Vec<u8>> { read_json(&self.back) }
93    pub fn read_cs(&self) -> Result<CSret> { read_json(&self.cs) }
94    pub fn read_palette(&self) -> Result<Colors> { read_json(&self.palette) }
95    pub fn write_backend(&self, bytes: &[u8]) -> Result<()> { write_json(&self.back, &bytes, &self.to_string(), false) }
96    pub fn write_cs(&self, colorspaces: &CSret) -> Result<()> { write_json(&self.cs, colorspaces, &self.to_string(), false) }
97    pub fn write_palette(&self, scheme: &Colors) -> Result<()> { write_json(&self.palette, scheme, &self.to_string(), true) }
98
99    pub fn is_cached_all(&self) -> IsCached {
100        let b  = self.back.exists();
101        let cs = self.cs.exists();
102        let p  = self.palette.exists();
103
104        if b && cs && p {
105            IsCached::BackendnCSnPalette
106        } else if b && cs {
107            IsCached::BackendnCS
108        } else if b {
109            IsCached::Backend
110        } else {
111            IsCached::None
112        }
113    }
114}
115
116/// path is the new location of the file to be written to
117/// value the contents of it, `cachepath` is only to print the cache absolute path,
118/// and pretty to use serde_to_string_pretty
119pub fn write_json<P: AsRef<std::path::Path>, T: serde::Serialize>(path: P, value: &T, cachepath: &str, pretty: bool) -> anyhow::Result<()> {
120    let serde_to_string = if pretty { serde_json::to_string_pretty } else { serde_json::to_string };
121    Ok(File::create(&path)?
122        .write_all(
123            serde_to_string(value)
124            .with_context(|| format!("Failed to deserilize from the json cached file: '{cachepath}':"))?
125            .as_bytes()
126        )?
127    )
128}
129
130fn read_json<P: AsRef<std::path::Path>, T: serde::de::DeserializeOwned>(path: P) -> anyhow::Result<T> {
131    let path = path.as_ref();
132    let f = File::open(path).with_context(|| format!("Failed to open cache file '{}'", path.display()))?;
133    serde_json::from_reader(BufReader::new(f))
134        .with_context(|| format!("Failed to parse JSON in cache file '{}'", path.display()))
135}
136
137
138/* helpers */
139
140/// Pretty fcking fast hashing
141/// the 32 bit version, should be enough for this use case
142/// Ref: https://en.m.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
143pub fn fnv1a(bytes: &[u8]) -> u32 {
144    let mut hash = 2166136261;
145
146    for byte in bytes {
147        hash ^= *byte as u32;
148        hash = hash.wrapping_mul(16777619);
149    }
150
151    hash
152}
153
154/// simple base36 encoding
155/// Also, there is no need to decode, since it should match if the contents of the file are the
156/// same, else just generate a new scheme.
157/// ref: https://stackoverflow.com/questions/50277050/format-convert-a-number-to-a-string-in-any-base-including-bases-other-than-deci
158pub fn base36(n: u32) -> String {
159    let mut n = n;
160    let mut result = vec![];
161
162    loop {
163        let m = n % 36;
164        n /= 36;
165        result.push(std::char::from_digit(m, 36).expect("is between [2; 36]"));
166        if n == 0 { break; }
167    }
168    result.into_iter().rev().collect()
169}