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    tokio::task::spawn_blocking(move || -> Result<()> {
142        tracing::debug!("setting uid and gid");
143        let uid_val = if settings.uid { Some(uid.into()) } else { None };
144        let gid_val = if settings.gid { Some(gid.into()) } else { None };
145        nix::unistd::fchownat(
146            nix::fcntl::AT_FDCWD,
147            &dst,
148            uid_val,
149            gid_val,
150            nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW,
151        )
152        .with_context(|| {
153            format!(
154                "cannot set {:?} owner to {:?} and/or group id to {:?}",
155                &dst, &uid_val, &gid_val
156            )
157        })?;
158        Ok(())
159    })
160    .await?
161}
162
163#[instrument]
164async fn set_time<Meta: Metadata + std::fmt::Debug>(
165    settings: &UserAndTimeSettings,
166    path: &std::path::Path,
167    metadata: &Meta,
168) -> Result<()> {
169    if !settings.time {
170        return Ok(());
171    }
172    let dst = path.to_owned();
173    let atime = metadata.atime();
174    let atime_nsec = metadata.atime_nsec();
175    let mtime = metadata.mtime();
176    let mtime_nsec = metadata.mtime_nsec();
177    tokio::task::spawn_blocking(move || -> Result<()> {
178        tracing::debug!("setting timestamps");
179        let atime_spec = nix::sys::time::TimeSpec::new(atime, atime_nsec);
180        let mtime_spec = nix::sys::time::TimeSpec::new(mtime, mtime_nsec);
181        nix::sys::stat::utimensat(
182            nix::fcntl::AT_FDCWD,
183            &dst,
184            &atime_spec,
185            &mtime_spec,
186            nix::sys::stat::UtimensatFlags::NoFollowSymlink,
187        )
188        .with_context(|| format!("failed setting timestamps for {:?}", &dst))?;
189        Ok(())
190    })
191    .await?
192}
193
194pub async fn set_file_metadata<Meta: Metadata + std::fmt::Debug>(
195    settings: &Settings,
196    metadata: &Meta,
197    path: &std::path::Path,
198) -> Result<()> {
199    let permissions = if settings.file.mode_mask == 0o7777 {
200        // special case for default preserve
201        metadata.permissions()
202    } else {
203        std::fs::Permissions::from_mode(metadata.permissions().mode() & settings.file.mode_mask)
204    };
205    // ordering: chown → chmod → utimensat
206    //
207    // chown first because fchownat clears setuid/setgid on regular files;
208    // chmod afterwards restores them. utimensat last because both chown and
209    // chmod update ctime and may touch mtime, so we set the desired
210    // timestamps as the final step.
211    //
212    // if chown fails (e.g. EPERM when not root), we bail out early rather
213    // than applying permissions for an unverified owner — setting setuid on
214    // a file whose ownership we couldn't control would be a security risk.
215    set_owner(&settings.file.user_and_time, path, metadata).await?;
216    let file = tokio::fs::File::open(path).await?;
217    file.set_permissions(permissions.clone())
218        .await
219        .with_context(|| format!("cannot set {:?} permissions to {:?}", &path, &permissions))?;
220    drop(file);
221    set_time(&settings.file.user_and_time, path, metadata).await?;
222    Ok(())
223}
224
225pub async fn set_dir_metadata<Meta: Metadata + std::fmt::Debug>(
226    settings: &Settings,
227    metadata: &Meta,
228    path: &std::path::Path,
229) -> Result<()> {
230    let permissions = if settings.dir.mode_mask == 0o7777 {
231        // special case for default preserve
232        metadata.permissions()
233    } else {
234        std::fs::Permissions::from_mode(metadata.permissions().mode() & settings.dir.mode_mask)
235    };
236    // same ordering as set_file_metadata: chown → chmod → utimensat.
237    // see that function for rationale.
238    set_owner(&settings.dir.user_and_time, path, metadata).await?;
239    tokio::fs::set_permissions(path, permissions.clone())
240        .await
241        .with_context(|| format!("cannot set {:?} permissions to {:?}", &path, &permissions))?;
242    set_time(&settings.dir.user_and_time, path, metadata).await?;
243    Ok(())
244}
245
246pub async fn set_symlink_metadata<Meta: Metadata + std::fmt::Debug>(
247    settings: &Settings,
248    metadata: &Meta,
249    path: &std::path::Path,
250) -> Result<()> {
251    // we don't set permissions for symlinks, only owner and time
252    set_owner(&settings.symlink.user_and_time, path, metadata).await?;
253    set_time(&settings.symlink.user_and_time, path, metadata).await?;
254    Ok(())
255}
256
257#[must_use]
258pub fn preserve_all() -> Settings {
259    let user_and_time = UserAndTimeSettings {
260        uid: true,
261        gid: true,
262        time: true,
263    };
264
265    Settings {
266        file: FileSettings {
267            user_and_time,
268            mode_mask: 0o7777,
269        },
270        dir: DirSettings {
271            user_and_time,
272            mode_mask: 0o7777,
273        },
274        symlink: SymlinkSettings { user_and_time },
275    }
276}
277
278#[must_use]
279pub fn preserve_none() -> Settings {
280    Settings::default()
281}