use std::collections::HashMap;
use std::time::SystemTime;
#[derive(Debug, Clone)]
pub enum VfsEntry<F> {
File(F),
Directory {
children: Vec<String>,
meta: Option<F>,
},
}
impl<F> VfsEntry<F> {
pub fn is_file(&self) -> bool {
matches!(self, VfsEntry::File(_))
}
pub fn is_dir(&self) -> bool {
matches!(self, VfsEntry::Directory { .. })
}
pub fn as_file(&self) -> Option<&F> {
match self {
VfsEntry::File(f) => Some(f),
_ => None,
}
}
pub fn children(&self) -> Option<&[String]> {
match self {
VfsEntry::Directory { children, .. } => Some(children),
_ => None,
}
}
pub fn meta(&self) -> Option<&F> {
match self {
VfsEntry::File(f) => Some(f),
VfsEntry::Directory { meta, .. } => meta.as_ref(),
}
}
}
pub trait Metadata {
fn len(&self) -> u64;
fn created(&self) -> Option<SystemTime> {
None
}
fn modified(&self) -> Option<SystemTime> {
None
}
fn accessed(&self) -> Option<SystemTime> {
None
}
}
pub fn system_time_from_unix(secs: i64) -> SystemTime {
if secs >= 0 {
std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs as u64)
} else {
std::time::UNIX_EPOCH - std::time::Duration::from_secs((-secs) as u64)
}
}
#[cfg(feature = "jiff")]
pub fn system_time_from_jiff(ts: &jiff::Timestamp) -> SystemTime {
let secs = ts.as_second();
let nanos = ts.subsec_nanosecond();
if secs >= 0 {
std::time::UNIX_EPOCH
+ std::time::Duration::from_secs(secs as u64)
+ std::time::Duration::from_nanos(nanos as u64)
} else {
let abs_secs = (-secs) as u64;
let base = std::time::UNIX_EPOCH - std::time::Duration::from_secs(abs_secs);
if nanos > 0 {
base + std::time::Duration::from_nanos(nanos as u64)
} else {
base
}
}
}
#[derive(Debug, Clone)]
pub struct VfsTree<F> {
entries: HashMap<String, VfsEntry<F>>,
}
impl<F> VfsTree<F> {
pub fn builder() -> VfsTreeBuilder<F> {
VfsTreeBuilder::new()
}
pub fn lookup(&self, path: &str) -> Option<&VfsEntry<F>> {
let key = normalize_path(path);
self.entries.get(key)
}
pub fn exists(&self, path: &str) -> bool {
self.lookup(path).is_some()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &VfsEntry<F>)> {
self.entries.iter().map(|(k, v)| (k.as_str(), v))
}
pub fn read_dir(&self, path: &str) -> Option<&[String]> {
self.lookup(path).and_then(|e| e.children())
}
}
fn empty_dir<F>() -> VfsEntry<F> {
VfsEntry::Directory {
children: Vec::new(),
meta: None,
}
}
#[derive(Debug)]
pub struct VfsTreeBuilder<F> {
entries: HashMap<String, VfsEntry<F>>,
}
impl<F> VfsTreeBuilder<F> {
pub fn new() -> Self {
let mut entries = HashMap::new();
entries.insert(String::new(), empty_dir());
Self { entries }
}
pub fn insert(mut self, path: impl Into<String>, file_meta: F) -> Self {
let path = path.into();
let path = path.strip_prefix('/').unwrap_or(&path).to_string();
let parts: Vec<&str> = path.split('/').collect();
for depth in 0..parts.len() {
let parent_path = if depth == 0 {
String::new()
} else {
parts[..depth].join("/")
};
let child_name = parts[depth];
let parent = self.entries.entry(parent_path).or_insert_with(empty_dir);
if let VfsEntry::Directory { children, .. } = parent
&& !children.contains(&child_name.to_string())
{
children.push(child_name.to_string());
}
if depth < parts.len() - 1 {
let dir_path = parts[..=depth].join("/");
self.entries.entry(dir_path).or_insert_with(empty_dir);
}
}
self.entries.insert(path, VfsEntry::File(file_meta));
self
}
pub fn insert_dir(mut self, path: impl Into<String>, meta: Option<F>) -> Self {
let path = path.into();
let path = path.strip_prefix('/').unwrap_or(&path).to_string();
if path.is_empty() {
if let Some(m) = meta
&& let Some(VfsEntry::Directory { meta: slot, .. }) = self.entries.get_mut("")
{
*slot = Some(m);
}
return self;
}
let parts: Vec<&str> = path.split('/').collect();
for depth in 0..parts.len() {
let parent_path = if depth == 0 {
String::new()
} else {
parts[..depth].join("/")
};
let child_name = parts[depth];
let parent = self.entries.entry(parent_path).or_insert_with(empty_dir);
if let VfsEntry::Directory { children, .. } = parent
&& !children.contains(&child_name.to_string())
{
children.push(child_name.to_string());
}
let dir_path = parts[..=depth].join("/");
if depth < parts.len() - 1 {
self.entries.entry(dir_path).or_insert_with(empty_dir);
} else {
match self.entries.get_mut(&dir_path) {
Some(VfsEntry::Directory { meta: slot, .. }) => {
if meta.is_some() {
*slot = meta;
}
return self;
}
None => {
self.entries.insert(
dir_path,
VfsEntry::Directory {
children: Vec::new(),
meta,
},
);
return self;
}
_ => {}
}
}
}
self
}
pub fn build(self) -> VfsTree<F> {
VfsTree {
entries: self.entries,
}
}
}
impl<F> Default for VfsTreeBuilder<F> {
fn default() -> Self {
Self::new()
}
}
fn normalize_path(path: &str) -> &str {
path.strip_prefix('/').unwrap_or(path)
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone)]
struct TestMeta {
size: u64,
}
impl Metadata for TestMeta {
fn len(&self) -> u64 {
self.size
}
}
#[test]
fn build_tree_creates_ancestors() {
let tree = VfsTree::builder()
.insert("a/b/c.txt", TestMeta { size: 100 })
.insert("a/b/d.txt", TestMeta { size: 200 })
.insert("a/e.txt", TestMeta { size: 50 })
.build();
assert!(tree.lookup("").unwrap().is_dir());
assert!(tree.lookup("/").unwrap().is_dir());
assert!(tree.lookup("a").unwrap().is_dir());
assert!(tree.lookup("a/b").unwrap().is_dir());
assert!(tree.lookup("a/b/c.txt").unwrap().is_file());
assert!(tree.lookup("a/b/d.txt").unwrap().is_file());
assert!(tree.lookup("a/e.txt").unwrap().is_file());
assert_eq!(
tree.lookup("a/b/c.txt").unwrap().as_file().unwrap().size,
100
);
let root_children = tree.read_dir("").unwrap();
assert_eq!(root_children, &["a"]);
let a_children = tree.read_dir("a").unwrap();
assert!(a_children.contains(&"b".to_string()));
assert!(a_children.contains(&"e.txt".to_string()));
let ab_children = tree.read_dir("a/b").unwrap();
assert!(ab_children.contains(&"c.txt".to_string()));
assert!(ab_children.contains(&"d.txt".to_string()));
}
#[test]
fn exists_and_missing() {
let tree = VfsTree::builder()
.insert("foo/bar.txt", TestMeta { size: 10 })
.build();
assert!(tree.exists("foo/bar.txt"));
assert!(tree.exists("foo"));
assert!(tree.exists(""));
assert!(!tree.exists("missing"));
assert!(!tree.exists("foo/missing.txt"));
}
#[test]
fn leading_slash_normalization() {
let tree = VfsTree::builder()
.insert("/hello/world.txt", TestMeta { size: 42 })
.build();
assert!(tree.exists("hello/world.txt"));
assert!(tree.exists("/hello/world.txt"));
}
#[test]
fn explicit_empty_dir() {
let tree: VfsTree<TestMeta> = VfsTree::builder().insert_dir("empty/dir", None).build();
assert!(tree.exists("empty"));
assert!(tree.exists("empty/dir"));
assert!(tree.lookup("empty/dir").unwrap().is_dir());
assert_eq!(tree.read_dir("empty/dir").unwrap().len(), 0);
}
#[test]
fn dir_with_metadata() {
let tree = VfsTree::builder()
.insert("dir/file.txt", TestMeta { size: 10 })
.insert_dir("dir", Some(TestMeta { size: 0 }))
.build();
assert!(tree.lookup("dir").unwrap().meta().is_some());
assert_eq!(tree.lookup("dir").unwrap().meta().unwrap().size, 0);
assert!(tree.lookup("").unwrap().meta().is_none());
}
#[test]
fn system_time_from_unix_positive() {
let st = system_time_from_unix(1_700_000_000);
let dur = st.duration_since(std::time::UNIX_EPOCH).unwrap();
assert_eq!(dur.as_secs(), 1_700_000_000);
}
#[test]
fn system_time_from_unix_zero() {
let st = system_time_from_unix(0);
assert_eq!(st, std::time::UNIX_EPOCH);
}
#[test]
fn system_time_from_unix_negative() {
let st = system_time_from_unix(-100);
let dur = std::time::UNIX_EPOCH.duration_since(st).unwrap();
assert_eq!(dur.as_secs(), 100);
}
#[test]
fn metadata_defaults_to_none() {
struct SizeOnly;
impl Metadata for SizeOnly {
fn len(&self) -> u64 {
42
}
}
let m = SizeOnly;
assert_eq!(m.len(), 42);
assert!(m.created().is_none());
assert!(m.modified().is_none());
assert!(m.accessed().is_none());
}
}
#[cfg(all(test, feature = "jiff"))]
mod jiff_tests {
use super::*;
#[test]
fn jiff_timestamp_to_system_time() {
let ts = jiff::Timestamp::from_second(1_700_000_000).unwrap();
let st = system_time_from_jiff(&ts);
let dur = st.duration_since(std::time::UNIX_EPOCH).unwrap();
assert_eq!(dur.as_secs(), 1_700_000_000);
assert_eq!(dur.subsec_nanos(), 0);
}
#[test]
fn jiff_timestamp_with_nanos() {
let ts = jiff::Timestamp::new(1_700_000_000, 500_000_000).unwrap();
let st = system_time_from_jiff(&ts);
let dur = st.duration_since(std::time::UNIX_EPOCH).unwrap();
assert_eq!(dur.as_secs(), 1_700_000_000);
assert_eq!(dur.subsec_nanos(), 500_000_000);
}
#[test]
fn jiff_epoch_zero() {
let ts = jiff::Timestamp::UNIX_EPOCH;
let st = system_time_from_jiff(&ts);
assert_eq!(st, std::time::UNIX_EPOCH);
}
}
#[cfg(all(test, feature = "vfs"))]
mod vfs_tests {
use super::*;
use std::io::Cursor;
use vfs::FileSystem;
use crate::vfs_impl::ReadOnlyVfs;
#[derive(Debug, Clone)]
struct FileMeta {
data: Vec<u8>,
}
impl Metadata for FileMeta {
fn len(&self) -> u64 {
self.data.len() as u64
}
}
#[derive(Debug)]
struct InMemoryOpener;
impl crate::vfs_impl::FileOpener<FileMeta> for InMemoryOpener {
fn open(&self, meta: &FileMeta) -> vfs::VfsResult<Box<dyn vfs::SeekAndRead + Send>> {
Ok(Box::new(Cursor::new(meta.data.clone())))
}
}
#[test]
fn read_only_vfs() {
use std::io::Read;
let tree = VfsTree::builder()
.insert(
"docs/readme.txt",
FileMeta {
data: b"hello world".to_vec(),
},
)
.insert(
"docs/license.txt",
FileMeta {
data: b"MIT".to_vec(),
},
)
.build();
let fs = ReadOnlyVfs::new(tree, InMemoryOpener);
let children: Vec<String> = fs.read_dir("").unwrap().collect();
assert_eq!(children, vec!["docs"]);
let doc_children: Vec<String> = fs.read_dir("docs").unwrap().collect();
assert!(doc_children.contains(&"readme.txt".to_string()));
assert!(doc_children.contains(&"license.txt".to_string()));
let mut file = fs.open_file("docs/readme.txt").unwrap();
let mut buf = String::new();
file.read_to_string(&mut buf).unwrap();
assert_eq!(buf, "hello world");
let meta = fs.metadata("docs/readme.txt").unwrap();
assert_eq!(meta.file_type, vfs::VfsFileType::File);
assert_eq!(meta.len, 11);
let dir_meta = fs.metadata("docs").unwrap();
assert_eq!(dir_meta.file_type, vfs::VfsFileType::Directory);
assert!(fs.exists("docs/readme.txt").unwrap());
assert!(!fs.exists("missing").unwrap());
assert!(fs.create_dir("foo").is_err());
assert!(fs.remove_file("docs/readme.txt").is_err());
}
#[test]
fn vfs_metadata_timestamps() {
#[derive(Debug, Clone)]
struct TimedMeta {
size: u64,
mtime: Option<SystemTime>,
}
impl Metadata for TimedMeta {
fn len(&self) -> u64 {
self.size
}
fn modified(&self) -> Option<SystemTime> {
self.mtime
}
}
let file_mtime = system_time_from_unix(1_700_000_000);
let dir_mtime = system_time_from_unix(1_600_000_000);
let tree = VfsTree::builder()
.insert(
"dir/file.txt",
TimedMeta {
size: 42,
mtime: Some(file_mtime),
},
)
.insert_dir(
"dir",
Some(TimedMeta {
size: 0,
mtime: Some(dir_mtime),
}),
)
.build();
let meta = tree.vfs_metadata("dir/file.txt").unwrap();
assert_eq!(meta.len, 42);
assert_eq!(meta.modified, Some(file_mtime));
assert_eq!(meta.created, None);
let dir_meta = tree.vfs_metadata("dir").unwrap();
assert_eq!(dir_meta.file_type, vfs::VfsFileType::Directory);
assert_eq!(dir_meta.modified, Some(dir_mtime));
assert_eq!(dir_meta.created, None);
let root_meta = tree.vfs_metadata("").unwrap();
assert_eq!(root_meta.modified, None);
}
}