Skip to main content

musefs_core/
freshness.rs

1//! The backing-file freshness stamp: the identity a `tracks` row records for
2//! its backing file, compared on every serve to detect an on-disk change that
3//! no database write covers. Strengthened past size + whole-second mtime to
4//! nanosecond mtime + ctime (#276) so a same-size in-place rewrite — including
5//! an adversarial one that resets mtime — cannot evade the guard.
6use std::os::unix::fs::MetadataExt;
7
8const NANOS_PER_SEC: i64 = 1_000_000_000;
9
10/// `(size, mtime_ns, ctime_ns)` captured from one `fstat`. `mtime_ns`/`ctime_ns`
11/// are nanoseconds since the Unix epoch (good until ~2262). `ctime` is the
12/// adversarial backstop: a writer can reset mtime with `utimensat`, but ctime
13/// is bumped by any write and cannot be set backward.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct BackingStamp {
16    pub size: u64,
17    pub mtime_ns: i64,
18    pub ctime_ns: i64,
19}
20
21impl BackingStamp {
22    pub fn from_metadata(meta: &std::fs::Metadata) -> BackingStamp {
23        BackingStamp {
24            size: meta.len(),
25            mtime_ns: meta
26                .mtime()
27                .saturating_mul(NANOS_PER_SEC)
28                .saturating_add(meta.mtime_nsec()),
29            ctime_ns: meta
30                .ctime()
31                .saturating_mul(NANOS_PER_SEC)
32                .saturating_add(meta.ctime_nsec()),
33        }
34    }
35
36    pub fn from_track(t: &musefs_db::Track) -> BackingStamp {
37        BackingStamp {
38            size: t.backing_size,
39            mtime_ns: t.backing_mtime_ns,
40            ctime_ns: t.backing_ctime_ns,
41        }
42    }
43
44    /// Whole-second mtime for the FUSE `getattr` display surface (never the raw
45    /// nanosecond value, which would advertise a ~10^18-second timestamp).
46    pub fn display_secs(&self) -> i64 {
47        self.mtime_ns / NANOS_PER_SEC
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use std::os::unix::fs::MetadataExt;
55
56    #[test]
57    fn from_metadata_captures_ns_and_display_secs() {
58        let dir = tempfile::tempdir().unwrap();
59        let p = dir.path().join("f");
60        std::fs::write(&p, b"hello").unwrap();
61        let meta = std::fs::metadata(&p).unwrap();
62
63        let s = BackingStamp::from_metadata(&meta);
64        assert_eq!(s.size, 5);
65        assert_eq!(s.mtime_ns, meta.mtime() * 1_000_000_000 + meta.mtime_nsec());
66        assert_eq!(s.ctime_ns, meta.ctime() * 1_000_000_000 + meta.ctime_nsec());
67        // Display is whole-second mtime, never the raw nanosecond value.
68        assert_eq!(s.display_secs(), meta.mtime());
69    }
70
71    #[test]
72    fn equality_is_field_wise() {
73        let a = BackingStamp {
74            size: 1,
75            mtime_ns: 2,
76            ctime_ns: 3,
77        };
78        assert_eq!(
79            a,
80            BackingStamp {
81                size: 1,
82                mtime_ns: 2,
83                ctime_ns: 3
84            }
85        );
86        assert_ne!(
87            a,
88            BackingStamp {
89                size: 1,
90                mtime_ns: 2,
91                ctime_ns: 4
92            }
93        );
94    }
95}