rcp-tools-common 0.40.0

Internal library for RCP file operation tools - shared utilities and core operations (not intended for direct use)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
use serde::{Deserialize, Serialize};
use std::os::unix::fs::MetadataExt;
use std::os::unix::prelude::PermissionsExt;

pub trait Metadata {
    fn uid(&self) -> u32;
    fn gid(&self) -> u32;
    fn atime(&self) -> i64;
    fn atime_nsec(&self) -> i64;
    fn mtime(&self) -> i64;
    fn mtime_nsec(&self) -> i64;
    fn permissions(&self) -> std::fs::Permissions;
    // ctime cannot be set manually, but we include it for comparison purposes
    // default implementation returns 0 to indicate ctime is not available (e.g., in protocol::Metadata)
    fn ctime(&self) -> i64 {
        0
    }
    fn ctime_nsec(&self) -> i64 {
        0
    }
    // size is not preserved (cannot be set), but included for comparison purposes
    // default implementation returns 0 to indicate size is not available or not applicable
    fn size(&self) -> u64 {
        0
    }
}

impl Metadata for std::fs::Metadata {
    fn uid(&self) -> u32 {
        MetadataExt::uid(self)
    }
    fn gid(&self) -> u32 {
        MetadataExt::gid(self)
    }
    fn atime(&self) -> i64 {
        MetadataExt::atime(self)
    }
    fn atime_nsec(&self) -> i64 {
        MetadataExt::atime_nsec(self)
    }
    fn mtime(&self) -> i64 {
        MetadataExt::mtime(self)
    }
    fn mtime_nsec(&self) -> i64 {
        MetadataExt::mtime_nsec(self)
    }
    fn permissions(&self) -> std::fs::Permissions {
        self.permissions()
    }
    fn ctime(&self) -> i64 {
        MetadataExt::ctime(self)
    }
    fn ctime_nsec(&self) -> i64 {
        MetadataExt::ctime_nsec(self)
    }
    fn size(&self) -> u64 {
        self.len()
    }
}

#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct UserAndTimeSettings {
    pub uid: bool,
    pub gid: bool,
    pub time: bool,
}

impl UserAndTimeSettings {
    #[must_use]
    pub fn any(&self) -> bool {
        self.uid || self.gid || self.time
    }
}

pub type ModeMask = u32;

#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct FileSettings {
    pub user_and_time: UserAndTimeSettings,
    pub mode_mask: ModeMask,
    /// Preserve the POSIX.1e access ACL (`system.posix_acl_access`).
    ///
    /// Opt-in, and deliberately not part of [`preserve_all`] — see the comment there.
    pub acl: bool,
}

impl Default for FileSettings {
    fn default() -> Self {
        Self {
            user_and_time: UserAndTimeSettings::default(),
            mode_mask: 0o0777, // remove sticky bit, setuid and setgid to mimic "cp" tool
            acl: false,
        }
    }
}

#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct DirSettings {
    pub user_and_time: UserAndTimeSettings,
    pub mode_mask: ModeMask,
    /// Preserve the POSIX.1e access *and* default ACLs (`system.posix_acl_access` and
    /// `system.posix_acl_default`).
    ///
    /// Opt-in, and deliberately not part of [`preserve_all`] — see the comment there.
    pub acl: bool,
}

impl Default for DirSettings {
    fn default() -> Self {
        Self {
            user_and_time: UserAndTimeSettings::default(),
            mode_mask: 0o0777,
            acl: false,
        }
    }
}

/// Symlink preserve settings.
///
/// There is deliberately no `acl` field: the kernel has no symlink ACL, so asking to preserve one
/// is a request that could only ever be silently ignored. The settings parser rejects `l:acl`
/// instead.
#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct SymlinkSettings {
    pub user_and_time: UserAndTimeSettings,
}

impl SymlinkSettings {
    #[must_use]
    pub fn any(&self) -> bool {
        self.user_and_time.any()
    }
}

#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct Settings {
    pub file: FileSettings,
    pub dir: DirSettings,
    pub symlink: SymlinkSettings,
}

impl Settings {
    /// Turn ACL preservation on (or off) for files and directories.
    ///
    /// Symlinks are untouched — see [`SymlinkSettings`]. This is the single place that knows which
    /// object types can carry an ACL, so the `all+acl` preset, the `none+acl` preset and
    /// [`preserve_all_with_acls`] cannot drift apart.
    #[must_use]
    pub fn with_acl(mut self, acl: bool) -> Self {
        self.file.acl = acl;
        self.dir.acl = acl;
        self
    }

