Skip to main content

common/
preserve.rs

1use serde::{Deserialize, Serialize};
2use std::os::unix::fs::MetadataExt;
3use std::os::unix::prelude::PermissionsExt;
4
5pub trait Metadata {
6    fn uid(&self) -> u32;
7    fn gid(&self) -> u32;
8    fn atime(&self) -> i64;
9    fn atime_nsec(&self) -> i64;
10    fn mtime(&self) -> i64;
11    fn mtime_nsec(&self) -> i64;
12    fn permissions(&self) -> std::fs::Permissions;
13    // ctime cannot be set manually, but we include it for comparison purposes
14    // default implementation returns 0 to indicate ctime is not available (e.g., in protocol::Metadata)
15    fn ctime(&self) -> i64 {
16        0
17    }
18    fn ctime_nsec(&self) -> i64 {
19        0
20    }
21    // size is not preserved (cannot be set), but included for comparison purposes
22    // default implementation returns 0 to indicate size is not available or not applicable
23    fn size(&self) -> u64 {
24        0
25    }
26}
27
28impl Metadata for std::fs::Metadata {
29    fn uid(&self) -> u32 {
30        MetadataExt::uid(self)
31    }
32    fn gid(&self) -> u32 {
33        MetadataExt::gid(self)
34    }
35    fn atime(&self) -> i64 {
36        MetadataExt::atime(self)
37    }
38    fn atime_nsec(&self) -> i64 {
39        MetadataExt::atime_nsec(self)
40    }
41    fn mtime(&self) -> i64 {
42        MetadataExt::mtime(self)
43    }
44    fn mtime_nsec(&self) -> i64 {
45        MetadataExt::mtime_nsec(self)
46    }
47    fn permissions(&self) -> std::fs::Permissions {
48        self.permissions()
49    }
50    fn ctime(&self) -> i64 {
51        MetadataExt::ctime(self)
52    }
53    fn ctime_nsec(&self) -> i64 {
54        MetadataExt::ctime_nsec(self)
55    }
56    fn size(&self) -> u64 {
57        self.len()
58    }
59}
60
61#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
62pub struct UserAndTimeSettings {
63    pub uid: bool,
64    pub gid: bool,
65    pub time: bool,
66}
67
68impl UserAndTimeSettings {
69    #[must_use]
70    pub fn any(&self) -> bool {
71        self.uid || self.gid || self.time
72    }
73}
74
75pub type ModeMask = u32;
76
77#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
78pub struct FileSettings {
79    pub user_and_time: UserAndTimeSettings,
80    pub mode_mask: ModeMask,
81    /// Preserve the POSIX.1e access ACL (`system.posix_acl_access`).
82    ///
83    /// Opt-in, and deliberately not part of [`preserve_all`] — see the comment there.
84    pub acl: bool,
85}
86
87impl Default for FileSettings {
88    fn default() -> Self {
89        Self {
90            user_and_time: UserAndTimeSettings::default(),
91            mode_mask: 0o0777, // remove sticky bit, setuid and setgid to mimic "cp" tool
92            acl: false,
93        }
94    }
95}
96
97#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
98pub struct DirSettings {
99    pub user_and_time: UserAndTimeSettings,
100    pub mode_mask: ModeMask,
101    /// Preserve the POSIX.1e access *and* default ACLs (`system.posix_acl_access` and
102    /// `system.posix_acl_default`).
103    ///
104    /// Opt-in, and deliberately not part of [`preserve_all`] — see the comment there.
105    pub acl: bool,
106}
107
108impl Default for DirSettings {
109    fn default() -> Self {
110        Self {
111            user_and_time: UserAndTimeSettings::default(),
112            mode_mask: 0o0777,
113            acl: false,
114        }
115    }
116}
117
118/// Symlink preserve settings.
119///
120/// There is deliberately no `acl` field: the kernel has no symlink ACL, so asking to preserve one
121/// is a request that could only ever be silently ignored. The settings parser rejects `l:acl`
122/// instead.
123#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
124pub struct SymlinkSettings {
125    pub user_and_time: UserAndTimeSettings,
126}
127
128impl SymlinkSettings {
129    #[must_use]
130    pub fn any(&self) -> bool {
131        self.user_and_time.any()
132    }
133}
134
135#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
136pub struct Settings {
137    pub file: FileSettings,
138    pub dir: DirSettings,
139    pub symlink: SymlinkSettings,
140}
141
142impl Settings {
143    /// Turn ACL preservation on (or off) for files and directories.
144    ///
145    /// Symlinks are untouched — see [`SymlinkSettings`]. This is the single place that knows which
146    /// object types can carry an ACL, so the `all+acl` preset, the `none+acl` preset and
147    /// [`preserve_all_with_acls`] cannot drift apart.
148    #[must_use]
149    pub fn with_acl(mut self, acl: bool) -> Self {
150        self.file.acl = acl;
151        self.dir.acl = acl;
152        self
153    }
154}
155
156/// Compute the permission bits to apply, honoring the mode mask.
157///
158/// When `mode_mask == 0o7777` (the "preserve everything" case) the source mode
159/// is returned verbatim, including setuid/setgid/sticky bits. Otherwise the mode
160/// is masked with `mode_mask` (e.g. the default `0o0777` strips the special
161/// bits, mimicking `cp`). The returned value is always confined to the
162/// permission bits (`0o7777`); file-type bits are never included.
163///
164/// This is the single source of truth for the mode the fd-based metadata appliers in
165/// `crate::safedir` apply. It does NOT decide the mode a destination is *created* at: destinations
166/// are always created owner-only ([`crate::safedir::DST_FILE_CREATE_MODE`] /
167/// [`crate::safedir::DST_DIR_CREATE_MODE`]) and only widened to this value once their contents are
168/// written. Files and directories share this logic; pass the relevant `mode_mask`
169/// ([`FileSettings::mode_mask`] or [`DirSettings::mode_mask`]).
170#[must_use]
171pub fn masked_mode<Meta: Metadata>(mode_mask: ModeMask, metadata: &Meta) -> u32 {
172    // confine to permission bits up front so a user-supplied mask that itself includes file-type
173    // (S_IF*) bits can never leak them into the create-mode / chmod target (`--preserve` parses the
174    // mask as an arbitrary octal, with no upper bound)
175    let mode = metadata.permissions().mode() & 0o7777;
176    if mode_mask == 0o7777 {
177        // default preserve keeps all permission bits verbatim
178        mode
179    } else {
180        mode & mode_mask
181    }
182}
183
184/// Preserve uid, gid, timestamps and the full mode (including setuid/setgid/sticky).
185///
186/// This does **not** preserve ACLs; use [`preserve_all_with_acls`], equivalent to the `all+acl`
187/// settings string, for that.
188#[must_use]
189pub fn preserve_all() -> Settings {
190    let user_and_time = UserAndTimeSettings {
191        uid: true,
192        gid: true,
193        time: true,
194    };
195    Settings {
196        file: FileSettings {
197            user_and_time,
198            mode_mask: 0o7777,
199            // `all` deliberately does NOT preserve ACLs, and this stays spelled out rather than
200            // riding on `..Default::default()`. There is no bit in `stat` saying whether an entry
201            // has an ACL, so merely finding out costs an extra syscall on every single entry -
202            // roughly doubling the per-entry metadata cost of the flag most people reach for by
203            // default. ACLs are therefore opt-in via `all+acl` / `preserve_all_with_acls`.
204            acl: false,
205        },
206        dir: DirSettings {
207            user_and_time,
208            mode_mask: 0o7777,
209            // see the file settings above: opt-in for the same per-entry cost reason (and
210            // directories pay for two ACLs, access and default)
211            acl: false,
212        },
213        symlink: SymlinkSettings { user_and_time },
214    }
215}
216
217/// [`preserve_all`] plus POSIX ACLs on files and directories, equivalent to the `all+acl` settings
218/// string.
219///
220/// The settings parser does not call this — it applies the `+acl` modifier generically to whichever
221/// preset it saw. The two agree because both route through [`Settings::with_acl`].
222#[must_use]
223pub fn preserve_all_with_acls() -> Settings {
224    preserve_all().with_acl(true)
225}
226
227#[must_use]
228pub fn preserve_none() -> Settings {
229    Settings::default()
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    use std::os::unix::fs::PermissionsExt;
236    struct FakeMeta {
237        mode: u32,
238    }
239    impl Metadata for FakeMeta {
240        fn uid(&self) -> u32 {
241            0
242        }
243        fn gid(&self) -> u32 {
244            0
245        }
246        fn atime(&self) -> i64 {
247            0
248        }
249        fn atime_nsec(&self) -> i64 {
250            0
251        }
252        fn mtime(&self) -> i64 {
253            0
254        }
255        fn mtime_nsec(&self) -> i64 {
256            0
257        }
258        fn permissions(&self) -> std::fs::Permissions {
259            std::fs::Permissions::from_mode(self.mode)
260        }
261    }
262    #[test]
263    fn default_mask_strips_setuid_setgid_sticky() {
264        // default mode_mask is 0o0777, which must drop the special bits like `cp`
265        let meta = FakeMeta { mode: 0o7755 };
266        assert_eq!(masked_mode(0o0777, &meta), 0o0755);
267    }
268    #[test]
269    fn full_mask_preserves_special_bits_verbatim() {
270        // 0o7777 is the "preserve everything" sentinel: setuid/setgid/sticky survive
271        let meta = FakeMeta { mode: 0o4755 };
272        assert_eq!(masked_mode(0o7777, &meta), 0o4755);
273    }
274    #[test]
275    fn masking_strips_file_type_bits() {
276        // source mode carries S_IFREG (0o100000); only permission bits may be returned
277        let meta = FakeMeta { mode: 0o100644 };
278        assert_eq!(masked_mode(0o7777, &meta), 0o0644);
279        assert_eq!(masked_mode(0o0777, &meta), 0o0644);
280    }
281    #[test]
282    fn out_of_range_mask_cannot_leak_file_type_bits() {
283        // --preserve parses the mask as an arbitrary octal u32 (no upper bound), so a mask that
284        // itself includes S_IF* bits must still not leak them into the returned mode
285        let meta = FakeMeta { mode: 0o100644 };
286        assert_eq!(masked_mode(0o100777, &meta), 0o0644);
287    }
288    #[test]
289    fn custom_mask_applies() {
290        let meta = FakeMeta { mode: 0o0777 };
291        assert_eq!(masked_mode(0o0700, &meta), 0o0700);
292    }
293    #[test]
294    fn shipped_defaults_strip_special_bits_while_preserve_all_keeps_them() {
295        // pin the settings the tools ship with so a default change can't silently weaken fidelity
296        assert_eq!(FileSettings::default().mode_mask, 0o0777);
297        assert_eq!(DirSettings::default().mode_mask, 0o0777);
298        let all = preserve_all();
299        assert_eq!(all.file.mode_mask, 0o7777);
300        assert_eq!(all.dir.mode_mask, 0o7777);
301    }
302    #[test]
303    fn shipped_defaults_and_preserve_all_leave_acls_off() {
304        // ACLs are opt-in because detecting one costs a syscall per entry; a silent flip here would
305        // impose that cost on every default copy
306        assert!(!FileSettings::default().acl);
307        assert!(!DirSettings::default().acl);
308        let all = preserve_all();
309        assert!(!all.file.acl);
310        assert!(!all.dir.acl);
311        let none = preserve_none();
312        assert!(!none.file.acl);
313        assert!(!none.dir.acl);
314    }
315    #[test]
316    fn preserve_all_with_acls_enables_acls_without_changing_anything_else() {
317        // whole-value equality, so any extra field `with_acl` touches shows up here rather than
318        // slipping past a hand-written list of assertions
319        let mut expected = preserve_all();
320        expected.file.acl = true;
321        expected.dir.acl = true;
322        assert_eq!(preserve_all_with_acls(), expected);
323    }
324    #[test]
325    fn with_acl_touches_only_the_acl_fields() {
326        let mut expected = preserve_none();
327        expected.file.acl = true;
328        expected.dir.acl = true;
329        assert_eq!(preserve_none().with_acl(true), expected);
330    }
331    #[test]
332    fn with_acl_is_idempotent_and_reversible() {
333        assert_eq!(
334            preserve_none().with_acl(true).with_acl(true),
335            preserve_none().with_acl(true)
336        );
337        assert_eq!(preserve_all_with_acls().with_acl(false), preserve_all());
338    }
339}