use std::cell::Cell;
use std::cell::OnceCell;
use std::cell::RefCell;
use std::fs::File;
use std::marker::PhantomData;
use std::path::Path;
use std::path::PathBuf;
use crate::insert_map::InsertMap;
use crate::util::stat;
use crate::ErrorExt as _;
use crate::Result;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(target_env = "musl", allow(deprecated))]
struct FileMeta {
dev: libc::dev_t,
inode: libc::ino_t,
size: libc::off_t,
mtime_sec: libc::time_t,
#[cfg(linux)]
mtime_nsec: i64,
}
impl From<&libc::stat> for FileMeta {
fn from(other: &libc::stat) -> Self {
#[allow(trivial_numeric_casts)]
Self {
dev: other.st_dev as _,
inode: other.st_ino as _,
size: other.st_size as _,
mtime_sec: other.st_mtime,
#[cfg(linux)]
mtime_nsec: other.st_mtime_nsec as _,
}
}
}
#[derive(Debug)]
struct Entry<T> {
references: Cell<usize>,
file: File,
value: OnceCell<T>,
}
impl<T> Entry<T> {
fn new(file: File) -> Self {
Self {
references: Cell::new(0),
file,
value: OnceCell::new(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum PinState {
Pinned,
Unpinned,
}
#[derive(Debug)]
struct PathEntry {
current: Cell<Option<(PinState, FileMeta)>>,
previous: RefCell<Vec<FileMeta>>,
}
impl PathEntry {
fn metas(&self) -> Vec<FileMeta> {
let mut metas = self.previous.borrow().clone();
if let Some((_pin_state, meta)) = self.current.get() {
if !metas.contains(&meta) {
let () = metas.push(meta);
}
}
metas
}
}
impl Default for PathEntry {
fn default() -> Self {
Self {
current: Cell::new(None),
previous: RefCell::new(Vec::new()),
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct Builder<T> {
auto_reload: bool,
_phantom: PhantomData<T>,
}
impl<T> Builder<T> {
pub(crate) fn enable_auto_reload(mut self, enable: bool) -> Self {
self.auto_reload = enable;
self
}
pub(crate) fn build(self) -> FileCache<T> {
let Self {
auto_reload,
_phantom: _,
} = self;
FileCache {
cache: InsertMap::new(),
entries: InsertMap::new(),
auto_reload,
}
}
}
impl<T> Default for Builder<T> {
fn default() -> Self {
Self {
auto_reload: true,
_phantom: PhantomData,
}
}
}
#[derive(Debug)]
pub(crate) struct FileCache<T> {
cache: InsertMap<PathBuf, PathEntry>,
entries: InsertMap<FileMeta, Entry<T>>,
auto_reload: bool,
}
impl<T> FileCache<T> {
pub(crate) fn builder() -> Builder<T> {
Builder::<T>::default()
}
fn get_or_insert(&self, path: &Path, path_entry: &PathEntry) -> Result<&Entry<T>> {
let stat = stat(path).with_context(|| format!("failed to stat `{}`", path.display()))?;
let meta = (PinState::Unpinned, FileMeta::from(&stat));
let entry = self.entries.get_or_try_insert(meta.1, || {
let file = File::open(path)
.with_context(|| format!("failed to open file `{}`", path.display()))?;
let entry = Entry::new(file);
Ok(entry)
})?;
let current = path_entry.current.get();
if current.is_none_or(|(_pin_state, current_meta)| current_meta != meta.1) {
let mut previous = path_entry.previous.borrow_mut();
if let Some((_pin_state, previous_meta)) = current {
if !previous.contains(&previous_meta) {
let () = previous.push(previous_meta);
}
}
if !previous.contains(&meta.1) {
let () = entry.references.set(entry.references.get() + 1);
}
}
let () = path_entry.current.set(Some(meta));
Ok(entry)
}
pub(crate) fn entry(&self, path: &Path) -> Result<(&File, &OnceCell<T>)> {
if let Some(path_entry) = self.cache.get(path) {
if let Some((pin_state, current_meta)) = path_entry.current.get() {
if !self.auto_reload || pin_state == PinState::Pinned {
let current = self.entries.get(¤t_meta).unwrap();
return Ok((¤t.file, ¤t.value))
}
}
let entry = self.get_or_insert(path, path_entry)?;
return Ok((&entry.file, &entry.value))
}
let path_entry = PathEntry::default();
let entry = self.get_or_insert(path, &path_entry)?;
let _path_entry = self.cache.get_or_insert(path.to_path_buf(), || path_entry);
Ok((&entry.file, &entry.value))
}
fn set_pin_state(&self, path: &Path, pin_state: PinState) -> Option<()> {
let path_entry = self.cache.get(path)?;
let current = path_entry.current.get()?;
let () = path_entry.current.set(Some((pin_state, current.1)));
Some(())
}
pub(crate) fn pin(&self, path: &Path) -> Option<()> {
self.set_pin_state(path, PinState::Pinned)
}
pub(crate) fn unpin(&self, path: &Path) -> Option<()> {
self.set_pin_state(path, PinState::Unpinned)
}
pub(crate) fn evict(&mut self, path: &Path) -> bool {
let Some(path_entry) = self.cache.remove(path) else {
return false
};
let mut evicted = false;
for meta in path_entry.metas() {
let entry = self.entries.get(&meta).unwrap();
let references = entry.references.get();
debug_assert_ne!(references, 0);
if references > 1 {
let () = entry.references.set(references - 1);
} else {
let _entry = self.entries.remove(&meta);
evicted = true;
}
}
evicted
}
#[cfg(test)]
pub(crate) fn entry_count(&self) -> usize {
self.entries.len()
}
#[cfg(test)]
pub(crate) fn path_count(&self) -> usize {
self.cache.len()
}
}
impl<T> Default for FileCache<T> {
fn default() -> Self {
Self::builder().build()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(linux)]
use std::fs::remove_file;
use std::fs::write;
#[cfg(feature = "nightly")]
use std::hint::black_box;
use std::io::Read as _;
#[cfg(linux)]
use std::os::fd::AsRawFd as _;
#[cfg(linux)]
use std::os::unix::fs::symlink;
use std::thread::sleep;
use std::time::Duration;
use tempfile::tempdir;
use tempfile::tempfile;
use tempfile::NamedTempFile;
#[cfg(feature = "nightly")]
use test::Bencher;
use crate::ErrorKind;
#[test]
fn debug_repr() {
let cache = FileCache::<()>::default();
assert_ne!(format!("{cache:?}"), "");
let tmpfile = tempfile().unwrap();
let entry = Entry::<usize>::new(tmpfile);
assert_ne!(format!("{entry:?}"), "");
}
#[test]
fn lookup() {
let cache = FileCache::<usize>::default();
let tmpfile = NamedTempFile::new().unwrap();
{
let (_file, cell) = cache.entry(tmpfile.path()).unwrap();
assert_eq!(cell.get(), None);
let () = cell.set(42).unwrap();
}
{
let (_file, cell) = cache.entry(tmpfile.path()).unwrap();
assert_eq!(cell.get(), Some(&42));
}
}
#[test]
fn failed_lookup_leaves_no_entry() {
let cache = FileCache::<usize>::default();
let err = cache.entry(Path::new("/does/not/exist")).unwrap_err();
assert_eq!(err.kind(), ErrorKind::NotFound);
assert_eq!(cache.path_count(), 0);
assert_eq!(cache.entry_count(), 0);
let tmpfile = NamedTempFile::new().unwrap();
{
let (_file, cell) = cache.entry(tmpfile.path()).unwrap();
assert_eq!(cell.get(), None);
}
assert_eq!(cache.path_count(), 1);
assert_eq!(cache.entry_count(), 1);
}
#[test]
fn evict_entry() {
let mut cache = FileCache::<usize>::default();
let tmpfile = NamedTempFile::new().unwrap();
assert!(!cache.evict(Path::new("/does/not/exist")));
{
let (_file, cell) = cache.entry(tmpfile.path()).unwrap();
let () = cell.set(42).unwrap();
}
assert_eq!(cache.entry_count(), 1);
assert!(cache.evict(tmpfile.path()));
assert_eq!(cache.entry_count(), 0);
assert!(!cache.evict(tmpfile.path()));
{
let (_file, cell) = cache.entry(tmpfile.path()).unwrap();
assert_eq!(cell.get(), None);
}
assert_eq!(cache.entry_count(), 1);
}
#[test]
fn evict_stale_entries() {
let mut cache = FileCache::<usize>::default();
let tmpfile = NamedTempFile::new().unwrap();
{
let (_file, cell) = cache.entry(tmpfile.path()).unwrap();
let () = cell.set(42).unwrap();
}
let () = sleep(Duration::from_millis(10));
let path = tmpfile.path().to_path_buf();
let () = drop(tmpfile);
let () = write(&path, b"foobar").unwrap();
{
let (_file, cell) = cache.entry(&path).unwrap();
assert_eq!(cell.get(), None);
let () = cell.set(43).unwrap();
}
assert_eq!(cache.entry_count(), 2);
assert!(cache.evict(&path));
assert_eq!(cache.entry_count(), 0);
}
#[cfg(linux)]
#[test]
fn evict_shared_entry() {
let tmpfile = NamedTempFile::new().unwrap();
let tmpdir = tempdir().unwrap();
let link = tmpdir.path().join("symlink");
let () = symlink(tmpfile.path(), &link).unwrap();
let mut cache = FileCache::<usize>::default();
{
let (_file, cell) = cache.entry(tmpfile.path()).unwrap();
let () = cell.set(42).unwrap();
}
{
let (_file, cell) = cache.entry(&link).unwrap();
assert_eq!(cell.get(), Some(&42));
}
assert_eq!(cache.entry_count(), 1);
assert!(!cache.evict(tmpfile.path()));
assert_eq!(cache.entry_count(), 1);
{
let (_file, cell) = cache.entry(&link).unwrap();
assert_eq!(cell.get(), Some(&42));
}
assert!(cache.evict(&link));
assert_eq!(cache.entry_count(), 0);
}
#[cfg(linux)]
#[test]
fn evict_reinstated_entry() {
let tmpfile1 = NamedTempFile::new().unwrap();
let tmpfile2 = NamedTempFile::new().unwrap();
let tmpdir = tempdir().unwrap();
let link = tmpdir.path().join("symlink");
let () = symlink(tmpfile1.path(), &link).unwrap();
let mut cache = FileCache::<usize>::default();
{
let (_file, cell) = cache.entry(&link).unwrap();
let () = cell.set(41).unwrap();
}
let () = remove_file(&link).unwrap();
let () = symlink(tmpfile2.path(), &link).unwrap();
{
let (_file, cell) = cache.entry(&link).unwrap();
let () = cell.set(42).unwrap();
}
let () = remove_file(&link).unwrap();
let () = symlink(tmpfile1.path(), &link).unwrap();
{
let (_file, cell) = cache.entry(&link).unwrap();
assert_eq!(cell.get(), Some(&41));
}
assert_eq!(cache.entry_count(), 2);
assert!(cache.evict(&link));
assert_eq!(cache.entry_count(), 0);
}
#[cfg(linux)]
#[test]
fn symlink_entries() {
let tmpfile = NamedTempFile::new().unwrap();
let tmpdir = tempdir().unwrap();
let link = tmpdir.path().join("symlink");
let () = symlink(tmpfile.path(), &link).unwrap();
let cache = FileCache::<usize>::default();
let (file1, cell) = cache.entry(tmpfile.path()).unwrap();
let () = cell.set(42).unwrap();
let (file2, cell) = cache.entry(&link).unwrap();
assert_eq!(cell.get(), Some(&42));
assert_eq!(file2.as_raw_fd(), file1.as_raw_fd());
let () = remove_file(&link).unwrap();
let () = write(&link, b"test").unwrap();
let (file3, cell) = cache.entry(&link).unwrap();
assert_eq!(cell.get(), None);
assert_ne!(file3.as_raw_fd(), file1.as_raw_fd());
let (file4, cell) = cache.entry(tmpfile.path()).unwrap();
assert_eq!(cell.get(), Some(&42));
assert_eq!(file4.as_raw_fd(), file1.as_raw_fd());
}
#[cfg(linux)]
#[test]
fn multi_symlink_reload() {
let tmpfile = NamedTempFile::new().unwrap();
let tmpdir = tempdir().unwrap();
let link2 = tmpdir.path().join("symlink2");
let () = symlink(tmpfile.path(), &link2).unwrap();
let link1 = tmpdir.path().join("symlink1");
let () = symlink(&link2, &link1).unwrap();
let cache = FileCache::<usize>::default();
let (file1, cell) = cache.entry(&link1).unwrap();
let () = cell.set(41).unwrap();
let tmpfile2 = NamedTempFile::new().unwrap();
let () = remove_file(&link2).unwrap();
let () = symlink(tmpfile2.path(), &link2).unwrap();
let (file2, cell) = cache.entry(&link1).unwrap();
assert_eq!(cell.get(), None);
assert_ne!(file2.as_raw_fd(), file1.as_raw_fd());
}
#[cfg(linux)]
#[test]
fn symlink_pinning() {
let tmpfile = NamedTempFile::new().unwrap();
let tmpdir = tempdir().unwrap();
let link = tmpdir.path().join("symlink");
let () = symlink(tmpfile.path(), &link).unwrap();
let cache = FileCache::<usize>::default();
let (file1, cell) = cache.entry(&link).unwrap();
let () = cell.set(42).unwrap();
let () = cache.pin(&link).unwrap();
let tmpfile2 = NamedTempFile::new().unwrap();
let () = remove_file(&link).unwrap();
let () = symlink(tmpfile2.path(), &link).unwrap();
let (file2, cell) = cache.entry(&link).unwrap();
assert_eq!(cell.get(), Some(&42));
assert_eq!(file2.as_raw_fd(), file1.as_raw_fd());
let () = write(tmpfile.path(), b"new-content").unwrap();
let (file3, cell) = cache.entry(&link).unwrap();
assert_eq!(cell.get(), Some(&42));
assert_eq!(file3.as_raw_fd(), file1.as_raw_fd());
}
#[cfg(linux)]
#[test]
fn symlink_dead_target() {
let tmpfile = NamedTempFile::new().unwrap();
let tmpdir = tempdir().unwrap();
let link = tmpdir.path().join("symlink");
let () = symlink(tmpfile.path(), &link).unwrap();
let () = tmpfile.close().unwrap();
let cache = FileCache::<usize>::default();
let err = cache.entry(&link).unwrap_err();
assert_eq!(err.kind(), ErrorKind::NotFound);
}
#[test]
fn outdated() {
fn test(auto_reload: bool, pin: bool) {
let cache = FileCache::<usize>::builder()
.enable_auto_reload(auto_reload)
.build();
let tmpfile = NamedTempFile::new().unwrap();
let modified = {
let (file, cell) = cache.entry(tmpfile.path()).unwrap();
if pin {
let () = cache.pin(tmpfile.path()).unwrap();
}
assert_eq!(cell.get(), None);
let () = cell.set(42).unwrap();
file.metadata().unwrap().modified().unwrap()
};
let () = sleep(Duration::from_millis(10));
let path = tmpfile.path().to_path_buf();
let () = drop(tmpfile);
{
let () = write(&path, b"foobar").unwrap();
}
{
let (mut file, entry) = cache.entry(&path).unwrap();
if auto_reload && !pin {
let new_modified = file.metadata().unwrap().modified().unwrap();
assert_eq!(entry.get(), None);
assert!(new_modified > modified, "{new_modified:?} | {modified:?}");
let mut content = Vec::new();
let _count = file.read_to_end(&mut content);
assert_eq!(content, b"foobar");
} else {
assert_eq!(entry.get(), Some(&42));
assert_eq!(file.metadata().unwrap().modified().unwrap(), modified);
}
}
}
for auto_reload in [false, true] {
for pin in [false, true] {
let () = test(auto_reload, pin);
}
}
}
#[test]
fn removed() {
#[track_caller]
fn test(pin: bool) {
let tmpfile = NamedTempFile::new().unwrap();
let cache = FileCache::<usize>::builder().build();
let (_file, cell) = cache.entry(tmpfile.path()).unwrap();
if pin {
let () = cache.pin(tmpfile.path()).unwrap();
}
let () = cell.set(42).unwrap();
let path = tmpfile.path().to_path_buf();
let () = drop(tmpfile);
let result = cache.entry(&path);
if pin {
let (_file, cell) = result.unwrap();
assert_eq!(cell.get(), Some(&42));
} else {
let err = result.unwrap_err();
assert_eq!(err.kind(), ErrorKind::NotFound);
}
}
for pin in [false, true] {
let () = test(pin);
}
}
#[cfg(feature = "nightly")]
fn bench_entry_retrieval_no_change_impl(b: &mut Bencher, pin: bool) {
let tmpfile = NamedTempFile::new().unwrap();
let path = tmpfile.path();
let cache = FileCache::<usize>::builder().build();
let (_file, cell) = cache.entry(path).unwrap();
if pin {
let () = cache.pin(path).unwrap();
}
let () = cell.set(42).unwrap();
let () = b.iter(|| {
let entry = cache.entry(&path).unwrap();
let _entry = black_box(entry);
});
}
#[cfg(feature = "nightly")]
#[bench]
fn bench_entry_retrieval_no_change(b: &mut Bencher) {
let pin = false;
bench_entry_retrieval_no_change_impl(b, pin)
}
#[cfg(feature = "nightly")]
#[bench]
fn bench_entry_retrieval_no_change_pinned(b: &mut Bencher) {
let pin = true;
bench_entry_retrieval_no_change_impl(b, pin)
}
}