    /// Whether this run asks for any metadata fidelity beyond what the tools ship with.
    ///
    /// The shipped default reproduces the source's `rwx` bits like `cp` and nothing else — no uid,
    /// no gid, no timestamps, no setuid/setgid/sticky. A run that stays there is not making a claim
    /// about the destination's metadata, so advice about metadata it also does not carry (see
    /// [`crate::safedir::RootAclNotice`]) is noise. Asking for *anything* more — `--preserve`,
    /// `all`, a bare `f:uid`, a `7777` mode mask — is that claim, and the advice becomes worth
    /// printing.
    ///
    /// Compared as a whole value rather than field by field, so a future field is covered the
    /// moment it exists. It reads the RESOLVED settings, so `--preserve-settings=none` — and any
    /// spelling that lands back on the default, such as `f:0777 d:0777` — counts as "did not ask",
    /// which is what those settings mean.
    #[must_use]
    pub fn requests_preservation(&self) -> bool {
        *self != Self::default()
    }
}

/// Compute the permission bits to apply, honoring the mode mask.
///
/// When `mode_mask == 0o7777` (the "preserve everything" case) the source mode
/// is returned verbatim, including setuid/setgid/sticky bits. Otherwise the mode
/// is masked with `mode_mask` (e.g. the default `0o0777` strips the special
/// bits, mimicking `cp`). The returned value is always confined to the
/// permission bits (`0o7777`); file-type bits are never included.
///
/// This is the single source of truth for the mode the fd-based metadata appliers in
/// `crate::safedir` apply. It does NOT decide the mode a destination is *created* at: destinations
/// are always created owner-only ([`crate::safedir::DST_FILE_CREATE_MODE`] /
/// [`crate::safedir::DST_DIR_CREATE_MODE`]) and only widened to this value once their contents are
/// written. Files and directories share this logic; pass the relevant `mode_mask`
/// ([`FileSettings::mode_mask`] or [`DirSettings::mode_mask`]).
#[must_use]
pub fn masked_mode<Meta: Metadata>(mode_mask: ModeMask, metadata: &Meta) -> u32 {
    // confine to permission bits up front so a user-supplied mask that itself includes file-type
    // (S_IF*) bits can never leak them into the create-mode / chmod target (`--preserve` parses the
    // mask as an arbitrary octal, with no upper bound)
    let mode = metadata.permissions().mode() & 0o7777;
    if mode_mask == 0o7777 {
        // default preserve keeps all permission bits verbatim
        mode
    } else {
        mode & mode_mask
    }
}

/// Preserve uid, gid, timestamps and the full mode (including setuid/setgid/sticky).
///
/// This does **not** preserve ACLs; use [`preserve_all_with_acls`], equivalent to the `all+acl`
/// settings string, for that.
#[must_use]
pub fn preserve_all() -> Settings {
    let user_and_time = UserAndTimeSettings {
        uid: true,
        gid: true,
        time: true,
    };
    Settings {
        file: FileSettings {
            user_and_time,
            mode_mask: 0o7777,
            // `all` deliberately does NOT preserve ACLs, and this stays spelled out rather than
            // riding on `..Default::default()`. There is no bit in `stat` saying whether an entry
            // has an ACL, so merely finding out costs an extra syscall on every single entry -
            // roughly doubling the per-entry metadata cost of the flag most people reach for by
            // default. ACLs are therefore opt-in via `all+acl` / `preserve_all_with_acls`.
            acl: false,
        },
        dir: DirSettings {
            user_and_time,
            mode_mask: 0o7777,
            // see the file settings above: opt-in for the same per-entry cost reason (and
            // directories pay for two ACLs, access and default)
            acl: false,
        },
        symlink: SymlinkSettings { user_and_time },
    }
}

/// [`preserve_all`] plus POSIX ACLs on files and directories, equivalent to the `all+acl` settings
/// string.
///
/// The settings parser does not call this — it applies the `+acl` modifier generically to whichever
/// preset it saw. The two agree because both route through [`Settings::with_acl`].
#[must_use]
pub fn preserve_all_with_acls() -> Settings {
    preserve_all().with_acl(true)
}

