use alloc::string::String;
use alloc::vec::Vec;
use super::types::{ChangeType, FileVersion, SnapshotInfo, TimeError};
use super::walker::{HistoricalEntry, HistoricalTreeProvider};
pub trait VersionHistoryProvider: HistoricalTreeProvider {
fn file_txg_history(&self, path: &str) -> Result<Vec<u64>, TimeError>;
fn all_snapshots(&self) -> Vec<SnapshotInfo>;
fn txg_timestamp(&self, txg: u64) -> Option<u64>;
}
pub struct VersionHistory<'a, P: VersionHistoryProvider> {
provider: &'a P,
}
impl<'a, P: VersionHistoryProvider> VersionHistory<'a, P> {
pub fn new(provider: &'a P) -> Self {
Self { provider }
}
pub fn get_versions(
&self,
path: &str,
limit: Option<usize>,
) -> Result<Vec<FileVersion>, TimeError> {
let txgs = self.provider.file_txg_history(path)?;
if txgs.is_empty() {
return Err(TimeError::PathNotFound(path.into()));
}
let snapshots = self.provider.all_snapshots();
let mut versions = Vec::new();
let mut prev_checksum: Option<[u64; 4]> = None;
for txg in &txgs {
let entry = self.provider.lookup_at_txg(path, *txg)?;
let change_type = if prev_checksum.is_none() {
ChangeType::Created
} else if prev_checksum != Some(entry.checksum) {
ChangeType::Modified
} else {
ChangeType::MetadataChanged
};
prev_checksum = Some(entry.checksum);
let snapshot_name = snapshots
.iter()
.find(|s| s.txg == *txg)
.map(|s| s.name.clone());
let timestamp = self.provider.txg_timestamp(*txg).unwrap_or(0);
versions.push(FileVersion {
txg: *txg,
timestamp,
snapshot_name,
size: entry.size,
checksum: entry.checksum,
change_type,
});
}
versions.sort_by(|a, b| b.txg.cmp(&a.txg));
if let Some(limit) = limit {
versions.truncate(limit);
}
Ok(versions)
}
pub fn get_version_at(&self, path: &str, txg: u64) -> Result<FileVersion, TimeError> {
let entry = self.provider.lookup_at_txg(path, txg)?;
let txgs = self.provider.file_txg_history(path)?;
let prev_txg = txgs.iter().filter(|t| **t < txg).max();
let change_type = if prev_txg.is_none() {
ChangeType::Created
} else if let Some(pt) = prev_txg {
let prev_entry = self.provider.lookup_at_txg(path, *pt)?;
if prev_entry.checksum != entry.checksum {
ChangeType::Modified
} else {
ChangeType::MetadataChanged
}
} else {
ChangeType::Created
};
let snapshots = self.provider.all_snapshots();
let snapshot_name = snapshots
.iter()
.find(|s| s.txg == txg)
.map(|s| s.name.clone());
let timestamp = self.provider.txg_timestamp(txg).unwrap_or(0);
Ok(FileVersion {
txg,
timestamp,
snapshot_name,
size: entry.size,
checksum: entry.checksum,
change_type,
})
}
pub fn count_versions(&self, path: &str) -> Result<usize, TimeError> {
let txgs = self.provider.file_txg_history(path)?;
Ok(txgs.len())
}
pub fn get_first_version(&self, path: &str) -> Result<FileVersion, TimeError> {
let txgs = self.provider.file_txg_history(path)?;
let first_txg = txgs
.iter()
.min()
.ok_or_else(|| TimeError::PathNotFound(path.into()))?;
let entry = self.provider.lookup_at_txg(path, *first_txg)?;
let snapshots = self.provider.all_snapshots();
let snapshot_name = snapshots
.iter()
.find(|s| s.txg == *first_txg)
.map(|s| s.name.clone());
let timestamp = self.provider.txg_timestamp(*first_txg).unwrap_or(0);
Ok(FileVersion {
txg: *first_txg,
timestamp,
snapshot_name,
size: entry.size,
checksum: entry.checksum,
change_type: ChangeType::Created,
})
}
pub fn get_latest_version(&self, path: &str) -> Result<FileVersion, TimeError> {
let txgs = self.provider.file_txg_history(path)?;
let latest_txg = txgs
.iter()
.max()
.ok_or_else(|| TimeError::PathNotFound(path.into()))?;
self.get_version_at(path, *latest_txg)
}
pub fn get_versions_between(
&self,
path: &str,
from_txg: u64,
to_txg: u64,
) -> Result<Vec<FileVersion>, TimeError> {
let all_versions = self.get_versions(path, None)?;
let filtered: Vec<_> = all_versions
.into_iter()
.filter(|v| v.txg >= from_txg && v.txg <= to_txg)
.collect();
Ok(filtered)
}
pub fn existed_at(&self, path: &str, txg: u64) -> bool {
self.provider.exists_at_txg(path, txg)
}
pub fn get_snapshot_versions(&self, path: &str) -> Result<Vec<FileVersion>, TimeError> {
let versions = self.get_versions(path, None)?;
let snapshot_versions: Vec<_> = versions
.into_iter()
.filter(|v| v.snapshot_name.is_some())
.collect();
Ok(snapshot_versions)
}
}
#[derive(Debug, Clone)]
pub struct VersionComparison {
pub version1: FileVersion,
pub version2: FileVersion,
pub size_delta: i64,
pub content_changed: bool,
pub txg_delta: u64,
pub time_delta: u64,
}
impl<'a, P: VersionHistoryProvider> VersionHistory<'a, P> {
pub fn compare_versions(
&self,
path: &str,
txg1: u64,
txg2: u64,
) -> Result<VersionComparison, TimeError> {
let v1 = self.get_version_at(path, txg1)?;
let v2 = self.get_version_at(path, txg2)?;
let size_delta = v2.size as i64 - v1.size as i64;
let content_changed = v1.checksum != v2.checksum;
let txg_delta = txg2.abs_diff(txg1);
let time_delta = v2.timestamp.abs_diff(v1.timestamp);
Ok(VersionComparison {
version1: v1,
version2: v2,
size_delta,
content_changed,
txg_delta,
time_delta,
})
}
}
use super::walker::InMemoryTreeProvider;
#[derive(Debug, Default)]
pub struct InMemoryVersionProvider {
pub tree: InMemoryTreeProvider,
pub snapshots: Vec<SnapshotInfo>,
pub txg_timestamps: Vec<(u64, u64)>,
}
impl InMemoryVersionProvider {
pub fn new() -> Self {
Self::default()
}
pub fn add_snapshot(&mut self, info: SnapshotInfo) {
self.snapshots.push(info);
}
pub fn add_txg_timestamp(&mut self, txg: u64, timestamp: u64) {
self.txg_timestamps.push((txg, timestamp));
}
pub fn add_entry(&mut self, txg: u64, entry: HistoricalEntry) {
self.tree.add_entry(txg, entry);
}
}
impl HistoricalTreeProvider for InMemoryVersionProvider {
fn root_at_txg(&self, txg: u64) -> Result<HistoricalEntry, TimeError> {
self.tree.root_at_txg(txg)
}
fn lookup_at_txg(&self, path: &str, txg: u64) -> Result<HistoricalEntry, TimeError> {
self.tree.lookup_at_txg(path, txg)
}
fn readdir_at_txg(&self, path: &str, txg: u64) -> Result<Vec<HistoricalEntry>, TimeError> {
self.tree.readdir_at_txg(path, txg)
}
fn lookup_by_id_at_txg(&self, object_id: u64, txg: u64) -> Result<HistoricalEntry, TimeError> {
self.tree.lookup_by_id_at_txg(object_id, txg)
}
fn exists_at_txg(&self, path: &str, txg: u64) -> bool {
self.tree.exists_at_txg(path, txg)
}
fn readlink_at_txg(&self, path: &str, txg: u64) -> Result<String, TimeError> {
self.tree.readlink_at_txg(path, txg)
}
}
impl VersionHistoryProvider for InMemoryVersionProvider {
fn file_txg_history(&self, path: &str) -> Result<Vec<u64>, TimeError> {
let txgs: Vec<u64> = self
.tree
.entries
.iter()
.filter(|(_, e)| e.path == path)
.map(|(txg, _)| *txg)
.collect();
if txgs.is_empty() {
return Err(TimeError::PathNotFound(path.into()));
}
Ok(txgs)
}
fn all_snapshots(&self) -> Vec<SnapshotInfo> {
self.snapshots.clone()
}
fn txg_timestamp(&self, txg: u64) -> Option<u64> {
self.txg_timestamps
.iter()
.find(|(t, _)| *t == txg)
.map(|(_, ts)| *ts)
}
}
#[cfg(test)]
mod tests {
use super::super::types::FileType;
use super::*;
fn create_entry(
path: &str,
name: &str,
size: u64,
txg: u64,
checksum: [u64; 4],
) -> HistoricalEntry {
HistoricalEntry {
name: name.into(),
path: path.into(),
object_id: path.len() as u64,
parent_id: 1,
file_type: FileType::Regular,
size,
mode: 0o644,
uid: 1000,
gid: 1000,
atime: txg * 1000,
mtime: txg * 1000,
ctime: txg * 1000,
txg,
checksum,
nlinks: 1,
blocks: size.div_ceil(512),
generation: txg,
}
}
fn create_test_provider() -> InMemoryVersionProvider {
let mut provider = InMemoryVersionProvider::new();
provider.add_entry(
100,
create_entry("/data/file.txt", "file.txt", 100, 100, [1; 4]),
);
provider.add_entry(
200,
create_entry("/data/file.txt", "file.txt", 150, 200, [2; 4]),
);
provider.add_entry(
300,
create_entry("/data/file.txt", "file.txt", 200, 300, [3; 4]),
);
provider.add_txg_timestamp(100, 1704067200); provider.add_txg_timestamp(200, 1705276800); provider.add_txg_timestamp(300, 1706486400);
provider.add_snapshot(SnapshotInfo {
name: "weekly-backup".into(),
creation_time: 1705276800,
txg: 200,
referenced: 1024 * 1024,
used: 512 * 1024,
});
provider
}
#[test]
fn test_get_versions() {
let provider = create_test_provider();
let history = VersionHistory::new(&provider);
let versions = history.get_versions("/data/file.txt", None).unwrap();
assert_eq!(versions.len(), 3);
assert_eq!(versions[0].txg, 300);
assert_eq!(versions[1].txg, 200);
assert_eq!(versions[2].txg, 100);
}
#[test]
fn test_get_versions_with_limit() {
let provider = create_test_provider();
let history = VersionHistory::new(&provider);
let versions = history.get_versions("/data/file.txt", Some(2)).unwrap();
assert_eq!(versions.len(), 2);
}
#[test]
fn test_version_change_types() {
let provider = create_test_provider();
let history = VersionHistory::new(&provider);
let versions = history.get_versions("/data/file.txt", None).unwrap();
let first = versions.iter().find(|v| v.txg == 100).unwrap();
assert!(matches!(first.change_type, ChangeType::Created));
let second = versions.iter().find(|v| v.txg == 200).unwrap();
assert!(matches!(second.change_type, ChangeType::Modified));
}
#[test]
fn test_snapshot_in_versions() {
let provider = create_test_provider();
let history = VersionHistory::new(&provider);
let versions = history.get_versions("/data/file.txt", None).unwrap();
let v200 = versions.iter().find(|v| v.txg == 200).unwrap();
assert_eq!(v200.snapshot_name, Some("weekly-backup".into()));
let v100 = versions.iter().find(|v| v.txg == 100).unwrap();
assert_eq!(v100.snapshot_name, None);
}
#[test]
fn test_get_first_version() {
let provider = create_test_provider();
let history = VersionHistory::new(&provider);
let first = history.get_first_version("/data/file.txt").unwrap();
assert_eq!(first.txg, 100);
assert!(matches!(first.change_type, ChangeType::Created));
}
#[test]
fn test_get_latest_version() {
let provider = create_test_provider();
let history = VersionHistory::new(&provider);
let latest = history.get_latest_version("/data/file.txt").unwrap();
assert_eq!(latest.txg, 300);
assert_eq!(latest.size, 200);
}
#[test]
fn test_count_versions() {
let provider = create_test_provider();
let history = VersionHistory::new(&provider);
let count = history.count_versions("/data/file.txt").unwrap();
assert_eq!(count, 3);
}
#[test]
fn test_versions_between() {
let provider = create_test_provider();
let history = VersionHistory::new(&provider);
let versions = history
.get_versions_between("/data/file.txt", 150, 250)
.unwrap();
assert_eq!(versions.len(), 1);
assert_eq!(versions[0].txg, 200);
}
#[test]
fn test_existed_at() {
let provider = create_test_provider();
let history = VersionHistory::new(&provider);
assert!(history.existed_at("/data/file.txt", 100));
assert!(history.existed_at("/data/file.txt", 200));
assert!(!history.existed_at("/data/file.txt", 50)); }
#[test]
fn test_snapshot_versions() {
let provider = create_test_provider();
let history = VersionHistory::new(&provider);
let snap_versions = history.get_snapshot_versions("/data/file.txt").unwrap();
assert_eq!(snap_versions.len(), 1);
assert_eq!(snap_versions[0].txg, 200);
}
#[test]
fn test_compare_versions() {
let provider = create_test_provider();
let history = VersionHistory::new(&provider);
let cmp = history
.compare_versions("/data/file.txt", 100, 300)
.unwrap();
assert_eq!(cmp.version1.txg, 100);
assert_eq!(cmp.version2.txg, 300);
assert_eq!(cmp.size_delta, 100); assert!(cmp.content_changed);
assert_eq!(cmp.txg_delta, 200);
}
#[test]
fn test_file_not_found() {
let provider = create_test_provider();
let history = VersionHistory::new(&provider);
let result = history.get_versions("/nonexistent", None);
assert!(matches!(result, Err(TimeError::PathNotFound(_))));
}
}