#![warn(missing_docs)]
use std::io;
use std::path::Path;
use bitflags::bitflags;
bitflags! {
pub struct AccessMode: u8 {
const EXISTS = 0b0001;
const READ = 0b0010;
const WRITE = 0b0100;
const EXECUTE = 0b1000;
}
}
#[cfg(unix)]
mod imp {
use super::*;
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
use libc::{c_int, faccessat, AT_FDCWD, F_OK, R_OK, W_OK, X_OK};
#[cfg(not(target_os = "android"))]
use libc::AT_EACCESS;
#[cfg(target_os = "android")]
const AT_EACCESS: c_int = 0;
fn eaccess(p: &Path, mode: c_int) -> io::Result<()> {
let path = CString::new(p.as_os_str().as_bytes()).expect("Path can't contain NULL");
unsafe {
if faccessat(AT_FDCWD, path.as_ptr() as *const i8, mode, AT_EACCESS) == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
}
pub fn access(p: &Path, mode: AccessMode) -> io::Result<()> {
let mut imode = 0;
if mode.contains(AccessMode::EXISTS) {
imode |= F_OK;
}
if mode.contains(AccessMode::READ) {
imode |= R_OK;
}
if mode.contains(AccessMode::WRITE) {
imode |= W_OK;
}
if mode.contains(AccessMode::EXECUTE) {
imode |= X_OK;
}
eaccess(p, imode)
}
}
#[cfg(windows)]
mod imp {
use super::*;
use std::os::windows::{ffi::OsStrExt, fs::OpenOptionsExt};
use std::path::Path;
use winapi::shared::minwindef::DWORD;
use winapi::shared::winerror::ERROR_SUCCESS;
use winapi::um::accctrl::SE_FILE_OBJECT;
use winapi::um::aclapi::GetNamedSecurityInfoW;
use winapi::um::handleapi::CloseHandle;
use winapi::um::processthreadsapi::{GetCurrentThread, OpenThreadToken};
use winapi::um::securitybaseapi::{
AccessCheck, GetSidIdentifierAuthority, ImpersonateSelf, IsValidSid, MapGenericMask,
RevertToSelf,
};
use winapi::um::winbase::LocalFree;
use winapi::um::winnt::{
SecurityImpersonation, DACL_SECURITY_INFORMATION, FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE,
FILE_GENERIC_READ, FILE_GENERIC_WRITE, GENERIC_MAPPING, GROUP_SECURITY_INFORMATION, HANDLE,
LABEL_SECURITY_INFORMATION, OWNER_SECURITY_INFORMATION, PACL, PRIVILEGE_SET,
PSECURITY_DESCRIPTOR, PSID, SID_IDENTIFIER_AUTHORITY, TOKEN_DUPLICATE, TOKEN_QUERY,
};
struct SecurityDescriptor {
pub sd: PSECURITY_DESCRIPTOR,
pub owner: PSID,
_group: PSID,
_dacl: PACL,
}
impl Drop for SecurityDescriptor {
fn drop(&mut self) {
if !self.sd.is_null() {
unsafe {
LocalFree(self.sd as *mut _);
}
}
}
}
impl SecurityDescriptor {
fn for_path(p: &Path) -> std::io::Result<SecurityDescriptor> {
let path = std::fs::canonicalize(p)?;
let pathos = path.into_os_string();
let mut pathw: Vec<u16> = Vec::with_capacity(pathos.len() + 1);
pathw.extend(pathos.encode_wide());
pathw.push(0);
let mut sd = std::ptr::null_mut();
let mut owner = std::ptr::null_mut();
let mut group = std::ptr::null_mut();
let mut dacl = std::ptr::null_mut();
let err = unsafe {
GetNamedSecurityInfoW(
pathw.as_ptr(),
SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION
| GROUP_SECURITY_INFORMATION
| DACL_SECURITY_INFORMATION
| LABEL_SECURITY_INFORMATION,
&mut owner,
&mut group,
&mut dacl,
std::ptr::null_mut(),
&mut sd,
)
};
if err == ERROR_SUCCESS {
Ok(SecurityDescriptor {
sd,
owner,
_group: group,
_dacl: dacl,
})
} else {
Err(std::io::Error::last_os_error())
}
}
}
struct ThreadToken(HANDLE);
impl Drop for ThreadToken {
fn drop(&mut self) {
unsafe {
CloseHandle(self.0);
}
}
}
impl ThreadToken {
fn new() -> io::Result<Self> {
unsafe {
if ImpersonateSelf(SecurityImpersonation) == 0 {
return Err(io::Error::last_os_error());
}
let mut token: HANDLE = std::ptr::null_mut();
let err = OpenThreadToken(
GetCurrentThread(),
TOKEN_DUPLICATE | TOKEN_QUERY,
0,
&mut token,
);
RevertToSelf();
if err == 0 {
return Err(io::Error::last_os_error());
}
Ok(Self(token))
}
}
unsafe fn as_handle(&self) -> HANDLE {
self.0
}
}
fn eaccess(p: &Path, mut mode: DWORD) -> io::Result<()> {
let md = p.metadata()?;
if !md.is_dir() {
if mode & FILE_GENERIC_WRITE == FILE_GENERIC_WRITE && md.permissions().readonly() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"File is read only",
));
}
if mode & FILE_GENERIC_EXECUTE == FILE_GENERIC_EXECUTE {
if let Some(ext) = p.extension().and_then(|s| s.to_str()) {
match ext {
"exe" | "com" | "bat" | "cmd" => (),
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"File not executable",
))
}
}
}
}
return std::fs::OpenOptions::new()
.access_mode(mode)
.open(p)
.map(|_| ());
}
let sd = SecurityDescriptor::for_path(p)?;
const SAMBA_UNMAPPED: SID_IDENTIFIER_AUTHORITY = SID_IDENTIFIER_AUTHORITY {
Value: [0, 0, 0, 0, 0, 22],
};
unsafe {
if IsValidSid(sd.owner) != 0
&& (*GetSidIdentifierAuthority(sd.owner)).Value == SAMBA_UNMAPPED.Value
{
return Ok(());
}
}
let token = ThreadToken::new()?;
let mut privileges: PRIVILEGE_SET = PRIVILEGE_SET::default();
let mut granted_access: DWORD = 0;
let mut privileges_length = std::mem::size_of::<PRIVILEGE_SET>() as u32;
let mut result = 0;
let mut mapping = GENERIC_MAPPING {
GenericRead: FILE_GENERIC_READ,
GenericWrite: FILE_GENERIC_WRITE,
GenericExecute: FILE_GENERIC_EXECUTE,
GenericAll: FILE_ALL_ACCESS,
};
unsafe { MapGenericMask(&mut mode, &mut mapping) };
if unsafe {
AccessCheck(
sd.sd,
token.as_handle(),
mode,
&mut mapping as *mut _,
&mut privileges as *mut _,
&mut privileges_length as *mut _,
&mut granted_access as *mut _,
&mut result as *mut _,
) != 0
} {
if result == 0 {
Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"Permission Denied",
))
} else {
Ok(())
}
} else {
Err(io::Error::last_os_error())
}
}
pub fn access(p: &Path, mode: AccessMode) -> io::Result<()> {
let mut imode = 0;
if mode.contains(AccessMode::READ) {
imode |= FILE_GENERIC_READ;
}
if mode.contains(AccessMode::WRITE) {
imode |= FILE_GENERIC_WRITE;
}
if mode.contains(AccessMode::EXECUTE) {
imode |= FILE_GENERIC_EXECUTE;
}
if imode == 0 {
if p.exists() {
Ok(())
} else {
Err(io::Error::new(io::ErrorKind::NotFound, "Not Found"))
}
} else {
eaccess(&p, imode)
}
}
}
#[cfg(not(any(unix, windows)))]
mod imp {
use super::*;
pub fn access(p: &Path, mode: AccessMode) -> io::Result<()> {
if mode.contains(AccessMode::WRITE) {
if std::fs::metadata(p)?.permissions().readonly() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"Path is read only",
));
} else {
return Ok(());
}
}
if p.exists() {
Ok(())
} else {
Err(io::Error::new(io::ErrorKind::NotFound, "Path not found"))
}
}
}
pub trait PathExt {
fn access(&self, mode: AccessMode) -> std::io::Result<()>;
fn readable(&self) -> bool {
self.access(AccessMode::READ).is_ok()
}
fn writable(&self) -> bool {
self.access(AccessMode::WRITE).is_ok()
}
fn executable(&self) -> bool {
self.access(AccessMode::EXECUTE).is_ok()
}
}
impl PathExt for Path {
fn access(&self, mode: AccessMode) -> io::Result<()> {
imp::access(&self, mode)
}
}
#[test]
fn amazing_test_suite() {
let cargotoml = Path::new("Cargo.toml");
assert!(cargotoml.access(AccessMode::EXISTS).is_ok());
assert!(cargotoml.access(AccessMode::READ).is_ok());
assert!(cargotoml
.access(AccessMode::READ | AccessMode::WRITE)
.is_ok());
assert!(cargotoml.readable());
assert!(cargotoml.writable());
#[cfg(unix)]
{
assert!(!cargotoml.executable());
assert!(cargotoml
.access(AccessMode::READ | AccessMode::EXECUTE)
.is_err());
let sh = Path::new("/bin/sh");
assert!(sh.readable());
assert!(!sh.writable());
assert!(sh.executable());
assert!(sh.access(AccessMode::READ | AccessMode::EXECUTE).is_ok());
assert!(sh.access(AccessMode::READ | AccessMode::WRITE).is_err());
}
#[cfg(windows)]
{
assert!(!cargotoml.executable());
assert!(cargotoml
.access(AccessMode::READ | AccessMode::EXECUTE)
.is_err());
let notepad = Path::new("C:\\Windows\\notepad.exe");
assert!(notepad.readable());
assert!(!notepad.writable());
assert!(notepad.executable());
let windows = Path::new("C:\\Windows");
assert!(windows.readable());
assert!(windows.executable());
}
#[cfg(not(any(unix, windows)))]
{
assert!(cargotoml.executable());
}
let missing = Path::new("Cargo.toml from another dimension");
assert_eq!(
missing
.access(AccessMode::EXISTS)
.map_err(|e| e.kind())
.expect_err("File should not exist"),
io::ErrorKind::NotFound
);
assert!(!missing.readable());
assert!(!missing.writable());
assert!(!missing.executable());
}