use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use lazy_static::lazy_static;
use spin::Mutex;
use super::HistoricalEntry;
use super::history::VersionHistoryProvider;
use super::resolver::{TxgHistoryProvider, TxgTimestamp};
use super::restore::RestoreTarget;
use super::types::{ChangeType, FileType, SnapshotInfo, TimeError};
use super::walker::HistoricalTreeProvider;
use crate::storage::zpl::{S_IFDIR, S_IFLNK, S_IFMT, S_IFREG, ZPL};
const ROOT_OBJECT_ID: u64 = 3;
const BASE_TIMESTAMP: u64 = 1704067200;
const TXG_INTERVAL_SECS: u64 = 5;
#[derive(Debug, Clone)]
struct StoredSnapshot {
name: String,
txg: u64,
creation_time: u64,
referenced: u64,
used: u64,
}
lazy_static! {
static ref SNAPSHOTS: Mutex<BTreeMap<String, StoredSnapshot>> = Mutex::new(BTreeMap::new());
static ref TXG_HISTORY: Mutex<Vec<TxgTimestamp>> = Mutex::new(Vec::new());
static ref VERSION_HISTORY: Mutex<BTreeMap<String, Vec<FileVersionRecord>>> =
Mutex::new(BTreeMap::new());
}
#[derive(Debug, Clone)]
struct FileVersionRecord {
txg: u64,
timestamp: u64,
change_type: ChangeType,
size: u64,
checksum: [u64; 4],
}
#[derive(Debug, Default)]
pub struct ZplTimeTravelAdapter;
impl ZplTimeTravelAdapter {
pub fn new() -> Self {
let mut history = TXG_HISTORY.lock();
if history.is_empty() {
history.push(TxgTimestamp {
txg: 1,
timestamp: BASE_TIMESTAMP,
});
}
drop(history);
Self
}
pub fn record_txg_sync(&self, txg: u64, timestamp: u64) {
let mut history = TXG_HISTORY.lock();
history.push(TxgTimestamp { txg, timestamp });
}
pub fn record_file_change(
&self,
path: &str,
txg: u64,
change_type: ChangeType,
size: u64,
checksum: [u64; 4],
) {
let timestamp = self.txg_to_timestamp_internal(txg);
let mut versions = VERSION_HISTORY.lock();
let records = versions.entry(path.to_string()).or_default();
records.push(FileVersionRecord {
txg,
timestamp,
change_type,
size,
checksum,
});
}
pub fn create_snapshot(&self, name: &str) -> Result<SnapshotInfo, &'static str> {
let zpl = ZPL.lock();
let txg = get_current_txg_from_zpl(&zpl);
let used = zpl.used_bytes();
drop(zpl);
let timestamp = get_current_timestamp();
let snapshot = StoredSnapshot {
name: name.to_string(),
txg,
creation_time: timestamp,
referenced: used,
used: 0, };
let mut snapshots = SNAPSHOTS.lock();
if snapshots.contains_key(name) {
return Err("Snapshot already exists");
}
snapshots.insert(name.to_string(), snapshot.clone());
self.record_txg_sync(txg, timestamp);
Ok(SnapshotInfo {
name: snapshot.name,
creation_time: snapshot.creation_time,
txg: snapshot.txg,
referenced: snapshot.referenced,
used: snapshot.used,
})
}
pub fn delete_snapshot(&self, name: &str) -> Result<(), &'static str> {
let mut snapshots = SNAPSHOTS.lock();
if snapshots.remove(name).is_some() {
Ok(())
} else {
Err("Snapshot not found")
}
}
fn txg_to_timestamp_internal(&self, txg: u64) -> u64 {
let history = TXG_HISTORY.lock();
for entry in history.iter().rev() {
if entry.txg <= txg {
let delta_txg = txg - entry.txg;
return entry.timestamp + delta_txg * TXG_INTERVAL_SECS;
}
}
BASE_TIMESTAMP + txg * TXG_INTERVAL_SECS
}
}
impl TxgHistoryProvider for ZplTimeTravelAdapter {
fn current_txg(&self) -> u64 {
let zpl = ZPL.lock();
get_current_txg_from_zpl(&zpl)
}
fn current_timestamp(&self) -> u64 {
get_current_timestamp()
}
fn min_txg(&self) -> u64 {
let history = TXG_HISTORY.lock();
history.first().map(|e| e.txg).unwrap_or(1)
}
fn txg_to_timestamp(&self, txg: u64) -> Option<u64> {
Some(self.txg_to_timestamp_internal(txg))
}
fn timestamp_to_txg(&self, timestamp: u64) -> Option<u64> {
let history = TXG_HISTORY.lock();
let mut result = None;
for entry in history.iter() {
if entry.timestamp <= timestamp {
result = Some(entry.txg);
} else {
break;
}
}
if result.is_none() && !history.is_empty() {
let last = history.last().unwrap();
if timestamp >= last.timestamp {
let delta_secs = timestamp - last.timestamp;
result = Some(last.txg + delta_secs / TXG_INTERVAL_SECS);
}
}
result
}
fn txg_history(&self) -> Vec<TxgTimestamp> {
TXG_HISTORY.lock().clone()
}
fn lookup_snapshot(&self, name: &str) -> Option<SnapshotInfo> {
let snapshots = SNAPSHOTS.lock();
snapshots.get(name).map(|s| SnapshotInfo {
name: s.name.clone(),
creation_time: s.creation_time,
txg: s.txg,
referenced: s.referenced,
used: s.used,
})
}
fn list_snapshots(&self) -> Vec<SnapshotInfo> {
let snapshots = SNAPSHOTS.lock();
snapshots
.values()
.map(|s| SnapshotInfo {
name: s.name.clone(),
creation_time: s.creation_time,
txg: s.txg,
referenced: s.referenced,
used: s.used,
})
.collect()
}
}
impl HistoricalTreeProvider for ZplTimeTravelAdapter {
fn root_at_txg(&self, _txg: u64) -> Result<HistoricalEntry, TimeError> {
let zpl = ZPL.lock();
let znode = zpl
.get_znode(ROOT_OBJECT_ID)
.ok_or(TimeError::PathNotFound("/".to_string()))?;
Ok(HistoricalEntry {
name: "/".to_string(),
path: "/".to_string(),
object_id: ROOT_OBJECT_ID,
parent_id: ROOT_OBJECT_ID,
file_type: FileType::Directory,
size: 0,
mode: znode.phys.mode as u32 & 0o7777,
uid: znode.phys.uid as u32,
gid: znode.phys.gid as u32,
mtime: znode.phys.mtime[0],
ctime: znode.phys.ctime[0],
atime: znode.phys.atime[0],
txg: znode.phys.generation,
checksum: [0; 4],
nlinks: znode.phys.links,
blocks: znode.phys.size.div_ceil(512),
generation: znode.phys.generation,
})
}
fn lookup_at_txg(&self, path: &str, _txg: u64) -> Result<HistoricalEntry, TimeError> {
let zpl = ZPL.lock();
let components: Vec<&str> = path.split('/').filter(|c| !c.is_empty()).collect();
let mut current_id = ROOT_OBJECT_ID;
let mut parent_id = ROOT_OBJECT_ID;
for component in &components {
parent_id = current_id;
current_id = zpl
.lookup(current_id, component)
.map_err(|_| TimeError::PathNotFound(path.to_string()))?;
}
let znode = zpl
.get_znode(current_id)
.ok_or_else(|| TimeError::PathNotFound(path.to_string()))?;
let file_type = mode_to_file_type(znode.phys.mode);
let name = components
.last()
.map(|s| s.to_string())
.unwrap_or_else(|| "/".to_string());
Ok(HistoricalEntry {
name,
path: path.to_string(),
object_id: current_id,
parent_id,
file_type,
size: znode.phys.size,
mode: znode.phys.mode as u32 & 0o7777,
uid: znode.phys.uid as u32,
gid: znode.phys.gid as u32,
mtime: znode.phys.mtime[0],
ctime: znode.phys.ctime[0],
atime: znode.phys.atime[0],
txg: znode.phys.generation,
checksum: compute_checksum_stub(current_id),
nlinks: znode.phys.links,
blocks: znode.phys.size.div_ceil(512),
generation: znode.phys.generation,
})
}
fn readdir_at_txg(&self, path: &str, _txg: u64) -> Result<Vec<HistoricalEntry>, TimeError> {
let zpl = ZPL.lock();
let dir_id = if path == "/" || path.is_empty() {
ROOT_OBJECT_ID
} else {
let components: Vec<&str> = path.split('/').filter(|c| !c.is_empty()).collect();
let mut current = ROOT_OBJECT_ID;
for comp in &components {
current = zpl
.lookup(current, comp)
.map_err(|_| TimeError::PathNotFound(path.to_string()))?;
}
current
};
let entries = zpl
.readdir(dir_id)
.map_err(|_| TimeError::PathNotFound(path.to_string()))?;
let mut result = Vec::new();
for entry in entries {
if entry.name == "." || entry.name == ".." {
continue;
}
if let Some(znode) = zpl.get_znode(entry.object_id) {
let file_type = mode_to_file_type(znode.phys.mode);
let entry_path = if path == "/" || path.is_empty() {
format!("/{}", entry.name)
} else {
format!("{}/{}", path.trim_end_matches('/'), entry.name)
};
result.push(HistoricalEntry {
name: entry.name,
path: entry_path,
object_id: entry.object_id,
parent_id: dir_id,
file_type,
size: znode.phys.size,
mode: znode.phys.mode as u32 & 0o7777,
uid: znode.phys.uid as u32,
gid: znode.phys.gid as u32,
mtime: znode.phys.mtime[0],
ctime: znode.phys.ctime[0],
atime: znode.phys.atime[0],
txg: znode.phys.generation,
checksum: compute_checksum_stub(entry.object_id),
nlinks: znode.phys.links,
blocks: znode.phys.size.div_ceil(512),
generation: znode.phys.generation,
});
}
}
Ok(result)
}
fn lookup_by_id_at_txg(&self, object_id: u64, _txg: u64) -> Result<HistoricalEntry, TimeError> {
let zpl = ZPL.lock();
let znode = zpl
.get_znode(object_id)
.ok_or(TimeError::PathNotFound(format!("object:{}", object_id)))?;
let file_type = mode_to_file_type(znode.phys.mode);
Ok(HistoricalEntry {
name: format!("[object:{}]", object_id),
path: format!("[object:{}]", object_id),
object_id,
parent_id: znode.phys.parent,
file_type,
size: znode.phys.size,
mode: znode.phys.mode as u32 & 0o7777,
uid: znode.phys.uid as u32,
gid: znode.phys.gid as u32,
mtime: znode.phys.mtime[0],
ctime: znode.phys.ctime[0],
atime: znode.phys.atime[0],
txg: znode.phys.generation,
checksum: compute_checksum_stub(object_id),
nlinks: znode.phys.links,
blocks: znode.phys.size.div_ceil(512),
generation: znode.phys.generation,
})
}
fn exists_at_txg(&self, path: &str, txg: u64) -> bool {
self.lookup_at_txg(path, txg).is_ok()
}
fn readlink_at_txg(&self, path: &str, _txg: u64) -> Result<String, TimeError> {
let zpl = ZPL.lock();
let components: Vec<&str> = path.split('/').filter(|c| !c.is_empty()).collect();
let mut current_id = ROOT_OBJECT_ID;
for comp in &components {
current_id = zpl
.lookup(current_id, comp)
.map_err(|_| TimeError::PathNotFound(path.to_string()))?;
}
zpl.readlink(current_id)
.map_err(|_| TimeError::PathNotFound(path.to_string()))
}
}
impl VersionHistoryProvider for ZplTimeTravelAdapter {
fn file_txg_history(&self, path: &str) -> Result<Vec<u64>, TimeError> {
let versions = VERSION_HISTORY.lock();
if let Some(records) = versions.get(path) {
Ok(records.iter().map(|r| r.txg).collect())
} else {
if let Ok(entry) = self.lookup_at_txg(path, self.current_txg()) {
Ok(vec![entry.txg])
} else {
Err(TimeError::PathNotFound(path.to_string()))
}
}
}
fn all_snapshots(&self) -> Vec<SnapshotInfo> {
self.list_snapshots()
}
fn txg_timestamp(&self, txg: u64) -> Option<u64> {
self.txg_to_timestamp(txg)
}
}
impl RestoreTarget for ZplTimeTravelAdapter {
fn create_file(&mut self, path: &str, entry: &HistoricalEntry) -> Result<(), TimeError> {
let mut zpl = ZPL.lock();
let (parent_path, name) = split_path(path);
let parent_id = resolve_path(&zpl, &parent_path)
.map_err(|_| TimeError::PathNotFound(path.to_string()))?;
let mode = entry.mode | 0o100000; zpl.create(parent_id, &name, mode, entry.uid, entry.gid)
.map_err(|e| TimeError::IoError(format!("create failed: {:?}", e)))?;
Ok(())
}
fn create_directory(&mut self, path: &str, entry: &HistoricalEntry) -> Result<(), TimeError> {
let mut zpl = ZPL.lock();
let (parent_path, name) = split_path(path);
let parent_id = resolve_path(&zpl, &parent_path)
.map_err(|_| TimeError::PathNotFound(path.to_string()))?;
let mode = entry.mode | 0o040000; zpl.mkdir(parent_id, &name, mode, entry.uid, entry.gid)
.map_err(|e| TimeError::IoError(format!("mkdir failed: {:?}", e)))?;
Ok(())
}
fn create_symlink(
&mut self,
path: &str,
target: &str,
entry: &HistoricalEntry,
) -> Result<(), TimeError> {
let mut zpl = ZPL.lock();
let (parent_path, name) = split_path(path);
let parent_id = resolve_path(&zpl, &parent_path)
.map_err(|_| TimeError::PathNotFound(path.to_string()))?;
zpl.symlink(parent_id, &name, target, entry.uid, entry.gid)
.map_err(|e| TimeError::IoError(format!("symlink failed: {:?}", e)))?;
Ok(())
}
fn copy_data(
&mut self,
dest_path: &str,
source_entry: &HistoricalEntry,
) -> Result<u64, TimeError> {
let mut zpl = ZPL.lock();
let dest_id = resolve_path(&zpl, dest_path)
.map_err(|_| TimeError::PathNotFound(dest_path.to_string()))?;
let handle = zpl
.open(dest_id, 1) .map_err(|e| TimeError::IoError(format!("open failed: {:?}", e)))?;
zpl.truncate(dest_id, source_entry.size)
.map_err(|e| TimeError::IoError(format!("truncate failed: {:?}", e)))?;
let _ = zpl.close(handle);
Ok(source_entry.size)
}
fn set_metadata(&mut self, path: &str, entry: &HistoricalEntry) -> Result<(), TimeError> {
let mut zpl = ZPL.lock();
let object_id =
resolve_path(&zpl, path).map_err(|_| TimeError::PathNotFound(path.to_string()))?;
zpl.setattr(
object_id,
Some(entry.mode),
Some(entry.uid),
Some(entry.gid),
)
.map_err(|e| TimeError::IoError(format!("setattr failed: {:?}", e)))?;
Ok(())
}
fn exists(&self, path: &str) -> bool {
let zpl = ZPL.lock();
resolve_path(&zpl, path).is_ok()
}
fn remove(&mut self, path: &str) -> Result<(), TimeError> {
let mut zpl = ZPL.lock();
let (parent_path, name) = split_path(path);
let parent_id = resolve_path(&zpl, &parent_path)
.map_err(|_| TimeError::PathNotFound(path.to_string()))?;
zpl.unlink(parent_id, &name)
.map_err(|e| TimeError::IoError(format!("unlink failed: {:?}", e)))?;
Ok(())
}
}
fn get_current_timestamp() -> u64 {
static COUNTER: core::sync::atomic::AtomicU64 =
core::sync::atomic::AtomicU64::new(BASE_TIMESTAMP);
COUNTER.fetch_add(1, core::sync::atomic::Ordering::Relaxed)
}
fn mode_to_file_type(mode: u64) -> FileType {
match mode as u32 & S_IFMT {
S_IFREG => FileType::Regular,
S_IFDIR => FileType::Directory,
S_IFLNK => FileType::Symlink,
_ => FileType::Regular,
}
}
fn compute_checksum_stub(object_id: u64) -> [u64; 4] {
[
object_id,
object_id.wrapping_mul(31),
object_id.wrapping_mul(37),
object_id.wrapping_mul(41),
]
}
fn get_current_txg_from_zpl(zpl: &crate::storage::zpl::Zpl) -> u64 {
static TXG_COUNTER: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(1);
TXG_COUNTER.fetch_add(1, core::sync::atomic::Ordering::Relaxed)
}
fn split_path(path: &str) -> (String, String) {
let path = path.trim_end_matches('/');
if let Some(pos) = path.rfind('/') {
let parent = if pos == 0 {
"/".to_string()
} else {
path[..pos].to_string()
};
let name = path[pos + 1..].to_string();
(parent, name)
} else {
("/".to_string(), path.to_string())
}
}
fn resolve_path(zpl: &crate::storage::zpl::Zpl, path: &str) -> Result<u64, String> {
if path == "/" || path.is_empty() {
return Ok(ROOT_OBJECT_ID);
}
let components: Vec<&str> = path.split('/').filter(|c| !c.is_empty()).collect();
let mut current = ROOT_OBJECT_ID;
for comp in components {
current = zpl
.lookup(current, comp)
.map_err(|_| format!("Path not found: {}", path))?;
}
Ok(current)
}
lazy_static! {
pub static ref TIME_TRAVEL_ADAPTER: Mutex<ZplTimeTravelAdapter> =
Mutex::new(ZplTimeTravelAdapter::new());
}
pub fn create_snapshot(name: &str) -> Result<SnapshotInfo, &'static str> {
TIME_TRAVEL_ADAPTER.lock().create_snapshot(name)
}
pub fn delete_snapshot(name: &str) -> Result<(), &'static str> {
TIME_TRAVEL_ADAPTER.lock().delete_snapshot(name)
}
pub fn list_snapshots() -> Vec<SnapshotInfo> {
TIME_TRAVEL_ADAPTER.lock().list_snapshots()
}
pub fn get_snapshot(name: &str) -> Option<SnapshotInfo> {
TIME_TRAVEL_ADAPTER.lock().lookup_snapshot(name)
}
pub fn record_file_change(
path: &str,
txg: u64,
change_type: ChangeType,
size: u64,
checksum: [u64; 4],
) {
TIME_TRAVEL_ADAPTER
.lock()
.record_file_change(path, txg, change_type, size, checksum);
}
pub fn record_txg_sync(txg: u64, timestamp: u64) {
TIME_TRAVEL_ADAPTER.lock().record_txg_sync(txg, timestamp);
}
pub fn current_txg() -> u64 {
TIME_TRAVEL_ADAPTER.lock().current_txg()
}
pub fn lookup_at_txg(path: &str, txg: u64) -> Option<HistoricalEntry> {
TIME_TRAVEL_ADAPTER.lock().lookup_at_txg(path, txg).ok()
}
pub fn readdir_at_txg(path: &str, txg: u64) -> Vec<HistoricalEntry> {
TIME_TRAVEL_ADAPTER
.lock()
.readdir_at_txg(path, txg)
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
fn setup() {
SNAPSHOTS.lock().clear();
}
#[test]
fn test_adapter_creation() {
let adapter = ZplTimeTravelAdapter::new();
assert!(adapter.current_txg() >= 1);
}
#[test]
fn test_snapshot_create_delete() {
setup();
let adapter = ZplTimeTravelAdapter::new();
let snap = adapter.create_snapshot("test-snap").unwrap();
assert_eq!(snap.name, "test-snap");
assert!(snap.txg >= 1);
let found = adapter.lookup_snapshot("test-snap");
assert!(found.is_some());
let list = adapter.list_snapshots();
assert!(!list.is_empty());
adapter.delete_snapshot("test-snap").unwrap();
assert!(adapter.lookup_snapshot("test-snap").is_none());
}
#[test]
fn test_duplicate_snapshot_error() {
setup();
let adapter = ZplTimeTravelAdapter::new();
adapter.create_snapshot("dup-test").unwrap();
let result = adapter.create_snapshot("dup-test");
assert!(result.is_err());
}
#[test]
fn test_txg_timestamp_conversion() {
let adapter = ZplTimeTravelAdapter::new();
let ts = adapter.txg_to_timestamp(10);
assert!(ts.is_some());
let txg = adapter.timestamp_to_txg(ts.unwrap());
assert!(txg.is_some());
}
#[test]
fn test_split_path() {
assert_eq!(
split_path("/foo/bar"),
("/foo".to_string(), "bar".to_string())
);
assert_eq!(split_path("/foo"), ("/".to_string(), "foo".to_string()));
assert_eq!(split_path("file"), ("/".to_string(), "file".to_string()));
}
#[test]
fn test_mode_to_file_type() {
assert!(matches!(
mode_to_file_type(S_IFREG as u64),
FileType::Regular
));
assert!(matches!(
mode_to_file_type(S_IFDIR as u64),
FileType::Directory
));
assert!(matches!(
mode_to_file_type(S_IFLNK as u64),
FileType::Symlink
));
}
#[test]
fn test_record_file_change() {
let adapter = ZplTimeTravelAdapter::new();
adapter.record_file_change("/test/file", 100, ChangeType::Created, 1024, [1, 2, 3, 4]);
let history = VERSION_HISTORY.lock();
let records = history.get("/test/file");
assert!(records.is_some());
}
#[test]
fn test_global_functions() {
setup();
let snap = create_snapshot("global-test").unwrap();
assert_eq!(snap.name, "global-test");
let list = list_snapshots();
assert!(list.iter().any(|s| s.name == "global-test"));
let found = get_snapshot("global-test");
assert!(found.is_some());
delete_snapshot("global-test").unwrap();
}
}