duat_core/
cache.rs

1//! Caching utilities for Duat
2//!
3//! The cache in Duat is used when restoring previous information from
4//! a previous Duat instance, or when reloading Duat's configuration
5//! crate.
6//!
7//! One example for where this is used is the [`Ui`], which has
8//! information about how much a given file should be scrolled when
9//! opening it in Duat. Another example is [`Cursors`], which stores a
10//! list of open [`Cursor`]s, to start the file on.
11//!
12//! Plugins are able to cache any type that implements [`Serialize`]
13//! and [`Deserialize`]. Duat provides these traits from [`serde`],
14//! but in order to derive them, you will need a small addition on the
15//! declaration of the type:
16//!
17//! ```rust
18//! # use duat_core::cache::{Deserialize, Serialize};
19//! #[derive(Deserialize, Serialize)]
20//! #[serde(crate = "duat_core::cache::serde")]
21//! enum MyCacheableEnum {
22//!     // ...
23//! }
24//! ```
25//!
26//! [`Ui`]: crate::ui::Ui
27//! [`Cursors`]: crate::mode::Cursors
28//! [`Cursor`]: crate::mode::Cursor
29//! [`Point`]: crate::text::Point
30use std::{any::TypeId, fs::File, path::PathBuf};
31
32use base64::Engine;
33pub use bincode;
34
35use self::bincode::{
36    Decode, Encode,
37    config::{Configuration, Fixint, LittleEndian, NoLimit},
38    encode_into_std_write,
39};
40use crate::{duat_name, src_crate};
41
42/// Tries to load the cache stored by Duat for the given type
43///
44/// The cache must have been previously stored by [`store_cache`]. If
45/// it does not exist, or the file can't be correctly interpreted,
46/// returns [`None`]
47pub(super) fn load_cache<C: Decode<()> + 'static>(path: impl Into<PathBuf>) -> Option<C> {
48    fn contents(path: PathBuf, type_id: TypeId, type_name: String) -> Option<File> {
49        if type_id == TypeId::of::<()>() {
50            return None;
51        }
52
53        let file_name = path.file_name()?.to_str()?;
54        let mut src = dirs_next::cache_dir()?;
55
56        let encoded: String = {
57            let base64 = base64::prelude::BASE64_URL_SAFE.encode(path.to_str().unwrap());
58            base64.chars().step_by(5).collect()
59        };
60
61        src.push("duat/structs");
62        src.push(format!("{encoded}:{file_name}"));
63        src.push(type_name);
64
65        std::fs::OpenOptions::new().read(true).open(src).ok()
66    }
67
68    let type_name = format!("{}::{}", src_crate::<C>(), duat_name::<C>());
69    let mut file = contents(path.into(), TypeId::of::<C>(), type_name)?;
70
71    let config = Configuration::<LittleEndian, Fixint, NoLimit>::default();
72    bincode::decode_from_std_read(&mut file, config).ok()
73}
74
75/// Stores the cache for the given type for that file
76///
77/// The cache will be stored under
78/// `$cache/duat/{base64_path}:{file_name}/{crate}::{type}`.
79/// The cache will then later be loaded by [`load_cache`].
80pub(super) fn store_cache<C: Encode + 'static>(path: impl Into<PathBuf>, cache: C) {
81    fn cache_file(path: PathBuf, type_id: TypeId, type_name: String) -> Option<File> {
82        if type_id == TypeId::of::<()>() {
83            return None;
84        }
85
86        let file_name = path.file_name().unwrap().to_str().unwrap();
87        let mut src = dirs_next::cache_dir()?;
88
89        let encoded: String = {
90            let base64 = base64::prelude::BASE64_URL_SAFE.encode(path.to_str().unwrap());
91            base64.chars().step_by(5).collect()
92        };
93
94        src.push("duat/structs");
95        src.push(format!("{encoded}:{file_name}"));
96
97        if !src.exists() {
98            std::fs::create_dir_all(src.clone()).unwrap();
99        }
100
101        src.push(type_name);
102
103        std::fs::OpenOptions::new()
104            .create(true)
105            .write(true)
106            .truncate(true)
107            .open(src)
108            .ok()
109    }
110
111    let type_name = format!("{}::{}", src_crate::<C>(), duat_name::<C>());
112    let Some(mut cache_file) = cache_file(path.into(), TypeId::of::<C>(), type_name) else {
113        return;
114    };
115
116    let config = Configuration::<LittleEndian, Fixint, NoLimit>::default();
117    encode_into_std_write(cache, &mut cache_file, config).unwrap();
118}
119
120/// Deletes the cache for all types for `path`
121///
122/// This is done if the file no longer exists, in order to prevent
123/// incorrect storage.
124pub(super) fn delete_cache(path: impl Into<PathBuf>) {
125    fn delete_cache_inner(path: PathBuf) {
126        let file_name = path.file_name().unwrap().to_str().unwrap();
127        let Some(mut src) = dirs_next::cache_dir() else {
128            return;
129        };
130
131        let encoded: String = {
132            let base64 = base64::prelude::BASE64_URL_SAFE.encode(path.to_str().unwrap());
133            base64.chars().step_by(5).collect()
134        };
135
136        src.push("duat/structs");
137        src.push(format!("{encoded}:{file_name}"));
138
139        if src.exists() {
140            std::fs::remove_dir_all(src).unwrap();
141        }
142    }
143
144    delete_cache_inner(path.into());
145}
146
147pub(super) fn delete_cache_for<C: 'static>(path: impl Into<PathBuf>) {
148    fn delete_cache_for_inner(path: PathBuf, type_name: String) {
149        let file_name = path.file_name().unwrap().to_str().unwrap();
150        let Some(mut src) = dirs_next::cache_dir() else {
151            return;
152        };
153
154        let encoded: String = {
155            let base64 = base64::prelude::BASE64_URL_SAFE.encode(path.to_str().unwrap());
156            base64.chars().step_by(5).collect()
157        };
158
159        src.push("duat/structs");
160        src.push(format!("{encoded}:{file_name}"));
161        src.push(type_name);
162
163        if src.exists() {
164            std::fs::remove_file(src).unwrap();
165        }
166    }
167
168    let type_name = format!("{}::{}", src_crate::<C>(), duat_name::<C>());
169    delete_cache_for_inner(path.into(), type_name);
170}