use std::collections::HashMap;
use std::fs;
use std::io;
use std::path::PathBuf;
use std::time::SystemTime;
use crate::file_path::WorkspaceFilePath;
#[derive(Debug, Clone)]
pub struct CacheEntry {
pub hash: u64,
pub mtime: SystemTime,
}
impl CacheEntry {
pub fn new(hash: u64, mtime: SystemTime) -> Self {
Self { hash, mtime }
}
pub fn is_fresh(&self, current_mtime: SystemTime) -> bool {
self.mtime == current_mtime
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Freshness {
Fresh,
Stale,
NotCached,
Missing,
}
#[derive(Debug, Clone, Default)]
pub struct ContentCache {
entries: HashMap<PathBuf, CacheEntry>,
}
impl ContentCache {
pub fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
entries: HashMap::with_capacity(capacity),
}
}
pub fn register(&mut self, path: &WorkspaceFilePath, hash: u64, mtime: SystemTime) {
let key = path.as_relative().to_path_buf();
self.entries.insert(key, CacheEntry::new(hash, mtime));
}
pub fn register_with_current_mtime(
&mut self,
path: &WorkspaceFilePath,
hash: u64,
) -> io::Result<()> {
let absolute = path.to_absolute();
let metadata = fs::metadata(&absolute)?;
let mtime = metadata.modified()?;
self.register(path, hash, mtime);
Ok(())
}
pub fn get(&self, path: &WorkspaceFilePath) -> Option<&CacheEntry> {
self.entries.get(path.as_relative())
}
pub fn contains(&self, path: &WorkspaceFilePath) -> bool {
self.entries.contains_key(path.as_relative())
}
pub fn remove(&mut self, path: &WorkspaceFilePath) -> Option<CacheEntry> {
self.entries.remove(path.as_relative())
}
pub fn check_freshness(&self, path: &WorkspaceFilePath) -> Freshness {
let entry = match self.entries.get(path.as_relative()) {
Some(e) => e,
None => return Freshness::NotCached,
};
let absolute = path.to_absolute();
let metadata = match fs::metadata(&absolute) {
Ok(m) => m,
Err(_) => return Freshness::Missing,
};
let current_mtime = match metadata.modified() {
Ok(t) => t,
Err(_) => return Freshness::Stale, };
if entry.is_fresh(current_mtime) {
Freshness::Fresh
} else {
Freshness::Stale
}
}
pub fn get_stale_files<'a>(
&self,
paths: &'a [WorkspaceFilePath],
) -> Vec<&'a WorkspaceFilePath> {
paths
.iter()
.filter(|p| self.check_freshness(p) == Freshness::Stale)
.collect()
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&PathBuf, &CacheEntry)> {
self.entries.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_path(relative: &str) -> WorkspaceFilePath {
WorkspaceFilePath::new_for_test(relative, "/workspace", "test_crate")
}
#[test]
fn test_register_and_get() {
let mut cache = ContentCache::new();
let path = make_path("src/lib.rs");
let mtime = SystemTime::now();
cache.register(&path, 12345, mtime);
let entry = cache.get(&path).unwrap();
assert_eq!(entry.hash, 12345);
assert_eq!(entry.mtime, mtime);
}
#[test]
fn test_contains() {
let mut cache = ContentCache::new();
let path = make_path("src/lib.rs");
assert!(!cache.contains(&path));
cache.register(&path, 12345, SystemTime::now());
assert!(cache.contains(&path));
}
#[test]
fn test_remove() {
let mut cache = ContentCache::new();
let path = make_path("src/lib.rs");
cache.register(&path, 12345, SystemTime::now());
assert!(cache.contains(&path));
let removed = cache.remove(&path);
assert!(removed.is_some());
assert!(!cache.contains(&path));
}
#[test]
fn test_freshness_not_cached() {
let cache = ContentCache::new();
let path = make_path("src/lib.rs");
assert_eq!(cache.check_freshness(&path), Freshness::NotCached);
}
#[test]
fn test_entry_is_fresh() {
let mtime = SystemTime::now();
let entry = CacheEntry::new(12345, mtime);
assert!(entry.is_fresh(mtime));
let different_mtime = mtime + std::time::Duration::from_secs(1);
assert!(!entry.is_fresh(different_mtime));
}
}