mod hash_helper;
mod package_helper;
use std::sync::Arc;
use rspack_cacheable::cacheable;
use rspack_fs::ReadableFileSystem;
use rspack_paths::{ArcPath, AssertUtf8};
use self::{
hash_helper::{ContentHash, HashHelper},
package_helper::PackageHelper,
};
use super::SnapshotOptions;
#[cacheable]
#[derive(Debug)]
pub enum Strategy {
PackageVersion(String),
FileHash { mtime: u64, hash: u64 },
DirHash { hash: u64 },
Missing,
Failed,
}
impl PartialEq for Strategy {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::PackageVersion(v1), Self::PackageVersion(v2)) => v1 == v2,
(Self::FileHash { hash: h1, .. }, Self::FileHash { hash: h2, .. }) => h1 == h2,
(Self::DirHash { hash: h1, .. }, Self::DirHash { hash: h2, .. }) => h1 == h2,
(Self::Missing, Self::Missing) => true,
(Self::Failed, Self::Failed) => true,
_ => false,
}
}
}
#[derive(Debug)]
pub enum ValidateResult {
Deleted,
Modified,
NoChanged,
}
pub struct StrategyHelper {
fs: Arc<dyn ReadableFileSystem>,
package_helper: Arc<PackageHelper>,
hash_helper: HashHelper,
}
impl StrategyHelper {
pub fn new(fs: Arc<dyn ReadableFileSystem>, snapshot_options: Arc<SnapshotOptions>) -> Self {
let package_helper = Arc::new(PackageHelper::new(fs.clone()));
Self {
fs: fs.clone(),
hash_helper: HashHelper::new(fs, snapshot_options, package_helper.clone()),
package_helper,
}
}
async fn modified_time(&self, path: &ArcPath) -> Option<u64> {
if let Ok(info) = self.fs.metadata(path.assert_utf8()).await {
if info.ctime_ms > info.mtime_ms {
Some(info.ctime_ms)
} else {
Some(info.mtime_ms)
}
} else {
None
}
}
pub async fn package_version(&self, path: &ArcPath) -> Option<Strategy> {
self
.package_helper
.package_version(path)
.await
.map(Strategy::PackageVersion)
}
pub async fn file_hash(&self, path: &ArcPath) -> Strategy {
if let Some(ContentHash { hash, mtime }) = self.hash_helper.file_hash(path).await {
Strategy::FileHash { mtime, hash }
} else {
Strategy::Missing
}
}
pub async fn dir_hash(&self, path: &ArcPath) -> Strategy {
if let Some(ContentHash { hash, .. }) = self.hash_helper.dir_hash(path).await {
Strategy::DirHash { hash }
} else {
Strategy::Failed
}
}
pub async fn validate(&self, path: &ArcPath, strategy: &Strategy) -> ValidateResult {
match strategy {
Strategy::PackageVersion(version) => {
let Some(ref cur_version) = self.package_helper.package_version(path).await else {
return ValidateResult::Deleted;
};
if cur_version == version {
ValidateResult::NoChanged
} else {
ValidateResult::Modified
}
}
Strategy::FileHash { mtime, hash } => {
let Some(modified_time) = self.modified_time(path).await else {
return ValidateResult::Deleted;
};
if &modified_time == mtime {
return ValidateResult::NoChanged;
}
let Some(ContentHash { hash: cur_hash, .. }) = self.hash_helper.file_hash(path).await
else {
return ValidateResult::Deleted;
};
if &cur_hash == hash {
ValidateResult::NoChanged
} else {
ValidateResult::Modified
}
}
Strategy::DirHash { hash } => {
let Some(ContentHash { hash: cur_hash, .. }) = self.hash_helper.dir_hash(path).await else {
return ValidateResult::Deleted;
};
if &cur_hash == hash {
ValidateResult::NoChanged
} else {
ValidateResult::Modified
}
}
Strategy::Missing => {
if self.modified_time(path).await.is_some() {
ValidateResult::Modified
} else {
ValidateResult::NoChanged
}
}
Strategy::Failed => ValidateResult::Modified,
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use rspack_fs::{MemoryFileSystem, WritableFileSystem};
use rspack_paths::ArcPath;
use super::{Strategy, StrategyHelper, ValidateResult};
#[tokio::test]
async fn validate_package_version() {
let fs = Arc::new(MemoryFileSystem::default());
fs.create_dir_all("/packages/lib".into()).await.unwrap();
fs.write(
"/packages/lib/package.json".into(),
r#"{"version": "1.0.0"}"#.as_bytes(),
)
.await
.unwrap();
fs.write("/packages/lib/file.js".into(), "abc".as_bytes())
.await
.unwrap();
let strategy = Strategy::PackageVersion("1.0.0".into());
let helper = StrategyHelper::new(fs.clone(), Default::default());
assert!(matches!(
helper
.validate(&ArcPath::from("/packages/lib/file.js"), &strategy)
.await,
ValidateResult::NoChanged
));
let helper = StrategyHelper::new(fs.clone(), Default::default());
fs.write(
"/packages/lib/package.json".into(),
r#"{"version": "1.2.0"}"#.as_bytes(),
)
.await
.unwrap();
assert!(matches!(
helper
.validate(&ArcPath::from("/packages/lib/file.js"), &strategy)
.await,
ValidateResult::Modified
));
let helper = StrategyHelper::new(fs.clone(), Default::default());
fs.remove_file("/packages/lib/package.json".into())
.await
.unwrap();
assert!(matches!(
helper
.validate(&ArcPath::from("/packages/lib/file.js"), &strategy)
.await,
ValidateResult::Deleted
));
}
#[tokio::test]
async fn validate_file_hash() {
let fs = Arc::new(MemoryFileSystem::default());
fs.create_dir_all("/".into()).await.unwrap();
fs.write("/file1.js".into(), "abc".as_bytes())
.await
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(100));
let helper = StrategyHelper::new(fs.clone(), Default::default());
let strategy = helper.file_hash(&ArcPath::from("/file1.js")).await;
assert!(matches!(
helper
.validate(&ArcPath::from("/file1.js"), &strategy)
.await,
ValidateResult::NoChanged
));
std::thread::sleep(std::time::Duration::from_millis(100));
let helper = StrategyHelper::new(fs.clone(), Default::default());
fs.write("/file1.js".into(), "abc".as_bytes())
.await
.unwrap();
assert!(matches!(
helper
.validate(&ArcPath::from("/file1.js"), &strategy)
.await,
ValidateResult::NoChanged
));
std::thread::sleep(std::time::Duration::from_millis(100));
let helper = StrategyHelper::new(fs.clone(), Default::default());
fs.write("/file1.js".into(), "abcd".as_bytes())
.await
.unwrap();
assert!(matches!(
helper
.validate(&ArcPath::from("/file1.js"), &strategy)
.await,
ValidateResult::Modified
));
std::thread::sleep(std::time::Duration::from_millis(100));
let helper = StrategyHelper::new(fs.clone(), Default::default());
fs.remove_file("/file1.js".into()).await.unwrap();
assert!(matches!(
helper
.validate(&ArcPath::from("/file1.js"), &strategy)
.await,
ValidateResult::Deleted
));
}
#[tokio::test]
async fn validate_missing() {
let fs = Arc::new(MemoryFileSystem::default());
fs.create_dir_all("/".into()).await.unwrap();
fs.write("/file1.js".into(), "abc".as_bytes())
.await
.unwrap();
let helper = StrategyHelper::new(fs.clone(), Default::default());
let strategy = Strategy::Missing;
assert!(matches!(
helper
.validate(&ArcPath::from("/file1.js"), &strategy)
.await,
ValidateResult::Modified
));
std::thread::sleep(std::time::Duration::from_millis(100));
fs.write("/file1.js".into(), "abcd".as_bytes())
.await
.unwrap();
assert!(matches!(
helper
.validate(&ArcPath::from("/file1.js"), &strategy)
.await,
ValidateResult::Modified
));
std::thread::sleep(std::time::Duration::from_millis(100));
fs.remove_file("/file1.js".into()).await.unwrap();
assert!(matches!(
helper
.validate(&ArcPath::from("/file1.js"), &strategy)
.await,
ValidateResult::NoChanged
));
}
}