use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use crate::{
error::Result,
package::checksum_of,
storage::{Stamp, Storage},
};
pub const RACY: Duration = Duration::from_secs(2);
#[derive(Debug, Serialize, Deserialize)]
struct Stamps {
written: (u64, u32),
checksum: String,
files: Vec<(String, Stamp)>,
}
pub fn stamps_path(id: &str, version: &str, location: &str) -> String {
use sha2::{Digest, Sha256};
let hash = Sha256::digest(location.as_bytes());
let hex: String = hash.iter().take(8).map(|b| format!("{b:02x}")).collect();
format!("{id}/{version}-{hex}.postcard")
}
fn now() -> (u64, u32) {
let since = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
(since.as_secs(), since.subsec_nanos())
}
fn current(storage: &dyn Storage, declared: &[String]) -> Option<Vec<(String, Stamp)>> {
declared
.iter()
.map(|path| storage.stamp(path).map(|s| (path.clone(), s)))
.collect()
}
fn remembered(store: &dyn Storage, path: &str, files: &[(String, Stamp)]) -> Option<String> {
let bytes = store.read(path).ok()?;
let stamps: Stamps = postcard::from_bytes(&bytes).ok()?;
if stamps.files != files {
return None;
}
let written = UNIX_EPOCH + Duration::new(stamps.written.0, stamps.written.1);
let settled = files.iter().all(|(_, stamp)| {
let modified = UNIX_EPOCH + Duration::new(stamp.modified.0, stamp.modified.1);
written
.duration_since(modified)
.is_ok_and(|age| age >= RACY)
});
settled.then_some(stamps.checksum)
}
pub fn checksum(
store: Option<&dyn Storage>,
storage: &dyn Storage,
declared: &[String],
path: &str,
) -> Result<String> {
let files = store.and_then(|_| current(storage, declared));
if let (Some(store), Some(files)) = (store, &files)
&& let Some(sum) = remembered(store, path, files)
{
return Ok(sum);
}
let sum = checksum_of(storage, declared)?;
if let (Some(store), Some(files)) = (store, files) {
let stamps = Stamps {
written: now(),
checksum: sum.clone(),
files,
};
if let Ok(bytes) = postcard::to_stdvec(&stamps) {
let _ = store.write(path, &bytes);
}
}
Ok(sum)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{FsStorage, MemoryStorage};
const OLD: Duration = Duration::from_secs(1_700_000_000);
fn set_modified(path: &std::path::Path, ago: Duration) {
let file = std::fs::File::options().write(true).open(path).unwrap();
file.set_modified(UNIX_EPOCH + ago).unwrap();
}
#[test]
fn checksums_are_remembered_while_stamps_hold_and_arent_racy() {
let dir = std::env::temp_dir().join(format!("logoxide-stamps-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let package = FsStorage::new(dir.join("package"));
package.write("logoxide.toml", b"[package]\n").unwrap();
package.write("usx/JHN.usx", b"john").unwrap();
let declared = vec![String::from("logoxide.toml"), String::from("usx/JHN.usx")];
for path in &declared {
set_modified(&dir.join("package").join(path), OLD);
}
let store = MemoryStorage::new();
let key = stamps_path("kjv", "1.0.0", "/somewhere");
let first = checksum(Some(&store), &package, &declared, &key).unwrap();
assert!(store.exists(&key));
std::fs::write(dir.join("package/usx/JHN.usx"), b"JOHN").unwrap();
set_modified(&dir.join("package/usx/JHN.usx"), OLD);
assert_eq!(
checksum(Some(&store), &package, &declared, &key).unwrap(),
first
);
package.write("usx/JHN.usx", b"john and more").unwrap();
set_modified(&dir.join("package/usx/JHN.usx"), OLD);
let second = checksum(Some(&store), &package, &declared, &key).unwrap();
assert_ne!(second, first);
assert_eq!(second, checksum_of(&package, &declared).unwrap());
std::fs::write(dir.join("package/usx/JHN.usx"), b"john and more").unwrap();
let third = checksum(Some(&store), &package, &declared, &key).unwrap();
assert_eq!(third, second);
std::fs::write(dir.join("package/usx/JHN.usx"), b"JOHN AND MORE").unwrap();
assert_ne!(
checksum(Some(&store), &package, &declared, &key).unwrap(),
third
);
let memory = MemoryStorage::with([("logoxide.toml", "x")]);
let one = vec![String::from("logoxide.toml")];
assert_eq!(
checksum(Some(&store), &memory, &one, &key).unwrap(),
checksum_of(&memory, &one).unwrap()
);
assert_eq!(
checksum(None, &package, &declared, &key).unwrap(),
checksum_of(&package, &declared).unwrap()
);
let _ = std::fs::remove_dir_all(&dir);
}
}