use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::{
domain::{error::DotLockError, model::DotLockResult},
storage::{paths::dotlock_data_root, secure_fs},
};
const EPOCH_DIR: &str = "epoch";
#[derive(Debug, Serialize, Deserialize)]
struct EpochAnchor {
epoch: u64,
}
fn anchor_path_in(root: &Path, project_uuid: &str) -> PathBuf {
let digest = Sha256::digest(project_uuid.as_bytes());
let mut name = String::with_capacity(64);
for byte in digest {
use std::fmt::Write as _;
let _ = write!(name, "{byte:02x}");
}
root.join(EPOCH_DIR).join(format!("{name}.toml"))
}
fn last_seen_epoch_in(root: &Path, project_uuid: &str) -> Option<u64> {
let path = anchor_path_in(root, project_uuid);
if !path.exists() {
return None;
}
let content = secure_fs::read_to_string(&path).ok()?;
toml::from_str::<EpochAnchor>(&content)
.ok()
.map(|anchor| anchor.epoch)
}
fn advance_epoch_in(root: &Path, project_uuid: &str, epoch: u64) -> DotLockResult<()> {
if last_seen_epoch_in(root, project_uuid).is_some_and(|stored| stored >= epoch) {
return Ok(());
}
let path = anchor_path_in(root, project_uuid);
let content =
toml::to_string(&EpochAnchor { epoch }).map_err(|e| DotLockError::Io(e.to_string()))?;
secure_fs::write_string_atomic(&path, &content, 0o700, 0o600)
}
pub fn last_seen_epoch(project_uuid: &str) -> Option<u64> {
let root = dotlock_data_root().ok()?;
last_seen_epoch_in(&root, project_uuid)
}
pub fn advance_epoch(project_uuid: &str, epoch: u64) -> DotLockResult<()> {
advance_epoch_in(&dotlock_data_root()?, project_uuid, epoch)
}
#[cfg(test)]
mod tests {
use super::{advance_epoch_in, last_seen_epoch_in};
#[test]
fn anchor_advances_monotonically_and_never_moves_backward() {
let dir = std::env::temp_dir().join(format!(
"dotlock-epoch-anchor-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("time")
.as_nanos()
));
std::fs::create_dir_all(&dir).expect("create dir");
let uuid = "test-uuid";
assert_eq!(last_seen_epoch_in(&dir, uuid), None);
advance_epoch_in(&dir, uuid, 3).expect("advance to 3");
assert_eq!(last_seen_epoch_in(&dir, uuid), Some(3));
advance_epoch_in(&dir, uuid, 2).expect("no-op backward");
assert_eq!(last_seen_epoch_in(&dir, uuid), Some(3));
advance_epoch_in(&dir, uuid, 5).expect("advance to 5");
assert_eq!(last_seen_epoch_in(&dir, uuid), Some(5));
let _ = std::fs::remove_dir_all(&dir);
}
}