Skip to main content

fallow_types/
source_fingerprint.rs

1//! Shared source-file fingerprint inputs for cache invalidation.
2
3use std::fs::Metadata;
4use std::time::SystemTime;
5
6use serde::{Deserialize, Serialize};
7
8/// File metadata used to decide whether a source-derived cache entry is fresh.
9///
10/// This is intentionally metadata-only. Callers that need content validation
11/// can combine it with their existing content hash, while cheap caches can use
12/// the same freshness shape without inventing their own `(mtime, size)` tuple.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub struct SourceFingerprint {
15    /// Source file modification time as nanoseconds since the Unix epoch.
16    ///
17    /// A value of `0` means the timestamp could not be read. Fast metadata-only
18    /// cache hits should treat that as unknown and miss conservatively.
19    pub mtime_ns: u64,
20    /// Source file size in bytes.
21    pub file_size: u64,
22}
23
24impl SourceFingerprint {
25    /// Build a fingerprint from explicit metadata parts.
26    #[must_use]
27    pub const fn new(mtime_ns: u64, file_size: u64) -> Self {
28        Self {
29            mtime_ns,
30            file_size,
31        }
32    }
33
34    /// Build a fingerprint from filesystem metadata.
35    #[must_use]
36    pub fn from_metadata(metadata: &Metadata) -> Self {
37        Self {
38            mtime_ns: metadata_mtime_ns(metadata),
39            file_size: metadata.len(),
40        }
41    }
42
43    /// Returns true when the modification time is known.
44    #[must_use]
45    pub const fn has_known_mtime(self) -> bool {
46        self.mtime_ns > 0
47    }
48}
49
50#[expect(
51    clippy::cast_possible_truncation,
52    reason = "filesystem mtimes used for cache invalidation fit in u64 nanoseconds for supported dates"
53)]
54fn metadata_mtime_ns(metadata: &Metadata) -> u64 {
55    metadata
56        .modified()
57        .ok()
58        .and_then(|time| time.duration_since(SystemTime::UNIX_EPOCH).ok())
59        .map_or(0, |duration| duration.as_nanos() as u64)
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn source_fingerprint_preserves_explicit_parts() {
68        let fingerprint = SourceFingerprint::new(123, 456);
69        assert_eq!(fingerprint.mtime_ns, 123);
70        assert_eq!(fingerprint.file_size, 456);
71        assert!(fingerprint.has_known_mtime());
72    }
73
74    #[test]
75    fn source_fingerprint_zero_mtime_is_unknown() {
76        let fingerprint = SourceFingerprint::new(0, 456);
77        assert!(!fingerprint.has_known_mtime());
78    }
79
80    #[test]
81    #[cfg_attr(miri, ignore = "filesystem metadata is blocked by Miri isolation")]
82    fn source_fingerprint_from_metadata_sets_size() {
83        let metadata = std::fs::metadata(".").expect("metadata");
84
85        let fingerprint = SourceFingerprint::from_metadata(&metadata);
86
87        assert_eq!(fingerprint.file_size, metadata.len());
88    }
89}