mod segment;
use std::path::PathBuf;
use std::sync::{Mutex, MutexGuard, OnceLock};
pub use concinnity_core::blob::CacheEntryKind;
pub use segment::Segment;
pub const CACHE_BUDGET_BYTES: u64 = 64 * 1024 * 1024;
pub fn load(kind: CacheEntryKind, key: &str) -> Option<Vec<u8>> {
with(|segment| segment.get(kind, key).map(<[u8]>::to_vec)).flatten()
}
pub fn load_bundled(kind: CacheEntryKind, key: &str) -> Option<Vec<u8>> {
bundled(|segment| segment.get(kind, key).map(<[u8]>::to_vec)).flatten()
}
pub fn store(kind: CacheEntryKind, key: &str, bytes: &[u8]) -> bool {
with(|segment| segment.put(kind, key, bytes)).unwrap_or(false)
}
pub fn delete(kind: CacheEntryKind, key: &str) {
with(|segment| segment.remove(kind, key));
}
pub fn verify_toolchain(id: &str) -> bool {
with(|segment| segment.adopt_toolchain(id)).unwrap_or(false)
}
pub fn flush() -> bool {
match lock().as_mut() {
Some(loaded) => loaded.segment.write_to(&loaded.path, CACHE_BUDGET_BYTES),
None => false,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CacheAnchor {
writable: PathBuf,
bundled: Option<PathBuf>,
}
impl CacheAnchor {
pub fn new<P: Into<PathBuf>>(writable: P) -> Self {
Self {
writable: writable.into(),
bundled: None,
}
}
#[must_use]
pub fn with_bundled<P: Into<PathBuf>>(mut self, bundled: P) -> Self {
self.bundled = Some(bundled.into());
self
}
}
fn anchored() -> &'static Mutex<Option<CacheAnchor>> {
static ANCHOR: OnceLock<Mutex<Option<CacheAnchor>>> = OnceLock::new();
ANCHOR.get_or_init(|| Mutex::new(None))
}
pub fn anchor(anchor: CacheAnchor) {
*anchored().lock().unwrap() = Some(anchor);
}
pub fn clear_anchor() {
*anchored().lock().unwrap() = None;
}
fn writable_path() -> Option<PathBuf> {
anchored()
.lock()
.unwrap()
.as_ref()
.map(|a| a.writable.clone())
}
struct Loaded {
path: PathBuf,
segment: Segment,
}
fn with<R>(f: impl FnOnce(&mut Segment) -> R) -> Option<R> {
let path = writable_path()?;
let mut held = lock();
if held.as_ref().is_some_and(|loaded| loaded.path != path)
&& let Some(mut previous) = held.take()
{
previous
.segment
.write_to(&previous.path, CACHE_BUDGET_BYTES);
}
let loaded = held.get_or_insert_with(|| Loaded {
segment: Segment::read_from(&path),
path,
});
Some(f(&mut loaded.segment))
}
fn lock() -> MutexGuard<'static, Option<Loaded>> {
static LOADED: Mutex<Option<Loaded>> = Mutex::new(None);
LOADED.lock().unwrap_or_else(|e| e.into_inner())
}
fn bundled<R>(f: impl FnOnce(&mut Segment) -> R) -> Option<R> {
let held = anchored().lock().unwrap().clone()?;
let path = shipped_path(held.bundled?, Some(held.writable))?;
static LOADED: Mutex<Option<Loaded>> = Mutex::new(None);
let mut held = LOADED.lock().unwrap_or_else(|e| e.into_inner());
if held.as_ref().is_some_and(|loaded| loaded.path != path) {
*held = None;
}
let loaded = held.get_or_insert_with(|| Loaded {
segment: Segment::read_from(&path),
path,
});
Some(f(&mut loaded.segment))
}
fn shipped_path(shipped: PathBuf, writable: Option<PathBuf>) -> Option<PathBuf> {
(writable.as_deref() != Some(shipped.as_path())).then_some(shipped)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn a_writable_bundle_has_no_second_tier() {
let one = PathBuf::from("/bundle/cache/0");
assert_eq!(shipped_path(one.clone(), Some(one.clone())), None);
}
#[test]
fn a_read_only_install_reads_the_shipped_segment() {
let shipped = PathBuf::from("/opt/app/cache/0");
let writable = PathBuf::from("/home/u/.local/share/app/cache/0");
assert_eq!(
shipped_path(shipped.clone(), Some(writable)),
Some(shipped.clone())
);
assert_eq!(shipped_path(shipped.clone(), None), Some(shipped));
}
#[test]
fn an_anchor_names_the_files_it_was_given() {
let plain = CacheAnchor::new("/run/segment");
assert_eq!(plain.writable, Path::new("/run/segment"));
assert_eq!(plain.bundled, None);
let tiered = CacheAnchor::new("/run/segment").with_bundled("/opt/shipped");
assert_eq!(tiered.writable, Path::new("/run/segment"));
assert_eq!(tiered.bundled.as_deref(), Some(Path::new("/opt/shipped")));
}
#[test]
fn a_tree_builds_the_anchor_its_layout_implies() {
let tree = super::super::paths::StateTree::at("/bundle");
let anchor = CacheAnchor::new(tree.runtime_cache_path())
.with_bundled(tree.bundled_runtime_cache_path());
assert_eq!(
shipped_path(anchor.bundled.clone().unwrap(), Some(anchor.writable)),
None,
"one file in both roles has no second tier"
);
}
}