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)]
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)]
78pub struct FileSettings {
79    pub user_and_time: UserAndTimeSettings,
80    pub mode_mask: ModeMask,
81}
82
83impl Default for FileSettings {
84    fn default() -> Self {
85        Self {
86            user_and_time: UserAndTimeSettings::default(),
87            mode_mask: 0o0777, // remove sticky bit, setuid and setgid to mimic "cp" tool
88        }
89    }
90}
91
92#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
93pub struct DirSettings {
94    pub user_and_time: UserAndTimeSettings,
95    pub mode_mask: ModeMask,
96}
97
98impl Default for DirSettings {
99    fn default() -> Self {
100        Self {
101            user_and_time: UserAndTimeSettings::default(),
102            mode_mask: 0o0777,
103        }
104    }
105}
106
107#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)]
108pub struct SymlinkSettings {
109    pub user_and_time: UserAndTimeSettings,
110}
111
112impl SymlinkSettings {
113    #[must_use]
114    pub fn any(&self) -> bool {
115        self.user_and_time.any()
116    }
117}
118
119#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)]
120pub struct Settings {
121    pub file: FileSettings,
122    pub dir: DirSettings,
123    pub symlink: SymlinkSettings,
124}
125
126/// Compute the permission bits to apply, honoring the mode mask.
127///
128/// When `mode_mask == 0o7777` (the "preserve everything" case) the source mode
129/// is returned verbatim, including setuid/setgid/sticky bits. Otherwise the mode
130/// is masked with `mode_mask` (e.g. the default `0o0777` strips the special
131/// bits, mimicking `cp`). The returned value is always confined to the
132/// permission bits (`0o7777`); file-type bits are never included.
133///
134/// This is the single source of truth for the destination create-mode and the fd-based metadata
135/// appliers in `crate::safedir`. Files and directories share this logic; pass the relevant
136/// `mode_mask` ([`FileSettings::mode_mask`] or [`DirSettings::mode_mask`]).
137#[must_use]
138pub fn masked_mode<Meta: Metadata>(mode_mask: ModeMask, metadata: &Meta) -> u32 {
139    // confine to permission bits up front so a user-supplied mask that itself includes file-type
140    // (S_IF*) bits can never leak them into the create-mode / chmod target (`--preserve` parses the
141    // mask as an arbitrary octal, with no upper bound)
142    let mode = metadata.permissions().mode() & 0o7777;
143    if mode_mask == 0o7777 {
144        // default preserve keeps all permission bits verbatim
145        mode
146    } else {
147        mode & mode_mask
148    }
149}
150
151#[must_use]
152pub fn preserve_all() -> Settings {
153    let user_and_time = UserAndTimeSettings {
154        uid: true,
155        gid: true,
156        time: true,
157    };
158
159    Settings {
160        file: FileSettings {
161            user_and_time,
162            mode_mask: 0o7777,
163        },
164        dir: DirSettings {
165            user_and_time,
166            mode_mask: 0o7777,
167        },
168        symlink: SymlinkSettings { user_and_time },
169    }
170}
171
172#[must_use]
173pub fn preserve_none() -> Settings {
174    Settings::default()
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use std::os::unix::fs::PermissionsExt;
181    struct FakeMeta {
182        mode: u32,
183    }
184    impl Metadata for FakeMeta {
185        fn uid(&self) -> u32 {
186            0
187        }
188        fn gid(&self) -> u32 {
189            0
190        }
191        fn atime(&self) -> i64 {
192            0
193        }
194        fn atime_nsec(&self) -> i64 {
195            0
196        }
197        fn mtime(&self) -> i64 {
198            0
199        }
200        fn mtime_nsec(&self) -> i64 {
201            0
202        }
203        fn permissions(&self) -> std::fs::Permissions {
204            std::fs::Permissions::from_mode(self.mode)
205        }
206    }
207    #[test]
208    fn default_mask_strips_setuid_setgid_sticky() {
209        // default mode_mask is 0o0777, which must drop the special bits like `cp`
210        let meta = FakeMeta { mode: 0o7755 };
211        assert_eq!(masked_mode(0o0777, &meta), 0o0755);
212    }
213    #[test]
214    fn full_mask_preserves_special_bits_verbatim() {
215        // 0o7777 is the "preserve everything" sentinel: setuid/setgid/sticky survive
216        let meta = FakeMeta { mode: 0o4755 };
217        assert_eq!(masked_mode(0o7777, &meta), 0o4755);
218    }
219    #[test]
220    fn masking_strips_file_type_bits() {
221        // source mode carries S_IFREG (0o100000); only permission bits may be returned
222        let meta = FakeMeta { mode: 0o100644 };
223        assert_eq!(masked_mode(0o7777, &meta), 0o0644);
224        assert_eq!(masked_mode(0o0777, &meta), 0o0644);
225    }
226    #[test]
227    fn out_of_range_mask_cannot_leak_file_type_bits() {
228        // --preserve parses the mask as an arbitrary octal u32 (no upper bound), so a mask that
229        // itself includes S_IF* bits must still not leak them into the returned mode
230        let meta = FakeMeta { mode: 0o100644 };
231        assert_eq!(masked_mode(0o100777, &meta), 0o0644);
232    }
233    #[test]
234    fn custom_mask_applies() {
235        let meta = FakeMeta { mode: 0o0777 };
236        assert_eq!(masked_mode(0o0700, &meta), 0o0700);
237    }
238    #[test]
239    fn shipped_defaults_strip_special_bits_while_preserve_all_keeps_them() {
240        // pin the settings the tools ship with so a default change can't silently weaken fidelity
241        assert_eq!(FileSettings::default().mode_mask, 0o0777);
242        assert_eq!(DirSettings::default().mode_mask, 0o0777);
243        let all = preserve_all();
244        assert_eq!(all.file.mode_mask, 0o7777);
245        assert_eq!(all.dir.mode_mask, 0o7777);
246    }
247}