1use std::path::{Component, Path, PathBuf};
11
12use filetime::FileTime;
13
14use crate::{Error, Result};
15
16#[cfg(not(unix))]
20fn warn_once(flag: &std::sync::atomic::AtomicBool, what: &str) {
21 use std::sync::atomic::Ordering;
22 if !flag.swap(true, Ordering::Relaxed) {
23 tracing::warn!("{what} is not supported on this platform; skipping (mtime is preserved)");
24 }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum FileTypeKind {
30 File,
32 Dir,
34 Symlink,
36 Other,
38}
39
40#[derive(Debug, Clone, Copy)]
43pub struct MinMeta {
44 pub kind: FileTypeKind,
46 pub len: u64,
48 pub mtime: FileTime,
50 pub mode: u32,
52 pub ino: u64,
54 pub dev: u64,
56 pub uid: u32,
58 pub gid: u32,
60}
61
62#[cfg(target_os = "linux")]
71pub fn meta_min(path: &Path) -> Result<MinMeta> {
72 use rustix::fs::{AtFlags, CWD, StatxFlags, statx};
73
74 const S_IFMT: u32 = 0o170_000;
75
76 let mask = StatxFlags::TYPE
77 | StatxFlags::MODE
78 | StatxFlags::SIZE
79 | StatxFlags::MTIME
80 | StatxFlags::INO
81 | StatxFlags::UID
82 | StatxFlags::GID;
83 let stx = statx(CWD, path, AtFlags::SYMLINK_NOFOLLOW, mask)
84 .map_err(|e| Error::io(path, std::io::Error::from_raw_os_error(e.raw_os_error())))?;
85
86 let mode = u32::from(stx.stx_mode);
87 let kind = match mode & S_IFMT {
88 0o100_000 => FileTypeKind::File,
89 0o040_000 => FileTypeKind::Dir,
90 0o120_000 => FileTypeKind::Symlink,
91 _ => FileTypeKind::Other,
92 };
93 let mtime = FileTime::from_unix_time(stx.stx_mtime.tv_sec, stx.stx_mtime.tv_nsec as u32);
94 let dev = rustix::fs::makedev(stx.stx_dev_major, stx.stx_dev_minor);
95 Ok(MinMeta {
96 kind,
97 len: stx.stx_size,
98 mtime,
99 mode,
100 ino: stx.stx_ino,
101 dev,
102 uid: stx.stx_uid,
103 gid: stx.stx_gid,
104 })
105}
106
107#[cfg(not(target_os = "linux"))]
113pub fn meta_min(path: &Path) -> Result<MinMeta> {
114 let meta = std::fs::symlink_metadata(path).map_err(|e| Error::io(path, e))?;
115 let ftype = meta.file_type();
116 let kind = if ftype.is_symlink() {
117 FileTypeKind::Symlink
118 } else if ftype.is_dir() {
119 FileTypeKind::Dir
120 } else if ftype.is_file() {
121 FileTypeKind::File
122 } else {
123 FileTypeKind::Other
124 };
125 #[cfg(unix)]
126 let (mode, ino, dev, uid, gid) = {
127 use std::os::unix::fs::MetadataExt;
128 (meta.mode(), meta.ino(), meta.dev(), meta.uid(), meta.gid())
129 };
130 #[cfg(not(unix))]
131 let (mode, ino, dev, uid, gid) = (0u32, 0u64, 0u64, 0u32, 0u32);
132 Ok(MinMeta {
133 kind,
134 len: meta.len(),
135 mtime: FileTime::from_last_modification_time(&meta),
136 mode,
137 ino,
138 dev,
139 uid,
140 gid,
141 })
142}
143
144pub fn set_owner_group(
150 path: &Path,
151 uid: u32,
152 gid: u32,
153 owner: bool,
154 group: bool,
155 follow: bool,
156) -> Result<()> {
157 #[cfg(unix)]
158 if owner || group {
159 use rustix::fs::{AtFlags, CWD, Gid, Uid};
160
161 let uid = owner.then(|| Uid::from_raw(uid));
162 let gid = group.then(|| Gid::from_raw(gid));
163 let flags = if follow {
164 AtFlags::empty()
165 } else {
166 AtFlags::SYMLINK_NOFOLLOW
167 };
168 rustix::fs::chownat(CWD, path, uid, gid, flags)
169 .map_err(|error| Error::io(path, std::io::Error::from(error)))?;
170 }
171 #[cfg(not(unix))]
172 {
173 if owner || group {
174 static WARNED: std::sync::atomic::AtomicBool =
175 std::sync::atomic::AtomicBool::new(false);
176 warn_once(&WARNED, "owner/group preservation");
177 }
178 let _ = (path, uid, gid, follow);
179 }
180 Ok(())
181}
182
183pub fn copy_xattrs(src: &Path, dst: &Path, xattrs: bool, acls: bool) -> Result<()> {
193 #[cfg(unix)]
194 if xattrs || acls {
195 for name in xattr::list(src).map_err(|error| Error::io(src, error))? {
196 let is_acl = is_acl_xattr(&name);
197 if (is_acl && !acls) || (!is_acl && !xattrs) {
198 continue;
199 }
200 if let Some(value) = xattr::get(src, &name).map_err(|error| Error::io(src, error))? {
201 xattr::set(dst, &name, &value).map_err(|error| Error::io(dst, error))?;
202 }
203 }
204 }
205 #[cfg(not(unix))]
206 {
207 if xattrs || acls {
208 static WARNED: std::sync::atomic::AtomicBool =
209 std::sync::atomic::AtomicBool::new(false);
210 warn_once(&WARNED, "extended attributes / POSIX ACLs");
211 }
212 let _ = (src, dst);
213 }
214 Ok(())
215}
216
217#[cfg(unix)]
218fn is_acl_xattr(name: &std::ffi::OsStr) -> bool {
219 let bytes = std::os::unix::ffi::OsStrExt::as_bytes(name);
220 bytes.starts_with(b"system.posix_acl_") || bytes == b"com.apple.system.Security"
221}
222
223pub fn canonical_root(root: &Path) -> Result<PathBuf> {
229 if !root.exists() {
230 std::fs::create_dir_all(root).map_err(|e| Error::io(root, e))?;
231 }
232 std::fs::canonicalize(root).map_err(|e| Error::io(root, e))
233}
234
235pub fn check_relative(rel: &Path) -> Result<()> {
243 for comp in rel.components() {
244 match comp {
245 Component::Normal(_) | Component::CurDir => {}
246 Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
247 return Err(Error::Containment(rel.to_path_buf()));
248 }
249 }
250 }
251 Ok(())
252}
253
254pub fn contained_target(root_canon: &Path, target: &Path) -> Result<PathBuf> {
265 let parent = target
266 .parent()
267 .ok_or_else(|| Error::Containment(target.to_path_buf()))?;
268 let parent_canon = std::fs::canonicalize(parent).map_err(|e| Error::io(parent, e))?;
269 if !parent_canon.starts_with(root_canon) {
270 return Err(Error::Containment(target.to_path_buf()));
271 }
272 let name = target
273 .file_name()
274 .ok_or_else(|| Error::Containment(target.to_path_buf()))?;
275 Ok(parent_canon.join(name))
276}
277
278#[allow(unused_variables)]
284pub fn set_mode(path: &Path, mode: u32) -> Result<()> {
285 #[cfg(unix)]
286 {
287 use std::os::unix::fs::PermissionsExt;
288 let perm = std::fs::Permissions::from_mode(mode & !0o6000 & 0o7777);
290 std::fs::set_permissions(path, perm).map_err(|e| Error::io(path, e))?;
291 }
292 #[cfg(not(unix))]
293 {
294 static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
295 warn_once(&WARNED, "POSIX permission bits");
296 }
297 Ok(())
298}
299
300pub fn set_mtime(path: &Path, mtime: FileTime) -> Result<()> {
307 filetime::set_file_mtime(path, mtime).map_err(|e| Error::io(path, e))
308}
309
310pub fn set_symlink_mtime(path: &Path, mtime: FileTime) -> Result<()> {
316 filetime::set_symlink_file_times(path, mtime, mtime).map_err(|e| Error::io(path, e))
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn rejects_parent_escape() {
325 assert!(check_relative(Path::new("../etc/passwd")).is_err());
326 assert!(check_relative(Path::new("a/../../b")).is_err());
327 assert!(check_relative(Path::new("a/b/c")).is_ok());
328 }
329
330 #[test]
331 fn contained_target_blocks_escape_via_symlink() {
332 let tmp = tempfile::tempdir().unwrap();
333 let root = tmp.path().join("dst");
334 let outside = tmp.path().join("outside");
335 std::fs::create_dir_all(&root).unwrap();
336 std::fs::create_dir_all(&outside).unwrap();
337 let root_canon = canonical_root(&root).unwrap();
338
339 #[cfg(unix)]
341 {
342 let link = root.join("escape");
343 std::os::unix::fs::symlink(&outside, &link).unwrap();
344 let target = link.join("evil.txt");
346 assert!(matches!(
347 contained_target(&root_canon, &target),
348 Err(Error::Containment(_))
349 ));
350 }
351
352 let ok = root.join("file.txt");
354 assert!(contained_target(&root_canon, &ok).is_ok());
355 }
356}