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 ctime_ns: u64,
36 pub file_size: u64,
38}
39
40impl SourceFingerprint {
41 #[must_use]
46 pub const fn new(mtime_ns: u64, file_size: u64) -> Self {
47 Self {
48 mtime_ns,
49 ctime_ns: 0,
50 file_size,
51 }
52 }
53
54 #[must_use]
56 pub const fn with_ctime(mtime_ns: u64, ctime_ns: u64, file_size: u64) -> Self {
57 Self {
58 mtime_ns,
59 ctime_ns,
60 file_size,
61 }
62 }
63
64 #[must_use]
66 pub fn from_metadata(metadata: &Metadata) -> Self {
67 Self {
68 mtime_ns: metadata_mtime_ns(metadata),
69 ctime_ns: metadata_ctime_ns(metadata),
70 file_size: metadata.len(),
71 }
72 }
73
74 #[must_use]
76 pub const fn has_known_mtime(self) -> bool {
77 self.mtime_ns > 0
78 }
79
80 #[must_use]
86 pub const fn is_trustworthy_without_content(self) -> bool {
87 self.mtime_ns > 0 && self.ctime_ns > 0
88 }
89
90 #[must_use]
104 pub const fn is_settled_before(self, read_started_ns: u64) -> bool {
105 let newest = if self.mtime_ns > self.ctime_ns {
106 self.mtime_ns
107 } else {
108 self.ctime_ns
109 };
110 self.is_trustworthy_without_content()
111 && newest.saturating_add(TIMESTAMP_SETTLE_WINDOW_NS) <= read_started_ns
112 }
113
114 #[must_use]
122 pub const fn for_content_read_at(self, read_started_ns: u64) -> Self {
123 if self.is_settled_before(read_started_ns) {
124 self
125 } else {
126 Self {
127 ctime_ns: 0,
128 ..self
129 }
130 }
131 }
132}
133
134pub const TIMESTAMP_SETTLE_WINDOW_NS: u64 = 3_000_000_000;
143
144#[must_use]
150pub fn now_ns() -> u64 {
151 SystemTime::now()
152 .duration_since(SystemTime::UNIX_EPOCH)
153 .ok()
154 .and_then(|duration| u64::try_from(duration.as_nanos()).ok())
155 .unwrap_or(0)
156}
157
158#[expect(
159 clippy::cast_possible_truncation,
160 reason = "filesystem mtimes used for cache invalidation fit in u64 nanoseconds for supported dates"
161)]
162fn metadata_mtime_ns(metadata: &Metadata) -> u64 {
163 metadata
164 .modified()
165 .ok()
166 .and_then(|time| time.duration_since(SystemTime::UNIX_EPOCH).ok())
167 .map_or(0, |duration| duration.as_nanos() as u64)
168}
169
170#[cfg(unix)]
175fn metadata_ctime_ns(metadata: &Metadata) -> u64 {
176 use std::os::unix::fs::MetadataExt;
177
178 let seconds = u64::try_from(metadata.ctime()).unwrap_or(0);
179 let nanos = u64::try_from(metadata.ctime_nsec()).unwrap_or(0);
180 seconds.saturating_mul(1_000_000_000).saturating_add(nanos)
181}
182
183#[cfg(not(unix))]
185fn metadata_ctime_ns(_metadata: &Metadata) -> u64 {
186 0
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn source_fingerprint_preserves_explicit_parts() {
195 let fingerprint = SourceFingerprint::new(123, 456);
196 assert_eq!(fingerprint.mtime_ns, 123);
197 assert_eq!(fingerprint.file_size, 456);
198 assert!(fingerprint.has_known_mtime());
199 }
200
201 #[test]
202 fn source_fingerprint_zero_mtime_is_unknown() {
203 let fingerprint = SourceFingerprint::new(0, 456);
204 assert!(!fingerprint.has_known_mtime());
205 }
206
207 #[test]
208 fn source_fingerprint_without_ctime_is_never_content_trustworthy() {
209 let fingerprint = SourceFingerprint::new(123, 456);
210 assert!(fingerprint.has_known_mtime());
211 assert!(!fingerprint.is_trustworthy_without_content());
212 }
213
214 #[test]
215 fn source_fingerprint_with_both_timestamps_is_content_trustworthy() {
216 let fingerprint = SourceFingerprint::with_ctime(123, 789, 456);
217 assert_eq!(fingerprint.ctime_ns, 789);
218 assert!(fingerprint.is_trustworthy_without_content());
219 }
220
221 #[test]
222 fn a_fingerprint_inside_the_settle_window_is_not_settled() {
223 let fingerprint = SourceFingerprint::with_ctime(1_000, 2_000, 456);
224 let read_started = 2_000 + TIMESTAMP_SETTLE_WINDOW_NS - 1;
225
226 assert!(!fingerprint.is_settled_before(read_started));
227 let stored = fingerprint.for_content_read_at(read_started);
228 assert_eq!(stored.ctime_ns, 0);
229 assert_eq!(stored.mtime_ns, 1_000);
230 assert!(!stored.is_trustworthy_without_content());
231 }
232
233 #[test]
234 fn a_fingerprint_older_than_the_settle_window_is_kept() {
235 let fingerprint = SourceFingerprint::with_ctime(1_000, 2_000, 456);
236 let read_started = 2_000 + TIMESTAMP_SETTLE_WINDOW_NS;
237
238 assert!(fingerprint.is_settled_before(read_started));
239 assert_eq!(fingerprint.for_content_read_at(read_started), fingerprint);
240 }
241
242 #[test]
243 fn a_future_mtime_is_not_settled() {
244 let fingerprint = SourceFingerprint::with_ctime(u64::MAX - 1, 2_000, 456);
245
246 assert!(!fingerprint.is_settled_before(2_000 + TIMESTAMP_SETTLE_WINDOW_NS));
247 }
248
249 #[test]
250 fn a_fingerprint_without_ctime_is_never_settled() {
251 let fingerprint = SourceFingerprint::new(1_000, 456);
252
253 assert!(!fingerprint.is_settled_before(u64::MAX));
254 }
255
256 #[test]
257 fn source_fingerprint_differs_when_only_ctime_moved() {
258 let before = SourceFingerprint::with_ctime(123, 700, 456);
259 let after = SourceFingerprint::with_ctime(123, 800, 456);
260 assert_ne!(before, after);
261 }
262
263 #[test]
264 #[cfg_attr(miri, ignore = "filesystem metadata is blocked by Miri isolation")]
265 fn source_fingerprint_from_metadata_sets_size() {
266 let metadata = std::fs::metadata(".").expect("metadata");
267
268 let fingerprint = SourceFingerprint::from_metadata(&metadata);
269
270 assert_eq!(fingerprint.file_size, metadata.len());
271 }
272
273 #[cfg(unix)]
274 #[test]
275 #[cfg_attr(miri, ignore = "filesystem metadata is blocked by Miri isolation")]
276 fn source_fingerprint_from_metadata_reads_unix_ctime() {
277 let metadata = std::fs::metadata(".").expect("metadata");
278
279 let fingerprint = SourceFingerprint::from_metadata(&metadata);
280
281 assert!(fingerprint.ctime_ns > 0);
282 assert!(fingerprint.is_trustworthy_without_content());
283 }
284}