Skip to main content

copy_metadata/
lib.rs

1#![warn(clippy::cargo)]
2
3#[cfg(unix)]
4use std::os::unix::fs::{chown, MetadataExt as _, PermissionsExt as _};
5use std::{fs::File, io, path::Path};
6
7use filetime::{set_file_handle_times, set_file_times, FileTime};
8
9const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x2000000;
10
11/// Opens a file with optimal flags for metadata updates.
12#[inline]
13fn open_file_for_metadata(path: &Path) -> io::Result<File> {
14    let mut opts = std::fs::OpenOptions::new();
15    opts.read(true);
16
17    #[cfg(windows)]
18    {
19        use std::os::windows::fs::OpenOptionsExt;
20        // 0x0100 = FILE_WRITE_ATTRIBUTES: Allows metadata changes even if read-only.
21        // 0x7 = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE: Maximize concurrency.
22        // 0x2000000 = FILE_FLAG_BACKUP_SEMANTICS: Allows dir.
23        opts.access_mode(0x0100)
24            .share_mode(0x7)
25            .custom_flags(FILE_FLAG_BACKUP_SEMANTICS);
26    }
27
28    opts.open(path)
29}
30
31#[cfg(unix)]
32fn copy_permission_impl(
33    to_path: &Path,
34    to_file: Option<&File>,
35    from_meta: &std::fs::Metadata,
36    to_meta: &std::fs::Metadata,
37) -> io::Result<()> {
38    let from_gid = from_meta.gid();
39    let to_gid = to_meta.gid();
40
41    let mut perms = from_meta.permissions();
42    perms.set_mode(perms.mode() & 0o0777);
43
44    // chown only supports path-based operation in std
45    if from_gid != to_gid && chown(to_path, None, Some(from_gid)).is_err() {
46        let new_perms = (perms.mode() & 0o0707) | ((perms.mode() & 0o07) << 3);
47        perms.set_mode(new_perms);
48    }
49
50    // Use handle-based fchmod if available, avoiding TOCTOU
51    if let Some(file) = to_file {
52        file.set_permissions(perms)
53    } else {
54        std::fs::set_permissions(to_path, perms)
55    }
56}
57
58#[cfg(windows)]
59#[inline]
60fn copy_permission_impl(
61    to_path: &Path,
62    to_file: Option<&File>,
63    from_meta: &std::fs::Metadata,
64    _to_meta: &std::fs::Metadata,
65) -> io::Result<()> {
66    let permissions = from_meta.permissions();
67    if let Some(file) = to_file {
68        file.set_permissions(permissions)
69    } else {
70        std::fs::set_permissions(to_path, permissions)
71    }
72}
73
74#[inline]
75fn copy_time_path(to: &Path, from_meta: &std::fs::Metadata) -> io::Result<()> {
76    let atime = FileTime::from_last_access_time(from_meta);
77    let mtime = FileTime::from_last_modification_time(from_meta);
78    set_file_times(to, atime, mtime)
79}
80
81/// Copy metadata from one file to another, including permissions and time.
82pub fn copy_metadata(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
83    let (from, to) = (from.as_ref(), to.as_ref());
84    let from_meta = std::fs::metadata(from)?;
85
86    match open_file_for_metadata(to) {
87        Ok(to_file) => {
88            let to_meta = to_file.metadata()?;
89            let atime = FileTime::from_last_access_time(&from_meta);
90            let mtime = FileTime::from_last_modification_time(&from_meta);
91
92            if let Err(e) = set_file_handle_times(&to_file, Some(atime), Some(mtime)) {
93                if e.kind() == io::ErrorKind::PermissionDenied {
94                    copy_time_path(to, &from_meta)?;
95                } else {
96                    return Err(e);
97                }
98            }
99
100            copy_permission_impl(to, Some(&to_file), &from_meta, &to_meta)
101        }
102        Err(_) => {
103            // Fallback to path-based operations if a handle cannot be opened
104            let to_meta = std::fs::metadata(to)?;
105            let res = copy_time_path(to, &from_meta);
106            copy_permission_impl(to, None, &from_meta, &to_meta)?;
107
108            if let Err(err) = res {
109                // Retry setting time if the initial failure was due to a read-only lock
110                if err.kind() == io::ErrorKind::PermissionDenied {
111                    copy_time_path(to, &from_meta)?;
112                } else {
113                    return Err(err);
114                }
115            }
116            Ok(())
117        }
118    }
119}
120
121/// Copy permission from one file to another.
122pub fn copy_permission(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
123    let (from, to) = (from.as_ref(), to.as_ref());
124    let from_meta = std::fs::metadata(from)?;
125
126    match open_file_for_metadata(to) {
127        Ok(to_file) => {
128            let to_meta = to_file.metadata()?;
129            copy_permission_impl(to, Some(&to_file), &from_meta, &to_meta)
130        }
131        Err(_) => {
132            let to_meta = std::fs::metadata(to)?;
133            copy_permission_impl(to, None, &from_meta, &to_meta)
134        }
135    }
136}
137
138/// Copy time stamp from one file to another.
139///
140/// Including last_access_time (atime) and last_modification_time (mtime).
141pub fn copy_time(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
142    let (from, to) = (from.as_ref(), to.as_ref());
143    let from_meta = std::fs::metadata(from)?;
144
145    let atime = FileTime::from_last_access_time(&from_meta);
146    let mtime = FileTime::from_last_modification_time(&from_meta);
147
148    if let Ok(to_file) = open_file_for_metadata(to) {
149        if let Err(e) = set_file_handle_times(&to_file, Some(atime), Some(mtime)) {
150            if e.kind() == io::ErrorKind::PermissionDenied {
151                return set_file_times(to, atime, mtime);
152            }
153            return Err(e);
154        }
155        Ok(())
156    } else {
157        set_file_times(to, atime, mtime)
158    }
159}