use std::path::{Component, Path, PathBuf};
use filetime::FileTime;
use crate::{Error, Result};
#[cfg(not(unix))]
fn warn_once(flag: &std::sync::atomic::AtomicBool, what: &str) {
use std::sync::atomic::Ordering;
if !flag.swap(true, Ordering::Relaxed) {
tracing::warn!("{what} is not supported on this platform; skipping (mtime is preserved)");
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileTypeKind {
File,
Dir,
Symlink,
Other,
}
#[derive(Debug, Clone, Copy)]
pub struct MinMeta {
pub kind: FileTypeKind,
pub len: u64,
pub mtime: FileTime,
pub mode: u32,
pub ino: u64,
pub dev: u64,
pub uid: u32,
pub gid: u32,
}
#[cfg(target_os = "linux")]
pub fn meta_min(path: &Path) -> Result<MinMeta> {
use rustix::fs::{AtFlags, CWD, StatxFlags, statx};
const S_IFMT: u32 = 0o170_000;
let mask = StatxFlags::TYPE
| StatxFlags::MODE
| StatxFlags::SIZE
| StatxFlags::MTIME
| StatxFlags::INO
| StatxFlags::UID
| StatxFlags::GID;
let stx = statx(CWD, path, AtFlags::SYMLINK_NOFOLLOW, mask)
.map_err(|e| Error::io(path, std::io::Error::from_raw_os_error(e.raw_os_error())))?;
let mode = u32::from(stx.stx_mode);
let kind = match mode & S_IFMT {
0o100_000 => FileTypeKind::File,
0o040_000 => FileTypeKind::Dir,
0o120_000 => FileTypeKind::Symlink,
_ => FileTypeKind::Other,
};
let mtime = FileTime::from_unix_time(stx.stx_mtime.tv_sec, stx.stx_mtime.tv_nsec as u32);
let dev = rustix::fs::makedev(stx.stx_dev_major, stx.stx_dev_minor);
Ok(MinMeta {
kind,
len: stx.stx_size,
mtime,
mode,
ino: stx.stx_ino,
dev,
uid: stx.stx_uid,
gid: stx.stx_gid,
})
}
#[cfg(not(target_os = "linux"))]
pub fn meta_min(path: &Path) -> Result<MinMeta> {
let meta = std::fs::symlink_metadata(path).map_err(|e| Error::io(path, e))?;
let ftype = meta.file_type();
let kind = if ftype.is_symlink() {
FileTypeKind::Symlink
} else if ftype.is_dir() {
FileTypeKind::Dir
} else if ftype.is_file() {
FileTypeKind::File
} else {
FileTypeKind::Other
};
#[cfg(unix)]
let (mode, ino, dev, uid, gid) = {
use std::os::unix::fs::MetadataExt;
(meta.mode(), meta.ino(), meta.dev(), meta.uid(), meta.gid())
};
#[cfg(not(unix))]
let (mode, ino, dev, uid, gid) = (0u32, 0u64, 0u64, 0u32, 0u32);
Ok(MinMeta {
kind,
len: meta.len(),
mtime: FileTime::from_last_modification_time(&meta),
mode,
ino,
dev,
uid,
gid,
})
}
pub fn set_owner_group(
path: &Path,
uid: u32,
gid: u32,
owner: bool,
group: bool,
follow: bool,
) -> Result<()> {
#[cfg(unix)]
if owner || group {
use rustix::fs::{AtFlags, CWD, Gid, Uid};
let uid = owner.then(|| Uid::from_raw(uid));
let gid = group.then(|| Gid::from_raw(gid));
let flags = if follow {
AtFlags::empty()
} else {
AtFlags::SYMLINK_NOFOLLOW
};
rustix::fs::chownat(CWD, path, uid, gid, flags)
.map_err(|error| Error::io(path, std::io::Error::from(error)))?;
}
#[cfg(not(unix))]
{
if owner || group {
static WARNED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
warn_once(&WARNED, "owner/group preservation");
}
let _ = (path, uid, gid, follow);
}
Ok(())
}
pub fn copy_xattrs(src: &Path, dst: &Path, xattrs: bool, acls: bool) -> Result<()> {
#[cfg(unix)]
if xattrs || acls {
for name in xattr::list(src).map_err(|error| Error::io(src, error))? {
let is_acl = is_acl_xattr(&name);
if (is_acl && !acls) || (!is_acl && !xattrs) {
continue;
}
if let Some(value) = xattr::get(src, &name).map_err(|error| Error::io(src, error))? {
xattr::set(dst, &name, &value).map_err(|error| Error::io(dst, error))?;
}
}
}
#[cfg(not(unix))]
{
if xattrs || acls {
static WARNED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
warn_once(&WARNED, "extended attributes / POSIX ACLs");
}
let _ = (src, dst);
}
Ok(())
}
#[cfg(unix)]
fn is_acl_xattr(name: &std::ffi::OsStr) -> bool {
let bytes = std::os::unix::ffi::OsStrExt::as_bytes(name);
bytes.starts_with(b"system.posix_acl_") || bytes == b"com.apple.system.Security"
}
pub fn canonical_root(root: &Path) -> Result<PathBuf> {
if !root.exists() {
std::fs::create_dir_all(root).map_err(|e| Error::io(root, e))?;
}
std::fs::canonicalize(root).map_err(|e| Error::io(root, e))
}
pub fn check_relative(rel: &Path) -> Result<()> {
for comp in rel.components() {
match comp {
Component::Normal(_) | Component::CurDir => {}
Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
return Err(Error::Containment(rel.to_path_buf()));
}
}
}
Ok(())
}
pub fn contained_target(root_canon: &Path, target: &Path) -> Result<PathBuf> {
let parent = target
.parent()
.ok_or_else(|| Error::Containment(target.to_path_buf()))?;
let parent_canon = std::fs::canonicalize(parent).map_err(|e| Error::io(parent, e))?;
if !parent_canon.starts_with(root_canon) {
return Err(Error::Containment(target.to_path_buf()));
}
let name = target
.file_name()
.ok_or_else(|| Error::Containment(target.to_path_buf()))?;
Ok(parent_canon.join(name))
}
#[allow(unused_variables)]
pub fn set_mode(path: &Path, mode: u32) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perm = std::fs::Permissions::from_mode(mode & !0o6000 & 0o7777);
std::fs::set_permissions(path, perm).map_err(|e| Error::io(path, e))?;
}
#[cfg(not(unix))]
{
static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
warn_once(&WARNED, "POSIX permission bits");
}
Ok(())
}
pub fn set_mtime(path: &Path, mtime: FileTime) -> Result<()> {
filetime::set_file_mtime(path, mtime).map_err(|e| Error::io(path, e))
}
pub fn set_symlink_mtime(path: &Path, mtime: FileTime) -> Result<()> {
filetime::set_symlink_file_times(path, mtime, mtime).map_err(|e| Error::io(path, e))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_parent_escape() {
assert!(check_relative(Path::new("../etc/passwd")).is_err());
assert!(check_relative(Path::new("a/../../b")).is_err());
assert!(check_relative(Path::new("a/b/c")).is_ok());
}
#[test]
fn contained_target_blocks_escape_via_symlink() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("dst");
let outside = tmp.path().join("outside");
std::fs::create_dir_all(&root).unwrap();
std::fs::create_dir_all(&outside).unwrap();
let root_canon = canonical_root(&root).unwrap();
#[cfg(unix)]
{
let link = root.join("escape");
std::os::unix::fs::symlink(&outside, &link).unwrap();
let target = link.join("evil.txt");
assert!(matches!(
contained_target(&root_canon, &target),
Err(Error::Containment(_))
));
}
let ok = root.join("file.txt");
assert!(contained_target(&root_canon, &ok).is_ok());
}
}