Skip to main content

common/
preserve.rs

1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::os::unix::fs::MetadataExt;
4use std::os::unix::prelude::PermissionsExt;
5use tracing::instrument;
6
7pub trait Metadata {
8    fn uid(&self) -> u32;
9    fn gid(&self) -> u32;
10    fn atime(&self) -> i64;
11    fn atime_nsec(&self) -> i64;
12    fn mtime(&self) -> i64;
13    fn mtime_nsec(&self) -> i64;
14    fn permissions(&self) -> std::fs::Permissions;
15    // ctime cannot be set manually, but we include it for comparison purposes
16    // default implementation returns 0 to indicate ctime is not available (e.g., in protocol::Metadata)
17    fn ctime(&self) -> i64 {
18        0
19    }
20    fn ctime_nsec(&self) -> i64 {
21        0
22    }
23    // size is not preserved (cannot be set), but included for comparison purposes
24    // default implementation returns 0 to indicate size is not available or not applicable
25    fn size(&self) -> u64 {
26        0
27    }
28}
29
30impl Metadata for std::fs::Metadata {
31    fn uid(&self) -> u32 {
32        MetadataExt::uid(self)
33    }
34    fn gid(&self) -> u32 {
35        MetadataExt::gid(self)
36    }
37    fn atime(&self) -> i64 {
38        MetadataExt::atime(self)
39    }
40    fn atime_nsec(&self) -> i64 {
41        MetadataExt::atime_nsec(self)
42    }
43    fn mtime(&self) -> i64 {
44        MetadataExt::mtime(self)
45    }
46    fn mtime_nsec(&self) -> i64 {
47        MetadataExt::mtime_nsec(self)
48    }
49    fn permissions(&self) -> std::fs::Permissions {
50        self.permissions()
51    }
52    fn ctime(&self) -> i64 {
53        MetadataExt::ctime(self)
54    }
55    fn ctime_nsec(&self) -> i64 {
56        MetadataExt::ctime_nsec(self)
57    }
58    fn size(&self) -> u64 {
59        self.len()
60    }
61}
62
63#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)]
64pub struct UserAndTimeSettings {
65    pub uid: bool,
66    pub gid: bool,
67    pub time: bool,
68}
69
70impl UserAndTimeSettings {
71    #[must_use]
72    pub fn any(&self) -> bool {
73        self.uid || self.gid || self.time
74    }
75}
76
77pub type ModeMask = u32;
78
79#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
80pub struct FileSettings {
81    pub user_and_time: UserAndTimeSettings,
82    pub mode_mask: ModeMask,
83}
84
85impl Default for FileSettings {
86    fn default() -> Self {
87        Self {
88            user_and_time: UserAndTimeSettings::default(),
89            mode_mask: 0o0777, // remove sticky bit, setuid and setgid to mimic "cp" tool
90        }
91    }
92}
93
94#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
95pub struct DirSettings {
96    pub user_and_time: UserAndTimeSettings,
97    pub mode_mask: ModeMask,
98}
99
100impl Default for DirSettings {
101    fn default() -> Self {
102        Self {
103            user_and_time: UserAndTimeSettings::default(),
104            mode_mask: 0o0777,
105        }
106    }
107}
108
109#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)]
110pub struct SymlinkSettings {
111    pub user_and_time: UserAndTimeSettings,
112}
113
114impl SymlinkSettings {
115    #[must_use]
116    pub fn any(&self) -> bool {
117        self.user_and_time.any()
118    }
119}
120
121#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)]
122pub struct Settings {
123    pub file: FileSettings,
124    pub dir: DirSettings,
125    pub symlink: SymlinkSettings,
126}
127
128#[instrument]
129async fn set_owner<Meta: Metadata + std::fmt::Debug>(
130    settings: &UserAndTimeSettings,
131    path: &std::path::Path,
132    metadata: &Meta,
133) -> Result<()> {
134    if !settings.uid && !settings.gid {
135        return Ok(());
136    }
137    let settings = settings.to_owned();
138    let dst = path.to_owned();
139    let uid = metadata.uid();
140    let gid = metadata.gid();
141    crate::walk::run_metadata_probed(
142        congestion::Side::Destination,
143        congestion::MetadataOp::Chmod,
144        async {
145            tokio::task::spawn_blocking(move || -> Result<()> {
146                tracing::debug!("setting uid and gid");
147                let uid_val = if settings.uid { Some(uid.into()) } else { None };
148                let gid_val = if settings.gid { Some(gid.into()) } else { None };
149                nix::unistd::fchownat(
150                    nix::fcntl::AT_FDCWD,
151                    &dst,
152                    uid_val,
153                    gid_val,
154                    nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW,
155                )
156                .with_context(|| {
157                    format!(
158                        "cannot set {:?} owner to {:?} and/or group id to {:?}",
159                        &dst, &uid_val, &gid_val
160                    )
161                })?;
162                Ok(())
163            })
164            .await?
165        },
166    )
167    .await
168}
169
170#[instrument]
171async fn set_time<Meta: Metadata + std::fmt::Debug>(
172    settings: &UserAndTimeSettings,
173    path: &std::path::Path,
174    metadata: &Meta,
175) -> Result<()> {
176    if !settings.time {
177        return Ok(());
178    }
179    let dst = path.to_owned();
180    let atime = metadata.atime();
181    let atime_nsec = metadata.atime_nsec();
182    let mtime = metadata.mtime();
183    let mtime_nsec = metadata.mtime_nsec();
184    crate::walk::run_metadata_probed(
185        congestion::Side::Destination,
186        congestion::MetadataOp::Chmod,
187        async {
188            tokio::task::spawn_blocking(move || -> Result<()> {
189                tracing::debug!("setting timestamps");
190                let atime_spec = nix::sys::time::TimeSpec::new(atime, atime_nsec);
191                let mtime_spec = nix::sys::time::TimeSpec::new(mtime, mtime_nsec);
192                nix::sys::stat::utimensat(
193                    nix::fcntl::AT_FDCWD,
194                    &dst,
195                    &atime_spec,
196                    &mtime_spec,
197                    nix::sys::stat::UtimensatFlags::NoFollowSymlink,
198                )
199                .with_context(|| format!("failed setting timestamps for {:?}", &dst))?;
200                Ok(())
201            })
202            .await?
203        },
204    )
205    .await
206}
207
208pub async fn set_file_metadata<Meta: Metadata + std::fmt::Debug>(
209    settings: &Settings,
210    metadata: &Meta,
211    path: &std::path::Path,
212) -> Result<()> {
213    let permissions = if settings.file.mode_mask == 0o7777 {
214        // special case for default preserve
215        metadata.permissions()
216    } else {
217        std::fs::Permissions::from_mode(metadata.permissions().mode() & settings.file.mode_mask)
218    };
219    // ordering: chown → chmod → utimensat
220    //
221    // chown first because fchownat clears setuid/setgid on regular files;
222    // chmod afterwards restores them. utimensat last because both chown and
223    // chmod update ctime and may touch mtime, so we set the desired
224    // timestamps as the final step.
225    //
226    // if chown fails (e.g. EPERM when not root), we bail out early rather
227    // than applying permissions for an unverified owner — setting setuid on
228    // a file whose ownership we couldn't control would be a security risk.
229    set_owner(&settings.file.user_and_time, path, metadata).await?;
230    let file = crate::walk::run_metadata_probed(
231        congestion::Side::Destination,
232        congestion::MetadataOp::Stat,
233        tokio::fs::File::open(path),
234    )
235    .await?;
236    crate::walk::run_metadata_probed(
237        congestion::Side::Destination,
238        congestion::MetadataOp::Chmod,
239        file.set_permissions(permissions.clone()),
240    )
241    .await
242    .with_context(|| format!("cannot set {:?} permissions to {:?}", &path, &permissions))?;
243    drop(file);
244    set_time(&settings.file.user_and_time, path, metadata).await?;
245    Ok(())
246}
247
248pub async fn set_dir_metadata<Meta: Metadata + std::fmt::Debug>(
249    settings: &Settings,
250    metadata: &Meta,
251    path: &std::path::Path,
252) -> Result<()> {
253    let permissions = if settings.dir.mode_mask == 0o7777 {
254        // special case for default preserve
255        metadata.permissions()
256    } else {
257        std::fs::Permissions::from_mode(metadata.permissions().mode() & settings.dir.mode_mask)
258    };
259    // same ordering as set_file_metadata: chown → chmod → utimensat.
260    // see that function for rationale.
261    set_owner(&settings.dir.user_and_time, path, metadata).await?;
262    crate::walk::run_metadata_probed(
263        congestion::Side::Destination,
264        congestion::MetadataOp::Chmod,
265        tokio::fs::set_permissions(path, permissions.clone()),
266    )
267    .await
268    .with_context(|| format!("cannot set {:?} permissions to {:?}", &path, &permissions))?;
269    set_time(&settings.dir.user_and_time, path, metadata).await?;
270    Ok(())
271}
272
273pub async fn set_symlink_metadata<Meta: Metadata + std::fmt::Debug>(
274    settings: &Settings,
275    metadata: &Meta,
276    path: &std::path::Path,
277) -> Result<()> {
278    // we don't set permissions for symlinks, only owner and time
279    set_owner(&settings.symlink.user_and_time, path, metadata).await?;
280    set_time(&settings.symlink.user_and_time, path, metadata).await?;
281    Ok(())
282}
283
284#[must_use]
285pub fn preserve_all() -> Settings {
286    let user_and_time = UserAndTimeSettings {
287        uid: true,
288        gid: true,
289        time: true,
290    };
291
292    Settings {
293        file: FileSettings {
294            user_and_time,
295            mode_mask: 0o7777,
296        },
297        dir: DirSettings {
298            user_and_time,
299            mode_mask: 0o7777,
300        },
301        symlink: SymlinkSettings { user_and_time },
302    }
303}
304
305#[must_use]
306pub fn preserve_none() -> Settings {
307    Settings::default()
308}