fallow_types/
source_fingerprint.rs1use std::fs::Metadata;
4use std::time::SystemTime;
5
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub struct SourceFingerprint {
15 pub mtime_ns: u64,
20 pub file_size: u64,
22}
23
24impl SourceFingerprint {
25 #[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 #[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 #[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}