mod acl;
use crate::{Access, Error, files::read_into, output_parent};
use acl::{Descriptor, Grant, User, last_error, refuse_untrusted, wide};
use std::{
fs::{File, OpenOptions},
io::{self, Write},
os::windows::{
fs::{MetadataExt, OpenOptionsExt},
io::FromRawHandle,
},
path::Path,
ptr,
};
use windows_sys::Win32::{
Foundation::INVALID_HANDLE_VALUE,
Storage::FileSystem::{
CREATE_NEW, CreateDirectoryW, CreateFileW, FILE_ATTRIBUTE_NORMAL,
FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT,
FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, MOVEFILE_REPLACE_EXISTING,
MOVEFILE_WRITE_THROUGH, MoveFileExW,
},
};
use zeroize::Zeroizing;
struct Policy;
impl Policy {
const DIRECTORY_INHERIT: &str = "OICI";
const FILE_INHERIT: &str = "";
const SHARED_READ: &str = "(A;;FR;;;BU)";
const READ_CONTROL: u32 = 0x0002_0000;
const GENERIC_READ: u32 = 0x8000_0000;
const GENERIC_WRITE: u32 = 0x4000_0000;
}
fn private_sddl(inherit: &str) -> Result<String, io::Error> {
let user = User::current()?.sddl()?;
Ok(["OW", "SY", "BA", user.as_str()]
.iter()
.fold(String::from("D:P"), |sddl, sid| {
sddl + &format!("(A;{inherit};FA;;;{sid})")
}))
}
pub fn create_private_directory(path: &Path) -> Result<(), io::Error> {
let descriptor = Descriptor::parse(&private_sddl(Policy::DIRECTORY_INHERIT)?)?;
let attributes = descriptor.attributes()?;
let path = wide(path.as_os_str());
match unsafe { CreateDirectoryW(path.as_ptr(), &attributes) } {
0 => Err(last_error()),
_ => Ok(()),
}
}
pub fn create_file(path: &Path, access: Access) -> Result<File, io::Error> {
let private = private_sddl(Policy::FILE_INHERIT)?;
let descriptor = Descriptor::parse(&match access {
Access::Private => private,
Access::Shared => private + Policy::SHARED_READ,
})?;
let attributes = descriptor.attributes()?;
let path = wide(path.as_os_str());
let handle = unsafe {
CreateFileW(
path.as_ptr(),
Policy::GENERIC_READ | Policy::GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
&attributes,
CREATE_NEW,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT,
ptr::null_mut(),
)
};
match handle {
INVALID_HANDLE_VALUE => Err(last_error()),
_ => Ok(unsafe { File::from_raw_handle(handle) }),
}
}
pub(crate) fn write_atomic(path: &Path, bytes: &[u8], replace: bool) -> Result<(), Error> {
let parent = output_parent(path);
let (mut file, mut temporary) = tempfile::Builder::new()
.make_in(parent, |candidate| create_file(candidate, Access::Private))?
.into_parts();
file.write_all(bytes)?;
file.sync_all()?;
drop(file);
let flags = match replace {
true => MOVEFILE_WRITE_THROUGH | MOVEFILE_REPLACE_EXISTING,
false => MOVEFILE_WRITE_THROUGH,
};
let from = wide(temporary.as_os_str());
let to = wide(path.as_os_str());
if unsafe { MoveFileExW(from.as_ptr(), to.as_ptr(), flags) } == 0 {
return Err(Error::Context {
message: format!("move into {}", path.display()),
cause: Box::new(Error::Io(last_error())),
});
}
temporary.disable_cleanup(true);
match sync_directory(parent) {
Ok(()) => {}
Err(cause) => tracing::warn!(
target: "urma_io",
error = %cause,
directory = %parent.display(),
"file landed; directory flush unsupported here"
),
}
Ok(())
}
pub fn sync_directory(path: &Path) -> Result<(), io::Error> {
OpenOptions::new()
.write(true)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
.open(path)?
.sync_all()
}
fn open_regular(path: &Path) -> Result<File, Error> {
let file = OpenOptions::new()
.read(true)
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)?;
let metadata = file.metadata()?;
if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 || !metadata.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"only regular input files are supported",
)
.into());
}
Ok(file)
}
fn read_checked(path: &Path, limit: usize, grant: Grant) -> Result<Zeroizing<Vec<u8>>, Error> {
let file = open_regular(path)?;
refuse_untrusted(&file, grant)?;
let mut bytes = Zeroizing::new(Vec::new());
read_into(file, limit, &mut bytes)?;
Ok(bytes)
}
pub fn read_regular(path: &Path, limit: usize) -> Result<Zeroizing<Vec<u8>>, Error> {
let file = open_regular(path)?;
let mut bytes = Zeroizing::new(Vec::new());
read_into(file, limit, &mut bytes)?;
Ok(bytes)
}
pub fn read_private(path: &Path, limit: usize) -> Result<Zeroizing<Vec<u8>>, Error> {
read_checked(path, limit, Grant::Any)
}
pub fn read_shared(path: &Path, limit: usize) -> Result<Zeroizing<Vec<u8>>, Error> {
read_checked(path, limit, Grant::Write)
}
pub fn open_private_append(path: &Path) -> Result<File, Error> {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Storage::FileSystem::{
BY_HANDLE_FILE_INFORMATION, FILE_APPEND_DATA, GetFileInformationByHandle,
};
let file = OpenOptions::new()
.access_mode(FILE_APPEND_DATA | Policy::READ_CONTROL)
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)?;
let mut info = BY_HANDLE_FILE_INFORMATION::default();
if unsafe { GetFileInformationByHandle(file.as_raw_handle(), &mut info) } == 0 {
return Err(last_error().into());
}
let metadata = file.metadata()?;
if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 || !metadata.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"only regular private files can be appended to",
)
.into());
}
if info.nNumberOfLinks != 1 {
return Err(Error::PrivatePermissions);
}
refuse_untrusted(&file, Grant::Any)?;
Ok(file)
}
pub fn read_admin(path: &Path, limit: usize) -> Result<Zeroizing<Vec<u8>>, Error> {
read_checked(path, limit, Grant::Admin)
}
pub fn check_admin(path: &Path) -> Result<(), Error> {
let entry = OpenOptions::new()
.access_mode(Policy::READ_CONTROL)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)?;
if entry.metadata()?.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"administrator-owned paths must not be reparse points",
)
.into());
}
refuse_untrusted(&entry, Grant::Admin)
}