aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-vfs/src
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-22 15:31:57 +0300
committerValentin Popov <valentin@popov.link>2026-06-22 15:31:57 +0300
commitaa1b809bd804655da1f5662c1553698883a92b52 (patch)
tree0567ad9b171a582835e84243fcb7df9476c1100e /crates/fparkan-vfs/src
parentf69c893a401730339ad72610c573e20282573045 (diff)
downloadfparkan-aa1b809bd804655da1f5662c1553698883a92b52.tar.xz
fparkan-aa1b809bd804655da1f5662c1553698883a92b52.zip
fix: strengthen resource fingerprints
Diffstat (limited to 'crates/fparkan-vfs/src')
-rw-r--r--crates/fparkan-vfs/src/lib.rs130
1 files changed, 90 insertions, 40 deletions
diff --git a/crates/fparkan-vfs/src/lib.rs b/crates/fparkan-vfs/src/lib.rs
index 2545ecf..a0cafa1 100644
--- a/crates/fparkan-vfs/src/lib.rs
+++ b/crates/fparkan-vfs/src/lib.rs
@@ -1,19 +1,21 @@
#![forbid(unsafe_code)]
//! Virtual filesystem ports for resource loading.
+use fparkan_binary::{sha256, Sha256Digest};
use fparkan_path::{ascii_lookup_key, join_under, NormalizedPath};
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
-use std::sync::Arc;
+use std::sync::{Arc, Mutex};
+use std::time::SystemTime;
/// VFS metadata.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VfsMetadata {
/// Byte length.
pub len: u64,
- /// Stable-enough source fingerprint for cache invalidation.
- pub fingerprint: u64,
+ /// SHA-256 content fingerprint for cache invalidation.
+ pub fingerprint: Sha256Digest,
}
/// VFS entry.
@@ -80,6 +82,7 @@ pub trait Vfs: Send + Sync {
#[derive(Clone, Debug)]
pub struct DirectoryVfs {
root: PathBuf,
+ fingerprint_cache: Arc<Mutex<BTreeMap<PathBuf, CachedHostFingerprint>>>,
}
impl DirectoryVfs {
@@ -88,6 +91,7 @@ impl DirectoryVfs {
pub fn new(root: impl AsRef<Path>) -> Self {
Self {
root: root.as_ref().to_path_buf(),
+ fingerprint_cache: Arc::default(),
}
}
@@ -95,12 +99,23 @@ impl DirectoryVfs {
join_under(&self.root, path).map_err(|_| VfsError::Path)?;
resolve_casefolded(&self.root, path.as_str())
}
+
+ fn metadata_from_host_file(&self, path: &Path) -> Result<VfsMetadata, VfsError> {
+ let metadata = fs::symlink_metadata(path).map_err(VfsError::Io)?;
+ metadata_from_host_file_with_cache(path, &metadata, &self.fingerprint_cache)
+ }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+struct CachedHostFingerprint {
+ len: u64,
+ modified: Option<SystemTime>,
+ fingerprint: Sha256Digest,
}
impl Vfs for DirectoryVfs {
fn metadata(&self, path: &NormalizedPath) -> Result<VfsMetadata, VfsError> {
- let meta = fs::symlink_metadata(self.host_path(path)?).map_err(VfsError::Io)?;
- Ok(metadata_from_fs(&meta))
+ self.metadata_from_host_file(&self.host_path(path)?)
}
fn read(&self, path: &NormalizedPath) -> Result<Arc<[u8]>, VfsError> {
@@ -123,11 +138,15 @@ impl Vfs for DirectoryVfs {
let metadata = fs::symlink_metadata(&base).map_err(VfsError::Io)?;
entries.push(VfsEntry {
path: prefix.clone(),
- metadata: metadata_from_fs(&metadata),
+ metadata: metadata_from_host_file_with_cache(
+ &base,
+ &metadata,
+ &self.fingerprint_cache,
+ )?,
});
return Ok(entries);
}
- list_recursive(&self.root, &base, &mut entries)?;
+ list_recursive(&self.root, &base, &self.fingerprint_cache, &mut entries)?;
entries.sort_by(|a, b| a.path.as_str().cmp(b.path.as_str()));
Ok(entries)
}
@@ -174,7 +193,12 @@ fn select_casefolded_match(
}
}
-fn list_recursive(root: &Path, dir: &Path, out: &mut Vec<VfsEntry>) -> Result<(), VfsError> {
+fn list_recursive(
+ root: &Path,
+ dir: &Path,
+ fingerprint_cache: &Mutex<BTreeMap<PathBuf, CachedHostFingerprint>>,
+ out: &mut Vec<VfsEntry>,
+) -> Result<(), VfsError> {
let read_dir = fs::read_dir(dir).map_err(VfsError::Io)?;
let mut children = Vec::new();
for entry in read_dir {
@@ -188,7 +212,7 @@ fn list_recursive(root: &Path, dir: &Path, out: &mut Vec<VfsEntry>) -> Result<()
return Err(VfsError::Path);
}
if metadata.is_dir() {
- list_recursive(root, &child, out)?;
+ list_recursive(root, &child, fingerprint_cache, out)?;
continue;
}
if !metadata.is_file() {
@@ -203,25 +227,49 @@ fn list_recursive(root: &Path, dir: &Path, out: &mut Vec<VfsEntry>) -> Result<()
.map_err(|_| VfsError::Path)?;
out.push(VfsEntry {
path,
- metadata: metadata_from_fs(&metadata),
+ metadata: metadata_from_host_file_with_cache(&child, &metadata, fingerprint_cache)?,
});
}
Ok(())
}
-fn metadata_from_fs(metadata: &fs::Metadata) -> VfsMetadata {
- let mut fingerprint = 0xcbf2_9ce4_8422_2325;
- hash_u64(&mut fingerprint, metadata.len());
- if let Ok(modified) = metadata.modified() {
- if let Ok(duration) = modified.duration_since(std::time::UNIX_EPOCH) {
- hash_u64(&mut fingerprint, duration.as_secs());
- hash_u64(&mut fingerprint, u64::from(duration.subsec_nanos()));
- }
- }
- VfsMetadata {
- len: metadata.len(),
- fingerprint,
+fn metadata_from_host_file_with_cache(
+ path: &Path,
+ metadata: &fs::Metadata,
+ fingerprint_cache: &Mutex<BTreeMap<PathBuf, CachedHostFingerprint>>,
+) -> Result<VfsMetadata, VfsError> {
+ if !metadata.is_file() {
+ return Err(VfsError::Path);
+ }
+ let len = metadata.len();
+ let modified = metadata.modified().ok();
+ if let Some(cached) = fingerprint_cache
+ .lock()
+ .map_err(|_| VfsError::Path)?
+ .get(path)
+ .cloned()
+ .filter(|cached| cached.len == len && cached.modified == modified)
+ {
+ return Ok(VfsMetadata {
+ len,
+ fingerprint: cached.fingerprint,
+ });
}
+
+ let bytes = fs::read(path).map_err(VfsError::Io)?;
+ let fingerprint = sha256(&bytes);
+ fingerprint_cache
+ .lock()
+ .map_err(|_| VfsError::Path)?
+ .insert(
+ path.to_path_buf(),
+ CachedHostFingerprint {
+ len,
+ modified,
+ fingerprint,
+ },
+ );
+ Ok(VfsMetadata { len, fingerprint })
}
/// In-memory VFS.
@@ -276,7 +324,7 @@ impl Vfs for MemoryVfs {
.ok_or_else(|| VfsError::NotFound(path.as_str().to_string()))?;
Ok(VfsMetadata {
len: bytes.len() as u64,
- fingerprint: stable_hash(bytes),
+ fingerprint: sha256(bytes),
})
}
@@ -305,7 +353,7 @@ impl Vfs for MemoryVfs {
path: normalized,
metadata: VfsMetadata {
len: bytes.len() as u64,
- fingerprint: stable_hash(bytes),
+ fingerprint: sha256(bytes),
},
});
}
@@ -314,22 +362,6 @@ impl Vfs for MemoryVfs {
}
}
-fn stable_hash(bytes: &[u8]) -> u64 {
- let mut state = 0xcbf2_9ce4_8422_2325;
- for byte in bytes {
- state ^= u64::from(*byte);
- state = state.wrapping_mul(0x0000_0100_0000_01b3);
- }
- state
-}
-
-fn hash_u64(state: &mut u64, value: u64) {
- for byte in value.to_le_bytes() {
- *state ^= u64::from(byte);
- *state = state.wrapping_mul(0x0000_0100_0000_01b3);
- }
-}
-
/// Layered VFS with deterministic first-layer precedence.
#[derive(Clone, Default)]
pub struct OverlayVfs {
@@ -475,6 +507,24 @@ mod tests {
std::fs::remove_dir_all(root).expect("cleanup");
}
+ #[test]
+ fn directory_vfs_fingerprint_changes_for_same_length_content() {
+ let root = unique_test_dir("content-fingerprint");
+ std::fs::create_dir_all(root.join("DATA")).expect("mkdir");
+ std::fs::write(root.join("DATA").join("File.bin"), b"before").expect("write before");
+
+ let vfs = DirectoryVfs::new(&root);
+ let path = normalize_relative(b"DATA/File.bin", PathPolicy::StrictLegacy).expect("path");
+ let before = vfs.metadata(&path).expect("before metadata");
+ std::fs::write(root.join("DATA").join("File.bin"), b"after!").expect("write after");
+ let after = vfs.metadata(&path).expect("after metadata");
+
+ assert_eq!(before.len, after.len);
+ assert_ne!(before.fingerprint, after.fingerprint);
+
+ std::fs::remove_dir_all(root).expect("cleanup");
+ }
+
#[cfg(unix)]
#[test]
fn directory_vfs_rejects_symlink_escape() {