use std::ffi::{c_void, OsStr};
use std::os::windows::ffi::OsStrExt;
use std::path::{Path, PathBuf};
const SE_FILE_OBJECT: u32 = 1;
const DACL_SECURITY_INFORMATION: u32 = 0x0000_0004;
const FILE_ADD_FILE: u32 = 0x0000_0002;
const FILE_ADD_SUBDIRECTORY: u32 = 0x0000_0004;
const NEW_ENTRIES: u32 = FILE_ADD_FILE | FILE_ADD_SUBDIRECTORY;
const ACL_REVISION: u32 = 2;
const ACCESS_DENIED_ACE_TYPE: u8 = 1;
const ACL_SIZE_INFORMATION: u32 = 2;
const MAXDWORD: u32 = 0xFFFF_FFFF;
const ERROR_SUCCESS: u32 = 0;
#[repr(C, align(4))]
struct Sid([u8; 12]);
static EVERYONE: Sid = Sid([1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]);
#[repr(C)]
struct AceHeader {
ace_type: u8,
ace_flags: u8,
size: u16,
}
#[repr(C)]
#[derive(Default)]
struct AclSizeInformation {
ace_count: u32,
bytes_in_use: u32,
bytes_free: u32,
}
#[link(name = "advapi32")]
extern "system" {
fn GetNamedSecurityInfoW(
object_name: *const u16,
object_type: u32,
security_information: u32,
owner: *mut *mut c_void,
group: *mut *mut c_void,
dacl: *mut *mut c_void,
sacl: *mut *mut c_void,
security_descriptor: *mut *mut c_void,
) -> u32;
fn SetNamedSecurityInfoW(
object_name: *mut u16,
object_type: u32,
security_information: u32,
owner: *mut c_void,
group: *mut c_void,
dacl: *mut c_void,
sacl: *mut c_void,
) -> u32;
fn GetAclInformation(
acl: *mut c_void,
information: *mut c_void,
length: u32,
class: u32,
) -> i32;
fn InitializeAcl(acl: *mut c_void, length: u32, revision: u32) -> i32;
fn GetAce(acl: *mut c_void, index: u32, ace: *mut *mut c_void) -> i32;
fn AddAce(
acl: *mut c_void,
revision: u32,
starting_index: u32,
ace_list: *const c_void,
ace_list_length: u32,
) -> i32;
fn AddAccessDeniedAceEx(
acl: *mut c_void,
revision: u32,
flags: u32,
mask: u32,
sid: *const c_void,
) -> i32;
}
#[link(name = "kernel32")]
extern "system" {
fn LocalFree(memory: *mut c_void) -> *mut c_void;
}
pub struct Narrowing {
applied: Vec<Vec<u16>>,
}
impl Narrowing {
pub fn directories(&self) -> usize {
self.applied.len()
}
}
impl Drop for Narrowing {
fn drop(&mut self) {
for path in &mut self.applied {
let _ = rewrite(path, Ace::Remove);
}
}
}
pub fn refuse_new_entries(directories: &[PathBuf]) -> (Narrowing, Vec<String>) {
let mut applied = Vec::new();
let mut warnings = Vec::new();
for directory in directories {
let mut wide = wide(directory);
match rewrite(&mut wide, Ace::Add) {
Ok(true) => applied.push(wide),
Ok(false) => warnings.push(format!(
"{} already grants or denies Everyone, so new files inside it are \
not refused — Ralon will not rewrite an ACL it did not write",
directory.display()
)),
Err(code) => warnings.push(format!(
"could not refuse new files inside {} (Windows error {code}); \
existing files there are still locked",
directory.display()
)),
}
}
(Narrowing { applied }, warnings)
}
pub fn leftovers(directories: &[PathBuf]) -> Vec<PathBuf> {
directories
.iter()
.filter(|directory| refuses_new_entries(directory))
.cloned()
.collect()
}
pub fn clear(directories: &[PathBuf]) -> Vec<PathBuf> {
let mut cleared = Vec::new();
for directory in directories {
if !refuses_new_entries(directory) {
continue;
}
let _ = rewrite(&mut wide(directory), Ace::Remove);
if !refuses_new_entries(directory) {
cleared.push(directory.clone());
}
}
cleared
}
enum Ace {
Add,
Remove,
}
fn rewrite(wide: &mut [u16], change: Ace) -> Result<bool, u32> {
let (descriptor, dacl) = read_dacl(wide)?;
let result = build(wide, dacl, change);
unsafe { LocalFree(descriptor) };
result
}
fn build(wide: &mut [u16], dacl: *mut c_void, change: Ace) -> Result<bool, u32> {
let existing = aces(dacl);
if matches!(change, Ace::Add)
&& existing
.iter()
.any(|ace| names_everyone(*ace) && !is_ours(*ace))
{
return Ok(false);
}
let size = bytes_in_use(dacl) + 64;
let mut buffer = vec![0u32; (size as usize).div_ceil(4)];
let acl = buffer.as_mut_ptr() as *mut c_void;
if unsafe { InitializeAcl(acl, size, ACL_REVISION) } == 0 {
return Err(last_error());
}
if matches!(change, Ace::Add)
&& unsafe {
AddAccessDeniedAceEx(
acl,
ACL_REVISION,
0, NEW_ENTRIES,
std::ptr::addr_of!(EVERYONE) as *const c_void,
)
} == 0
{
return Err(last_error());
}
for ace in existing {
if is_ours(ace) {
continue;
}
let header = unsafe { &*(ace as *const AceHeader) };
if unsafe { AddAce(acl, ACL_REVISION, MAXDWORD, ace, u32::from(header.size)) } == 0 {
return Err(last_error());
}
}
let code = unsafe {
SetNamedSecurityInfoW(
wide.as_mut_ptr(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
std::ptr::null_mut(),
acl,
std::ptr::null_mut(),
)
};
if code == ERROR_SUCCESS {
Ok(true)
} else {
Err(code)
}
}
fn refuses_new_entries(directory: &Path) -> bool {
let Ok((descriptor, dacl)) = read_dacl(&wide(directory)) else {
return false;
};
let refuses = aces(dacl).into_iter().any(is_ours);
unsafe { LocalFree(descriptor) };
refuses
}
fn read_dacl(wide: &[u16]) -> Result<(*mut c_void, *mut c_void), u32> {
let mut dacl: *mut c_void = std::ptr::null_mut();
let mut descriptor: *mut c_void = std::ptr::null_mut();
let code = unsafe {
GetNamedSecurityInfoW(
wide.as_ptr(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut dacl,
std::ptr::null_mut(),
&mut descriptor,
)
};
if code != ERROR_SUCCESS {
return Err(code);
}
Ok((descriptor, dacl))
}
fn aces(dacl: *mut c_void) -> Vec<*mut c_void> {
if dacl.is_null() {
return Vec::new();
}
let mut found = Vec::new();
for index in 0..count(dacl) {
let mut ace: *mut c_void = std::ptr::null_mut();
if unsafe { GetAce(dacl, index, &mut ace) } != 0 && !ace.is_null() {
found.push(ace);
}
}
found
}
fn size_information(dacl: *mut c_void) -> AclSizeInformation {
let mut information = AclSizeInformation::default();
if dacl.is_null() {
return information;
}
unsafe {
GetAclInformation(
dacl,
std::ptr::addr_of_mut!(information) as *mut c_void,
std::mem::size_of::<AclSizeInformation>() as u32,
ACL_SIZE_INFORMATION,
)
};
information
}
fn count(dacl: *mut c_void) -> u32 {
size_information(dacl).ace_count
}
fn bytes_in_use(dacl: *mut c_void) -> u32 {
size_information(dacl).bytes_in_use.max(64)
}
fn is_ours(ace: *mut c_void) -> bool {
let header = unsafe { &*(ace as *const AceHeader) };
if header.ace_type != ACCESS_DENIED_ACE_TYPE {
return false;
}
mask(ace) == NEW_ENTRIES && names_everyone(ace)
}
fn mask(ace: *mut c_void) -> u32 {
unsafe { std::ptr::read_unaligned(ace.byte_add(4) as *const u32) }
}
fn names_everyone(ace: *mut c_void) -> bool {
let header = unsafe { &*(ace as *const AceHeader) };
if usize::from(header.size) < 8 + EVERYONE.0.len() {
return false;
}
let sid = unsafe { std::slice::from_raw_parts(ace.byte_add(8) as *const u8, EVERYONE.0.len()) };
sid == EVERYONE.0
}
fn last_error() -> u32 {
std::io::Error::last_os_error().raw_os_error().unwrap_or(0) as u32
}
fn wide(path: &Path) -> Vec<u16> {
OsStr::new(path)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}