mirror of https://github.com/helix-editor/helix
Use a temporary file for writes (#9236)
Co-authored-by: Pascal Kuthe <pascalkuthe@pm.me>pull/10022/head^2
parent
a224ee5079
commit
88d455afeb
@ -0,0 +1,459 @@
|
||||
//! From <https://github.com/Freaky/faccess>
|
||||
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
use bitflags::bitflags;
|
||||
|
||||
// Licensed under MIT from faccess
|
||||
bitflags! {
|
||||
/// Access mode flags for `access` function to test for.
|
||||
pub struct AccessMode: u8 {
|
||||
/// Path exists
|
||||
const EXISTS = 0b0001;
|
||||
/// Path can likely be read
|
||||
const READ = 0b0010;
|
||||
/// Path can likely be written to
|
||||
const WRITE = 0b0100;
|
||||
/// Path can likely be executed
|
||||
const EXECUTE = 0b1000;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
use rustix::fs::Access;
|
||||
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||
|
||||
pub fn access(p: &Path, mode: AccessMode) -> io::Result<()> {
|
||||
let mut imode = Access::empty();
|
||||
|
||||
if mode.contains(AccessMode::EXISTS) {
|
||||
imode |= Access::EXISTS;
|
||||
}
|
||||
|
||||
if mode.contains(AccessMode::READ) {
|
||||
imode |= Access::READ_OK;
|
||||
}
|
||||
|
||||
if mode.contains(AccessMode::WRITE) {
|
||||
imode |= Access::WRITE_OK;
|
||||
}
|
||||
|
||||
if mode.contains(AccessMode::EXECUTE) {
|
||||
imode |= Access::EXEC_OK;
|
||||
}
|
||||
|
||||
rustix::fs::access(p, imode)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn chown(p: &Path, uid: Option<u32>, gid: Option<u32>) -> io::Result<()> {
|
||||
let uid = uid.map(|n| unsafe { rustix::fs::Uid::from_raw(n) });
|
||||
let gid = gid.map(|n| unsafe { rustix::fs::Gid::from_raw(n) });
|
||||
rustix::fs::chown(p, uid, gid)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn copy_metadata(from: &Path, to: &Path) -> io::Result<()> {
|
||||
let from_meta = std::fs::metadata(from)?;
|
||||
let to_meta = std::fs::metadata(to)?;
|
||||
let from_gid = from_meta.gid();
|
||||
let to_gid = to_meta.gid();
|
||||
|
||||
let mut perms = from_meta.permissions();
|
||||
perms.set_mode(perms.mode() & 0o0777);
|
||||
if from_gid != to_gid && chown(to, None, Some(from_gid)).is_err() {
|
||||
let new_perms = (perms.mode() & 0o0707) | ((perms.mode() & 0o07) << 3);
|
||||
perms.set_mode(new_perms);
|
||||
}
|
||||
|
||||
std::fs::set_permissions(to, perms)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Licensed under MIT from faccess except for `chown`, `copy_metadata` and `is_acl_inherited`
|
||||
#[cfg(windows)]
|
||||
mod imp {
|
||||
|
||||
use windows_sys::Win32::Foundation::{CloseHandle, LocalFree, ERROR_SUCCESS, HANDLE, PSID};
|
||||
use windows_sys::Win32::Security::Authorization::{
|
||||
GetNamedSecurityInfoW, SetNamedSecurityInfoW, SE_FILE_OBJECT,
|
||||
};
|
||||
use windows_sys::Win32::Security::{
|
||||
AccessCheck, AclSizeInformation, GetAce, GetAclInformation, GetSidIdentifierAuthority,
|
||||
ImpersonateSelf, IsValidAcl, IsValidSid, MapGenericMask, RevertToSelf,
|
||||
SecurityImpersonation, ACCESS_ALLOWED_CALLBACK_ACE, ACL, ACL_SIZE_INFORMATION,
|
||||
DACL_SECURITY_INFORMATION, GENERIC_MAPPING, GROUP_SECURITY_INFORMATION, INHERITED_ACE,
|
||||
LABEL_SECURITY_INFORMATION, OBJECT_SECURITY_INFORMATION, OWNER_SECURITY_INFORMATION,
|
||||
PRIVILEGE_SET, PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR,
|
||||
SID_IDENTIFIER_AUTHORITY, TOKEN_DUPLICATE, TOKEN_QUERY,
|
||||
};
|
||||
use windows_sys::Win32::Storage::FileSystem::{
|
||||
FILE_ACCESS_RIGHTS, FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ,
|
||||
FILE_GENERIC_WRITE,
|
||||
};
|
||||
use windows_sys::Win32::System::Threading::{GetCurrentThread, OpenThreadToken};
|
||||
|
||||
use super::*;
|
||||
|
||||
use std::ffi::c_void;
|
||||
|
||||
use std::os::windows::{ffi::OsStrExt, fs::OpenOptionsExt};
|
||||
|
||||
struct SecurityDescriptor {
|
||||
sd: PSECURITY_DESCRIPTOR,
|
||||
owner: PSID,
|
||||
group: PSID,
|
||||
dacl: *mut ACL,
|
||||
}
|
||||
|
||||
impl Drop for SecurityDescriptor {
|
||||
fn drop(&mut self) {
|
||||
if !self.sd.is_null() {
|
||||
unsafe {
|
||||
LocalFree(self.sd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SecurityDescriptor {
|
||||
fn for_path(p: &Path) -> 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,
|
||||
dacl,
|
||||
})
|
||||
} else {
|
||||
Err(io::Error::last_os_error())
|
||||
}
|
||||
}
|
||||
|
||||
fn is_acl_inherited(&self) -> bool {
|
||||
let mut acl_info: ACL_SIZE_INFORMATION = unsafe { ::core::mem::zeroed() };
|
||||
let acl_info_ptr: *mut c_void = &mut acl_info as *mut _ as *mut c_void;
|
||||
let mut ace: ACCESS_ALLOWED_CALLBACK_ACE = unsafe { ::core::mem::zeroed() };
|
||||
|
||||
unsafe {
|
||||
GetAclInformation(
|
||||
self.dacl,
|
||||
acl_info_ptr,
|
||||
std::mem::size_of_val(&acl_info) as u32,
|
||||
AclSizeInformation,
|
||||
)
|
||||
};
|
||||
|
||||
for i in 0..acl_info.AceCount {
|
||||
let mut ptr = &mut ace as *mut _ as *mut c_void;
|
||||
unsafe { GetAce(self.dacl, i, &mut ptr) };
|
||||
if (ace.Header.AceFlags as u32 & INHERITED_ACE) != 0 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn descriptor(&self) -> &PSECURITY_DESCRIPTOR {
|
||||
&self.sd
|
||||
}
|
||||
|
||||
fn owner(&self) -> &PSID {
|
||||
&self.owner
|
||||
}
|
||||
}
|
||||
|
||||
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 token: *mut HANDLE = std::ptr::null_mut();
|
||||
let err =
|
||||
OpenThreadToken(GetCurrentThread(), TOKEN_DUPLICATE | TOKEN_QUERY, 0, token);
|
||||
|
||||
RevertToSelf();
|
||||
|
||||
if err == 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
Ok(Self(*token))
|
||||
}
|
||||
}
|
||||
|
||||
fn as_handle(&self) -> &HANDLE {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
// Based roughly on Tcl's NativeAccess()
|
||||
// https://github.com/tcltk/tcl/blob/2ee77587e4dc2150deb06b48f69db948b4ab0584/win/tclWinFile.c
|
||||
fn eaccess(p: &Path, mut mode: FILE_ACCESS_RIGHTS) -> io::Result<()> {
|
||||
let md = p.metadata()?;
|
||||
|
||||
if !md.is_dir() {
|
||||
// Read Only is ignored for directories
|
||||
if mode & FILE_GENERIC_WRITE == FILE_GENERIC_WRITE && md.permissions().readonly() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
"File is read only",
|
||||
));
|
||||
}
|
||||
|
||||
// If it doesn't have the correct extension it isn't executable
|
||||
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)?;
|
||||
|
||||
// Unmapped Samba users are assigned a top level authority of 22
|
||||
// ACL tests are likely to be misleading
|
||||
const SAMBA_UNMAPPED: SID_IDENTIFIER_AUTHORITY = SID_IDENTIFIER_AUTHORITY {
|
||||
Value: [0, 0, 0, 0, 0, 22],
|
||||
};
|
||||
unsafe {
|
||||
let owner = sd.owner();
|
||||
if IsValidSid(*owner) != 0
|
||||
&& (*GetSidIdentifierAuthority(*owner)).Value == SAMBA_UNMAPPED.Value
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let token = ThreadToken::new()?;
|
||||
|
||||
let mut privileges: PRIVILEGE_SET = unsafe { std::mem::zeroed() };
|
||||
let mut granted_access: u32 = 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.descriptor(),
|
||||
*token.as_handle(),
|
||||
mode,
|
||||
&mut mapping,
|
||||
&mut privileges,
|
||||
&mut privileges_length,
|
||||
&mut granted_access,
|
||||
&mut result,
|
||||
)
|
||||
} != 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)
|
||||
}
|
||||
}
|
||||
|
||||
fn chown(p: &Path, sd: SecurityDescriptor) -> io::Result<()> {
|
||||
let path = std::fs::canonicalize(p)?;
|
||||
let pathos = path.as_os_str();
|
||||
let mut pathw = Vec::with_capacity(pathos.len() + 1);
|
||||
pathw.extend(pathos.encode_wide());
|
||||
pathw.push(0);
|
||||
|
||||
let mut owner = std::ptr::null_mut();
|
||||
let mut group = std::ptr::null_mut();
|
||||
let mut dacl = std::ptr::null();
|
||||
|
||||
let mut si = OBJECT_SECURITY_INFORMATION::default();
|
||||
if unsafe { IsValidSid(sd.owner) } != 0 {
|
||||
si |= OWNER_SECURITY_INFORMATION;
|
||||
owner = sd.owner;
|
||||
}
|
||||
|
||||
if unsafe { IsValidSid(sd.group) } != 0 {
|
||||
si |= GROUP_SECURITY_INFORMATION;
|
||||
group = sd.group;
|
||||
}
|
||||
|
||||
if unsafe { IsValidAcl(sd.dacl) } != 0 {
|
||||
si |= DACL_SECURITY_INFORMATION;
|
||||
if !sd.is_acl_inherited() {
|
||||
si |= PROTECTED_DACL_SECURITY_INFORMATION;
|
||||
}
|
||||
dacl = sd.dacl as *const _;
|
||||
}
|
||||
|
||||
let err = unsafe {
|
||||
SetNamedSecurityInfoW(
|
||||
pathw.as_ptr(),
|
||||
SE_FILE_OBJECT,
|
||||
si,
|
||||
owner,
|
||||
group,
|
||||
dacl,
|
||||
std::ptr::null(),
|
||||
)
|
||||
};
|
||||
|
||||
if err == ERROR_SUCCESS {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::last_os_error())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_metadata(from: &Path, to: &Path) -> io::Result<()> {
|
||||
let sd = SecurityDescriptor::for_path(from)?;
|
||||
chown(to, sd)?;
|
||||
|
||||
let meta = std::fs::metadata(from)?;
|
||||
let perms = meta.permissions();
|
||||
|
||||
std::fs::set_permissions(to, perms)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Licensed under MIT from faccess except for `copy_metadata`
|
||||
#[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 fn copy_metadata(from: &path, to: &Path) -> io::Result<()> {
|
||||
let meta = std::fs::metadata(from)?;
|
||||
let perms = meta.permissions();
|
||||
std::fs::set_permissions(to, perms)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn readonly(p: &Path) -> bool {
|
||||
match imp::access(p, AccessMode::WRITE) {
|
||||
Ok(_) => false,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => false,
|
||||
Err(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_metadata(from: &Path, to: &Path) -> io::Result<()> {
|
||||
imp::copy_metadata(from, to)
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
pub mod env;
|
||||
pub mod faccess;
|
||||
pub mod path;
|
||||
pub mod rope;
|
||||
|
Loading…
Reference in New Issue