Skip to main content

common/
filecmp.rs

1use std::os::unix::prelude::PermissionsExt;
2use tracing::instrument;
3
4#[derive(Copy, Clone, Debug, Default)]
5pub struct MetadataCmpSettings {
6    pub uid: bool,
7    pub gid: bool,
8    pub mode: bool,
9    pub size: bool,
10    pub mtime: bool,
11    pub ctime: bool,
12}
13
14#[instrument]
15pub fn metadata_equal<
16    M1: crate::preserve::Metadata + std::fmt::Debug,
17    M2: crate::preserve::Metadata + std::fmt::Debug,
18>(
19    settings: &MetadataCmpSettings,
20    metadata1: &M1,
21    metadata2: &M2,
22) -> bool {
23    if settings.uid && metadata1.uid() != metadata2.uid() {
24        return false;
25    }
26    if settings.gid && metadata1.gid() != metadata2.gid() {
27        return false;
28    }
29    if settings.mode && metadata1.permissions().mode() != metadata2.permissions().mode() {
30        return false;
31    }
32    if settings.size && metadata1.size() != metadata2.size() {
33        return false;
34    }
35    if settings.mtime {
36        if metadata1.mtime() != metadata2.mtime() {
37            return false;
38        }
39        // some filesystems do not support nanosecond precision, so we only compare nanoseconds if both files have them
40        if metadata1.mtime_nsec() != 0
41            && metadata2.mtime_nsec() != 0
42            && metadata1.mtime_nsec() != metadata2.mtime_nsec()
43        {
44            return false;
45        }
46    }
47    if settings.ctime {
48        // ctime() returns 0 if not available (e.g., in protocol::Metadata)
49        // only compare if both have ctime available
50        if metadata1.ctime() != 0 && metadata2.ctime() != 0 {
51            if metadata1.ctime() != metadata2.ctime() {
52                return false;
53            }
54            if metadata1.ctime_nsec() != 0
55                && metadata2.ctime_nsec() != 0
56                && metadata1.ctime_nsec() != metadata2.ctime_nsec()
57            {
58                return false;
59            }
60        }
61    }
62    true
63}
64
65/// Returns true if dest mtime is strictly greater than src mtime (including nanoseconds).
66///
67/// Unlike [`metadata_equal`], this does not special-case zero nanoseconds for filesystems
68/// without nanosecond precision. Zero nsec is compared literally, which is the safest
69/// default for a directional check: when in doubt, we overwrite rather than skip.
70#[instrument]
71pub fn dest_is_newer<
72    M1: crate::preserve::Metadata + std::fmt::Debug,
73    M2: crate::preserve::Metadata + std::fmt::Debug,
74>(
75    src: &M1,
76    dest: &M2,
77) -> bool {
78    (dest.mtime(), dest.mtime_nsec()) > (src.mtime(), src.mtime_nsec())
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    #[derive(Debug)]
85    struct FakeMeta {
86        mtime: i64,
87        mtime_nsec: i64,
88    }
89    impl crate::preserve::Metadata for FakeMeta {
90        fn uid(&self) -> u32 {
91            0
92        }
93        fn gid(&self) -> u32 {
94            0
95        }
96        fn atime(&self) -> i64 {
97            0
98        }
99        fn atime_nsec(&self) -> i64 {
100            0
101        }
102        fn mtime(&self) -> i64 {
103            self.mtime
104        }
105        fn mtime_nsec(&self) -> i64 {
106            self.mtime_nsec
107        }
108        fn permissions(&self) -> std::fs::Permissions {
109            std::os::unix::fs::PermissionsExt::from_mode(0o644)
110        }
111    }
112    #[test]
113    fn dest_newer_by_seconds() {
114        let src = FakeMeta {
115            mtime: 100,
116            mtime_nsec: 0,
117        };
118        let dest = FakeMeta {
119            mtime: 200,
120            mtime_nsec: 0,
121        };
122        assert!(dest_is_newer(&src, &dest));
123    }
124    #[test]
125    fn dest_older_by_seconds() {
126        let src = FakeMeta {
127            mtime: 200,
128            mtime_nsec: 0,
129        };
130        let dest = FakeMeta {
131            mtime: 100,
132            mtime_nsec: 0,
133        };
134        assert!(!dest_is_newer(&src, &dest));
135    }
136    #[test]
137    fn same_mtime_not_newer() {
138        let src = FakeMeta {
139            mtime: 100,
140            mtime_nsec: 500,
141        };
142        let dest = FakeMeta {
143            mtime: 100,
144            mtime_nsec: 500,
145        };
146        assert!(!dest_is_newer(&src, &dest));
147    }
148    #[test]
149    fn dest_newer_by_nsec() {
150        let src = FakeMeta {
151            mtime: 100,
152            mtime_nsec: 500,
153        };
154        let dest = FakeMeta {
155            mtime: 100,
156            mtime_nsec: 600,
157        };
158        assert!(dest_is_newer(&src, &dest));
159    }
160    #[test]
161    fn dest_older_by_nsec() {
162        let src = FakeMeta {
163            mtime: 100,
164            mtime_nsec: 600,
165        };
166        let dest = FakeMeta {
167            mtime: 100,
168            mtime_nsec: 500,
169        };
170        assert!(!dest_is_newer(&src, &dest));
171    }
172}