use std::fs::File;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use std::path::Path;
#[cfg(unix)]
use colored::Colorize;
use lazy_static::lazy_static;
use tracing::error;
#[cfg(windows)]
use winapi::um::{
processthreadsapi::GetCurrentProcess,
processthreadsapi::OpenProcessToken,
securitybaseapi::GetTokenInformation,
winnt::{HANDLE, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation},
};
#[cfg(test)]
static mut WARNING_PRINTED: bool = false;
fn is_elevated_impl() -> bool {
#[cfg(unix)]
{
unsafe { libc::geteuid() == 0 }
}
#[cfg(windows)]
{
let mut token: HANDLE = std::ptr::null_mut();
if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } == 0 {
return false;
}
let mut elevation: TOKEN_ELEVATION = unsafe { std::mem::zeroed() };
let mut return_length = 0;
let success = unsafe {
GetTokenInformation(
token,
TokenElevation,
&mut elevation as *mut _ as *mut _,
std::mem::size_of::<TOKEN_ELEVATION>() as u32,
&mut return_length,
)
};
if success == 0 {
false
} else {
elevation.TokenIsElevated != 0
}
}
#[cfg(not(any(unix, windows)))]
{
false
}
}
lazy_static! {
static ref IS_ELEVATED: bool = is_elevated_impl();
}
pub fn is_elevated() -> bool {
*IS_ELEVATED
}
#[derive(Debug, Clone, Copy)]
pub enum PrivilegedExecutionContext {
Regular,
Elevated,
}
impl PrivilegedExecutionContext {
pub fn current() -> PrivilegedExecutionContext {
match is_elevated() {
false => PrivilegedExecutionContext::Regular,
true => PrivilegedExecutionContext::Elevated,
}
}
pub fn is_elevated(&self) -> bool {
match self {
PrivilegedExecutionContext::Regular => false,
PrivilegedExecutionContext::Elevated => true,
}
}
pub fn create_dir_all(&self, path: impl AsRef<Path>) -> std::io::Result<()> {
let path = std::env::current_dir()?.join(path);
let path = path.as_path();
let mut root = path;
while !root.exists() {
let Some(pparent) = root.parent() else {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Path {root:?} has no parent."),
));
};
root = pparent;
}
std::fs::create_dir_all(path).inspect_err(|err| {
if err.kind() == std::io::ErrorKind::PermissionDenied {
permission_warning(root, true);
}
})?;
#[cfg(unix)]
if self.is_elevated() {
let root_meta = std::fs::metadata(root)?;
let mut path = path;
while path != root {
std::os::unix::fs::chown(path, Some(root_meta.uid()), Some(root_meta.gid()))?;
let Some(pparent) = path.parent() else {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Path {path:?} has no parent."),
));
};
path = pparent;
}
}
Ok(())
}
pub fn create_file(&self, path: impl AsRef<Path>) -> std::io::Result<File> {
let path = std::env::current_dir()?.join(path);
let path = path.as_path();
let Some(pparent) = path.parent() else {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("Path {path:?} has no parent.")));
};
self.create_dir_all(pparent)?;
#[allow(unused_variables)]
let parent_meta = std::fs::metadata(pparent)?;
#[cfg(unix)]
let exist = path.exists();
let create = || {
std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(path)
.inspect_err(|err| {
if err.kind() == std::io::ErrorKind::PermissionDenied {
permission_warning(path, false);
}
})
};
create()?;
#[cfg(unix)]
if !exist && self.is_elevated() {
std::os::unix::fs::chown(path, Some(parent_meta.uid()), Some(parent_meta.gid()))?;
}
create()
}
}
pub fn create_dir_all(path: impl AsRef<Path>) -> std::io::Result<()> {
PrivilegedExecutionContext::current().create_dir_all(path)
}
pub fn create_file(path: impl AsRef<Path>) -> std::io::Result<File> {
PrivilegedExecutionContext::current().create_file(path)
}
#[allow(unused_variables)]
fn permission_warning(path: &Path, recursive: bool) {
#[cfg(unix)]
{
let username = whoami::username().unwrap_or_else(|_| "unknown".to_string());
let message = format!(
"The process doesn't have correct read-write permission into path {path:?}, please resets
ownership by 'sudo chown{}{} {path:?}'.",
if recursive { " -R " } else { " " },
username
);
eprintln!("{}", message.bright_blue());
}
#[cfg(windows)]
eprintln!(
"The process doesn't have correct read-write permission into path {path:?}, please resets
permission in the Properties dialog box under the Security tab."
);
error!("Permission denied for path {path:?}");
#[cfg(test)]
unsafe {
WARNING_PRINTED = true
};
}
#[cfg(all(test, unix))]
mod test {
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use anyhow::Result;
use super::{PrivilegedExecutionContext, WARNING_PRINTED};
#[test]
#[ignore = "run manually"]
fn test_create_dir_all() -> Result<()> {
let test_path = std::env::var("HF_XET_TEST_PATH")?;
std::env::set_current_dir(test_path)?;
let permission = PrivilegedExecutionContext::current();
let test = Path::new("rootdir/regdir1/regdir2");
assert!(permission.create_dir_all(test).is_err());
unsafe { assert!(WARNING_PRINTED) };
Ok(())
}
#[test]
#[ignore = "run manually"]
fn test_create_dir_all_sudo() -> Result<()> {
let test_path = std::env::var("HF_XET_TEST_PATH")?;
std::env::set_current_dir(test_path)?;
let permission = PrivilegedExecutionContext::current();
let test = Path::new("regdir/regdir1/regdir2");
permission.create_dir_all(test)?;
assert!(test.exists());
assert!(std::fs::metadata(test)?.uid() != 0);
let parent = test.parent().unwrap();
assert!(std::fs::metadata(parent)?.uid() != 0);
Ok(())
}
#[test]
#[ignore = "run manually"]
fn test_create_file() -> Result<()> {
let test_path = std::env::var("HF_XET_TEST_PATH")?;
std::env::set_current_dir(test_path)?;
let permission = PrivilegedExecutionContext::current();
let test1 = Path::new("rootdir/regdir1/regdir2/file");
assert!(permission.create_file(test1).is_err());
unsafe { assert!(WARNING_PRINTED) };
unsafe { WARNING_PRINTED = false };
let test2 = Path::new("rootdir/file");
assert!(permission.create_file(test2).is_err());
unsafe { assert!(WARNING_PRINTED) };
Ok(())
}
#[test]
#[ignore = "run manually"]
fn test_create_file_sudo() -> Result<()> {
let test_path = std::env::var("HF_XET_TEST_PATH")?;
std::env::set_current_dir(test_path)?;
let test = Path::new("regdir/regdir1/regdir2/file");
let permission = PrivilegedExecutionContext::current();
permission.create_file(test)?;
assert!(test.exists());
assert!(std::fs::metadata(test)?.uid() != 0);
let parent = test.parent().unwrap();
assert!(std::fs::metadata(parent)?.uid() != 0);
Ok(())
}
}