#[must_use]
pub fn preserve_none() -> Settings {
    Settings::default()
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::os::unix::fs::PermissionsExt;
    struct FakeMeta {
        mode: u32,
    }
    impl Metadata for FakeMeta {
        fn uid(&self) -> u32 {
            0
        }
        fn gid(&self) -> u32 {
            0
        }
        fn atime(&self) -> i64 {
            0
        }
        fn atime_nsec(&self) -> i64 {
            0
        }
        fn mtime(&self) -> i64 {
            0
        }
        fn mtime_nsec(&self) -> i64 {
            0
        }
        fn permissions(&self) -> std::fs::Permissions {
            std::fs::Permissions::from_mode(self.mode)
        }
    }
    #[test]
    fn default_mask_strips_setuid_setgid_sticky() {
        // default mode_mask is 0o0777, which must drop the special bits like `cp`
        let meta = FakeMeta { mode: 0o7755 };
        assert_eq!(masked_mode(0o0777, &meta), 0o0755);
    }
    #[test]
    fn full_mask_preserves_special_bits_verbatim() {
        // 0o7777 is the "preserve everything" sentinel: setuid/setgid/sticky survive
        let meta = FakeMeta { mode: 0o4755 };
        assert_eq!(masked_mode(0o7777, &meta), 0o4755);
    }
    #[test]
    fn masking_strips_file_type_bits() {
        // source mode carries S_IFREG (0o100000); only permission bits may be returned
        let meta = FakeMeta { mode: 0o100644 };
        assert_eq!(masked_mode(0o7777, &meta), 0o0644);
        assert_eq!(masked_mode(0o0777, &meta), 0o0644);
    }
    #[test]
    fn out_of_range_mask_cannot_leak_file_type_bits() {
        // --preserve parses the mask as an arbitrary octal u32 (no upper bound), so a mask that
        // itself includes S_IF* bits must still not leak them into the returned mode
        let meta = FakeMeta { mode: 0o100644 };
        assert_eq!(masked_mode(0o100777, &meta), 0o0644);
    }
    #[test]
    fn custom_mask_applies() {
        let meta = FakeMeta { mode: 0o0777 };
        assert_eq!(masked_mode(0o0700, &meta), 0o0700);
    }
    #[test]
    fn shipped_defaults_strip_special_bits_while_preserve_all_keeps_them() {
        // pin the settings the tools ship with so a default change can't silently weaken fidelity
        assert_eq!(FileSettings::default().mode_mask, 0o0777);
        assert_eq!(DirSettings::default().mode_mask, 0o0777);
        let all = preserve_all();
        assert_eq!(all.file.mode_mask, 0o7777);
        assert_eq!(all.dir.mode_mask, 0o7777);
    }
    #[test]
    fn shipped_defaults_and_preserve_all_leave_acls_off() {
        // ACLs are opt-in because detecting one costs a syscall per entry; a silent flip here would
        // impose that cost on every default copy
        assert!(!FileSettings::default().acl);
        assert!(!DirSettings::default().acl);
        let all = preserve_all();
        assert!(!all.file.acl);
        assert!(!all.dir.acl);
        let none = preserve_none();
        assert!(!none.file.acl);
        assert!(!none.dir.acl);
    }
    #[test]
    fn preserve_all_with_acls_enables_acls_without_changing_anything_else() {
        // whole-value equality, so any extra field `with_acl` touches shows up here rather than
        // slipping past a hand-written list of assertions
        let mut expected = preserve_all();
        expected.file.acl = true;
        expected.dir.acl = true;
        assert_eq!(preserve_all_with_acls(), expected);
    }
    #[test]
    fn with_acl_touches_only_the_acl_fields() {
        let mut expected = preserve_none();
        expected.file.acl = true;
        expected.dir.acl = true;
        assert_eq!(preserve_none().with_acl(true), expected);
    }
    #[test]
    fn with_acl_is_idempotent_and_reversible() {
        assert_eq!(
            preserve_none().with_acl(true).with_acl(true),
            preserve_none().with_acl(true)
        );
        assert_eq!(preserve_all_with_acls().with_acl(false), preserve_all());
    }
    #[test]
    fn the_shipped_default_requests_no_preservation() {
        // `none` IS the shipped default, so both spellings of "I did not ask" agree
        assert!(!Settings::default().requests_preservation());
        assert!(!preserve_none().requests_preservation());
    }
    #[test]
    fn any_attribute_beyond_the_default_requests_preservation() {
        // one field at a time, so a predicate that happened to key off only `uid` (or only the
        // mode mask) fails here rather than in whatever consumes it
        for settings in [
            preserve_all(),
            preserve_all_with_acls(),
            preserve_none().with_acl(true),
            Settings {
                file: FileSettings {
                    user_and_time: UserAndTimeSettings {
                        uid: true,
                        ..Default::default()
                    },
                    ..Default::default()
                },
                ..Default::default()
            },
            Settings {
                dir: DirSettings {
                    user_and_time: UserAndTimeSettings {
                        time: true,
                        ..Default::default()
                    },
                    ..Default::default()
                },
                ..Default::default()
            },
            Settings {
                symlink: SymlinkSettings {
                    user_and_time: UserAndTimeSettings {
                        gid: true,
                        ..Default::default()
                    },
                },
                ..Default::default()
            },
            Settings {
                file: FileSettings {
                    mode_mask: 0o7777,
                    ..Default::default()
                },
                ..Default::default()
            },
        ] {
            assert!(
                settings.requests_preservation(),
                "{settings:?} asks for more than the shipped default but reads as 'did not ask'"
            );
        }
    }
    #[test]
    fn settings_that_spell_out_the_default_request_no_preservation() {
        // the predicate reads the RESOLVED value, so writing the default out longhand is the same
        // as not writing it at all — otherwise `f:0777 d:0777` would behave unlike a bare run
        let spelled_out = Settings {
            file: FileSettings {
                mode_mask: 0o0777,
                ..Default::default()
            },
            dir: DirSettings {
                mode_mask: 0o0777,
                ..Default::default()
            },
            symlink: SymlinkSettings::default(),
        };
        assert!(!spelled_out.requests_preservation());
    }
}