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 fn ctime(&self) -> i64 {
16 0
17 }
18 fn ctime_nsec(&self) -> i64 {
19 0
20 }
21 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 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, 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 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#[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 #[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#[must_use]
171pub fn masked_mode<Meta: Metadata>(mode_mask: ModeMask, metadata: &Meta) -> u32 {
172 let mode = metadata.permissions().mode() & 0o7777;
176 if mode_mask == 0o7777 {
177 mode
179 } else {
180 mode & mode_mask
181 }
182}
183
184#[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 acl: false,
205 },
206 dir: DirSettings {
207 user_and_time,
208 mode_mask: 0o7777,
209 acl: false,
212 },
213 symlink: SymlinkSettings { user_and_time },
214 }
215}
216
217#[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 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 let meta = FakeMeta { mode: 0o4755 };
272 assert_eq!(masked_mode(0o7777, &meta), 0o4755);
273 }
274 #[test]
275 fn masking_strips_file_type_bits() {
276 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 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 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 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 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}