use std::fs::Metadata;
use std::time::SystemTime;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SourceFingerprint {
pub mtime_ns: u64,
pub ctime_ns: u64,
pub file_size: u64,
}
impl SourceFingerprint {
#[must_use]
pub const fn new(mtime_ns: u64, file_size: u64) -> Self {
Self {
mtime_ns,
ctime_ns: 0,
file_size,
}
}
#[must_use]
pub const fn with_ctime(mtime_ns: u64, ctime_ns: u64, file_size: u64) -> Self {
Self {
mtime_ns,
ctime_ns,
file_size,
}
}
#[must_use]
pub fn from_metadata(metadata: &Metadata) -> Self {
Self {
mtime_ns: metadata_mtime_ns(metadata),
ctime_ns: metadata_ctime_ns(metadata),
file_size: metadata.len(),
}
}
#[must_use]
pub const fn has_known_mtime(self) -> bool {
self.mtime_ns > 0
}
#[must_use]
pub const fn is_trustworthy_without_content(self) -> bool {
self.mtime_ns > 0 && self.ctime_ns > 0
}
#[must_use]
pub const fn is_settled_before(self, read_started_ns: u64) -> bool {
let newest = if self.mtime_ns > self.ctime_ns {
self.mtime_ns
} else {
self.ctime_ns
};
self.is_trustworthy_without_content()
&& newest.saturating_add(TIMESTAMP_SETTLE_WINDOW_NS) <= read_started_ns
}
#[must_use]
pub const fn for_content_read_at(self, read_started_ns: u64) -> Self {
if self.is_settled_before(read_started_ns) {
self
} else {
Self {
ctime_ns: 0,
..self
}
}
}
}
pub const TIMESTAMP_SETTLE_WINDOW_NS: u64 = 3_000_000_000;
#[must_use]
pub fn now_ns() -> u64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.and_then(|duration| u64::try_from(duration.as_nanos()).ok())
.unwrap_or(0)
}
#[expect(
clippy::cast_possible_truncation,
reason = "filesystem mtimes used for cache invalidation fit in u64 nanoseconds for supported dates"
)]
fn metadata_mtime_ns(metadata: &Metadata) -> u64 {
metadata
.modified()
.ok()
.and_then(|time| time.duration_since(SystemTime::UNIX_EPOCH).ok())
.map_or(0, |duration| duration.as_nanos() as u64)
}
#[cfg(unix)]
fn metadata_ctime_ns(metadata: &Metadata) -> u64 {
use std::os::unix::fs::MetadataExt;
let seconds = u64::try_from(metadata.ctime()).unwrap_or(0);
let nanos = u64::try_from(metadata.ctime_nsec()).unwrap_or(0);
seconds.saturating_mul(1_000_000_000).saturating_add(nanos)
}
#[cfg(not(unix))]
fn metadata_ctime_ns(_metadata: &Metadata) -> u64 {
0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_fingerprint_preserves_explicit_parts() {
let fingerprint = SourceFingerprint::new(123, 456);
assert_eq!(fingerprint.mtime_ns, 123);
assert_eq!(fingerprint.file_size, 456);
assert!(fingerprint.has_known_mtime());
}
#[test]
fn source_fingerprint_zero_mtime_is_unknown() {
let fingerprint = SourceFingerprint::new(0, 456);
assert!(!fingerprint.has_known_mtime());
}
#[test]
fn source_fingerprint_without_ctime_is_never_content_trustworthy() {
let fingerprint = SourceFingerprint::new(123, 456);
assert!(fingerprint.has_known_mtime());
assert!(!fingerprint.is_trustworthy_without_content());
}
#[test]
fn source_fingerprint_with_both_timestamps_is_content_trustworthy() {
let fingerprint = SourceFingerprint::with_ctime(123, 789, 456);
assert_eq!(fingerprint.ctime_ns, 789);
assert!(fingerprint.is_trustworthy_without_content());
}
#[test]
fn a_fingerprint_inside_the_settle_window_is_not_settled() {
let fingerprint = SourceFingerprint::with_ctime(1_000, 2_000, 456);
let read_started = 2_000 + TIMESTAMP_SETTLE_WINDOW_NS - 1;
assert!(!fingerprint.is_settled_before(read_started));
let stored = fingerprint.for_content_read_at(read_started);
assert_eq!(stored.ctime_ns, 0);
assert_eq!(stored.mtime_ns, 1_000);
assert!(!stored.is_trustworthy_without_content());
}
#[test]
fn a_fingerprint_older_than_the_settle_window_is_kept() {
let fingerprint = SourceFingerprint::with_ctime(1_000, 2_000, 456);
let read_started = 2_000 + TIMESTAMP_SETTLE_WINDOW_NS;
assert!(fingerprint.is_settled_before(read_started));
assert_eq!(fingerprint.for_content_read_at(read_started), fingerprint);
}
#[test]
fn a_future_mtime_is_not_settled() {
let fingerprint = SourceFingerprint::with_ctime(u64::MAX - 1, 2_000, 456);
assert!(!fingerprint.is_settled_before(2_000 + TIMESTAMP_SETTLE_WINDOW_NS));
}
#[test]
fn a_fingerprint_without_ctime_is_never_settled() {
let fingerprint = SourceFingerprint::new(1_000, 456);
assert!(!fingerprint.is_settled_before(u64::MAX));
}
#[test]
fn source_fingerprint_differs_when_only_ctime_moved() {
let before = SourceFingerprint::with_ctime(123, 700, 456);
let after = SourceFingerprint::with_ctime(123, 800, 456);
assert_ne!(before, after);
}
#[test]
#[cfg_attr(miri, ignore = "filesystem metadata is blocked by Miri isolation")]
fn source_fingerprint_from_metadata_sets_size() {
let metadata = std::fs::metadata(".").expect("metadata");
let fingerprint = SourceFingerprint::from_metadata(&metadata);
assert_eq!(fingerprint.file_size, metadata.len());
}
#[cfg(unix)]
#[test]
#[cfg_attr(miri, ignore = "filesystem metadata is blocked by Miri isolation")]
fn source_fingerprint_from_metadata_reads_unix_ctime() {
let metadata = std::fs::metadata(".").expect("metadata");
let fingerprint = SourceFingerprint::from_metadata(&metadata);
assert!(fingerprint.ctime_ns > 0);
assert!(fingerprint.is_trustworthy_without_content());
}
}