mod process;
mod security;
use std::{
ffi::{OsStr, OsString},
fs::{File, OpenOptions},
future::Future,
io, mem,
os::windows::{
ffi::{OsStrExt, OsStringExt},
fs::{MetadataExt, OpenOptionsExt},
io::{AsRawHandle, FromRawHandle},
},
path::{Component, Path, PathBuf, Prefix},
process::ExitStatus,
ptr, slice,
};
use tokio::signal::windows::{ctrl_break, ctrl_c, ctrl_close};
use windows_sys::{
Wdk::{
Foundation::OBJECT_ATTRIBUTES,
Storage::FileSystem::{
FILE_OPEN, FILE_OPEN_REPARSE_POINT, FILE_SYNCHRONOUS_IO_NONALERT, NtCreateFile,
},
},
Win32::{
Foundation::{
ERROR_NO_MORE_FILES, HANDLE, INVALID_HANDLE_VALUE, OBJ_CASE_INSENSITIVE,
OBJ_DONT_REPARSE, RtlNtStatusToDosError, STATUS_INVALID_PARAMETER, UNICODE_STRING,
},
Storage::FileSystem::{
FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_REPARSE_POINT, FILE_ATTRIBUTE_TAG_INFO,
FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_GENERIC_READ,
FILE_ID_BOTH_DIR_INFO, FILE_READ_ATTRIBUTES, FILE_SHARE_DELETE, FILE_SHARE_READ,
FILE_SHARE_WRITE, FileAttributeTagInfo, FileIdBothDirectoryInfo,
FileIdBothDirectoryRestartInfo, GetFileInformationByHandleEx, SYNCHRONIZE,
},
System::{
Console::{
CONSOLE_MODE, ENABLE_ECHO_INPUT, GetConsoleMode, GetStdHandle, STD_INPUT_HANDLE,
SetConsoleMode,
},
IO::IO_STATUS_BLOCK,
SystemInformation::{ComputerNameDnsHostname, GetComputerNameExW},
},
},
};
use super::FileKind;
pub(crate) use process::{Child, process_start_time, spawn_detached, spawn_gated};
pub(crate) use security::{
create_private_dir, create_private_dir_all, is_private_dir, is_private_file, private_options,
private_tempdir_in, restrict_file,
};
const NAME_SURROGATE: u32 = 0x2000_0000;
pub(crate) fn sync_dir(_path: &Path) -> io::Result<()> {
Ok(())
}
pub(crate) fn open_no_follow(options: &mut OpenOptions, path: &Path) -> io::Result<File> {
let file = options
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)?;
if file.metadata()?.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(reparse_point());
}
Ok(file)
}
pub(crate) fn open_nonblocking(path: &Path) -> io::Result<File> {
OpenOptions::new()
.read(true)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
.open(path)
}
pub(crate) fn canonicalize(path: &Path) -> io::Result<PathBuf> {
let path = std::fs::canonicalize(path)?;
Ok(plain(&path).unwrap_or(path))
}
fn plain(path: &Path) -> Option<PathBuf> {
let mut components = path.components();
let Some(Component::Prefix(prefix)) = components.next() else {
return None;
};
let mut plain = match prefix.kind() {
Prefix::VerbatimDisk(drive) => OsString::from(format!("{}:", char::from(drive))),
Prefix::VerbatimUNC(server, share) => {
let mut plain = OsString::from(r"\\");
plain.push(server);
plain.push(r"\");
plain.push(share);
plain
}
_ => return None,
};
if components.next() != Some(Component::RootDir) {
return None;
}
plain.push(r"\");
for (index, component) in components.enumerate() {
let Component::Normal(name) = component else {
return None;
};
if !plain_name(name) {
return None;
}
if index > 0 {
plain.push(r"\");
}
plain.push(name);
}
(plain.encode_wide().count() < 260).then(|| PathBuf::from(plain))
}
fn plain_name(name: &OsStr) -> bool {
let Some(name) = name.to_str() else {
return false;
};
let stem = name
.split('.')
.next()
.unwrap_or_default()
.trim_end_matches(' ');
let device = match stem.to_ascii_uppercase().as_str() {
"CON" | "PRN" | "AUX" | "NUL" | "CONIN$" | "CONOUT$" => true,
port => {
(port.starts_with("COM") || port.starts_with("LPT"))
&& port[3..].chars().count() == 1
&& port[3..]
.chars()
.all(|digit| "0123456789¹²³".contains(digit))
}
};
!name.is_empty()
&& name.len() <= 255
&& !name.ends_with(['.', ' '])
&& !name
.chars()
.any(|c| c < ' ' || matches!(c, '<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*'))
&& !device
}
pub(crate) fn open_directory(path: &Path) -> io::Result<File> {
OpenOptions::new()
.read(true)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
.open(path)
}
pub(crate) struct EntryName(Vec<u16>);
impl EntryName {
pub fn new(name: &OsStr) -> Option<Self> {
let name: Vec<u16> = name.encode_wide().collect();
let dots = |count| name.len() == count && name.iter().all(|&unit| unit == u16::from(b'.'));
let valid = !name.is_empty()
&& name.len() <= usize::from(u16::MAX / 2)
&& !dots(1)
&& !dots(2)
&& !name
.iter()
.any(|&unit| [0, b'/', b':', b'\\'].map(u16::from).contains(&unit));
valid.then_some(Self(name))
}
}
pub(crate) fn open_entry(directory: &File, name: &EntryName) -> io::Result<File> {
let file = open_relative(directory, &name.0, FILE_GENERIC_READ)?;
if attribute_tag(&file)?.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(reparse_point());
}
Ok(file)
}
pub(crate) fn entry_kind(directory: &File, name: &OsStr) -> io::Result<FileKind> {
let name = EntryName::new(name).ok_or(io::ErrorKind::InvalidInput)?;
let file = open_relative(directory, &name.0, FILE_READ_ATTRIBUTES | SYNCHRONIZE)?;
let info = attribute_tag(&file)?;
Ok(kind(info.FileAttributes, info.ReparseTag))
}
fn open_relative(directory: &File, name: &[u16], access: u32) -> io::Result<File> {
let length = u16::try_from(mem::size_of_val(name)).map_err(|_| io::ErrorKind::InvalidInput)?;
let name = UNICODE_STRING {
Length: length,
MaximumLength: length,
Buffer: name.as_ptr().cast_mut(),
};
let mut attributes = OBJECT_ATTRIBUTES {
Length: mem::size_of::<OBJECT_ATTRIBUTES>() as u32,
RootDirectory: directory.as_raw_handle(),
ObjectName: &name,
Attributes: OBJ_CASE_INSENSITIVE | OBJ_DONT_REPARSE,
..Default::default()
};
let open = |attributes: &OBJECT_ATTRIBUTES, handle: &mut HANDLE| {
let mut status = IO_STATUS_BLOCK::default();
unsafe {
NtCreateFile(
handle,
access,
attributes,
&mut status,
ptr::null(),
0,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_OPEN,
FILE_OPEN_REPARSE_POINT | FILE_SYNCHRONOUS_IO_NONALERT,
ptr::null(),
0,
)
}
};
let mut handle = ptr::null_mut();
let mut status = open(&attributes, &mut handle);
if status == STATUS_INVALID_PARAMETER {
attributes.Attributes = OBJ_CASE_INSENSITIVE;
status = open(&attributes, &mut handle);
}
if status < 0 {
let error = unsafe { RtlNtStatusToDosError(status) };
return Err(io::Error::from_raw_os_error(error as i32));
}
Ok(unsafe { File::from_raw_handle(handle) })
}
fn attribute_tag(file: &File) -> io::Result<FILE_ATTRIBUTE_TAG_INFO> {
let mut info = FILE_ATTRIBUTE_TAG_INFO::default();
let ok = unsafe {
GetFileInformationByHandleEx(
file.as_raw_handle(),
FileAttributeTagInfo,
(&raw mut info).cast(),
mem::size_of::<FILE_ATTRIBUTE_TAG_INFO>() as u32,
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
Ok(info)
}
fn kind(attributes: u32, reparse_tag: u32) -> FileKind {
if attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
if reparse_tag & NAME_SURROGATE != 0 {
FileKind::Symlink
} else {
FileKind::Other
}
} else if attributes & FILE_ATTRIBUTE_DIRECTORY != 0 {
FileKind::Directory
} else {
FileKind::File
}
}
fn reparse_point() -> io::Error {
io::Error::new(
io::ErrorKind::InvalidInput,
"refusing a symbolic link, junction or other reparse point",
)
}
pub(crate) fn read_dir(
directory: &File,
) -> io::Result<impl Iterator<Item = io::Result<DirEntry>> + '_> {
Ok(ReadDir {
directory,
buffer: vec![0; 8 * 1024],
next: None,
restart: true,
done: false,
})
}
pub(crate) struct DirEntry {
name: OsString,
kind: FileKind,
}
impl DirEntry {
pub fn file_name(&self) -> OsString {
self.name.clone()
}
pub fn file_type(&self) -> io::Result<FileKind> {
Ok(self.kind)
}
}
struct ReadDir<'a> {
directory: &'a File,
buffer: Vec<u64>,
next: Option<usize>,
restart: bool,
done: bool,
}
impl Iterator for ReadDir<'_> {
type Item = io::Result<DirEntry>;
fn next(&mut self) -> Option<Self::Item> {
loop {
if let Some(offset) = self.next {
match self.record(offset) {
Ok(Some(entry)) => return Some(Ok(entry)),
Ok(None) => continue,
Err(error) => return Some(Err(error)),
}
}
if self.done {
return None;
}
let class = if self.restart {
FileIdBothDirectoryRestartInfo
} else {
FileIdBothDirectoryInfo
};
self.restart = false;
let ok = unsafe {
GetFileInformationByHandleEx(
self.directory.as_raw_handle(),
class,
self.buffer.as_mut_ptr().cast(),
mem::size_of_val(self.buffer.as_slice()) as u32,
)
};
if ok == 0 {
self.done = true;
let error = io::Error::last_os_error();
if error.raw_os_error() == Some(ERROR_NO_MORE_FILES as i32) {
return None;
}
return Some(Err(error));
}
self.next = Some(0);
}
}
}
impl ReadDir<'_> {
fn record(&mut self, offset: usize) -> io::Result<Option<DirEntry>> {
let size = mem::size_of_val(self.buffer.as_slice());
let header = mem::offset_of!(FILE_ID_BOTH_DIR_INFO, FileName);
if !offset.is_multiple_of(8) || offset + header > size {
self.done = true;
self.next = None;
return Err(io::Error::other("malformed directory listing"));
}
let info = unsafe {
self.buffer
.as_ptr()
.cast::<u8>()
.add(offset)
.cast::<FILE_ID_BOTH_DIR_INFO>()
};
let (next, attributes, tag, length) = unsafe {
(
(*info).NextEntryOffset as usize,
(*info).FileAttributes,
(*info).EaSize,
(*info).FileNameLength as usize,
)
};
if offset + header + length > size {
self.done = true;
self.next = None;
return Err(io::Error::other("malformed directory listing"));
}
let name = unsafe {
slice::from_raw_parts((&raw const (*info).FileName).cast::<u16>(), length / 2)
};
self.next = (next != 0).then_some(offset + next);
let dot = u16::from(b'.');
if name == [dot] || name == [dot, dot] {
return Ok(None);
}
Ok(Some(DirEntry {
name: OsString::from_wide(name),
kind: kind(attributes, tag),
}))
}
}
pub(crate) fn is_executable(path: &Path) -> bool {
let file = |path: &Path| path.metadata().is_ok_and(|m| m.is_file());
file(path) || (path.extension().is_none() && file(&path.with_extension("exe")))
}
pub(crate) const DEFAULT_EDITOR: &str = "notepad";
pub(crate) fn editor(editor: &str, file: &Path) -> tokio::process::Command {
let mut command = tokio::process::Command::new("cmd");
command.raw_arg(format!("/d /s /c \"{editor} \"{}\"\"", file.display()));
command
}
pub(crate) fn exit_signal(_status: &ExitStatus) -> Option<i32> {
None
}
pub(crate) fn host_name() -> Option<String> {
let mut size = 0;
unsafe { GetComputerNameExW(ComputerNameDnsHostname, ptr::null_mut(), &mut size) };
let mut buffer = vec![0_u16; size as usize];
if unsafe { GetComputerNameExW(ComputerNameDnsHostname, buffer.as_mut_ptr(), &mut size) } == 0 {
return None;
}
buffer.truncate(size as usize);
Some(String::from_utf16_lossy(&buffer))
}
pub(crate) fn stop_requested() -> io::Result<impl Future<Output = ()> + Send + 'static> {
let mut interrupt = ctrl_c()?;
let mut brk = ctrl_break()?;
let mut close = ctrl_close()?;
Ok(async move {
tokio::select! {
_ = interrupt.recv() => {},
_ = brk.recv() => {},
_ = close.recv() => {},
}
})
}
pub(crate) struct HiddenInput {
console: HANDLE,
mode: CONSOLE_MODE,
}
impl HiddenInput {
pub fn new() -> io::Result<Self> {
let console = unsafe { GetStdHandle(STD_INPUT_HANDLE) };
if console.is_null() || console == INVALID_HANDLE_VALUE {
return Err(io::Error::last_os_error());
}
let mut mode = 0;
if unsafe { GetConsoleMode(console, &mut mode) } == 0 {
return Err(io::Error::last_os_error());
}
if unsafe { SetConsoleMode(console, mode & !ENABLE_ECHO_INPUT) } == 0 {
return Err(io::Error::last_os_error());
}
Ok(Self { console, mode })
}
}
impl Drop for HiddenInput {
fn drop(&mut self) {
unsafe { SetConsoleMode(self.console, self.mode) };
}
}
#[cfg(test)]
mod tests {
use std::process::Stdio;
use super::*;
#[tokio::test]
async fn the_editor_runs_through_cmd_and_gets_the_file_as_one_word() {
let directory = tempfile::tempdir().unwrap();
let file = directory.path().join("fields file.json");
std::fs::write(&file, "{}").unwrap();
let edited = directory.path().join("edited.json");
std::fs::write(&edited, r#"{"approved":true}"#).unwrap();
let command_line = format!("copy /y \"{}\"", edited.display());
let status = editor(&command_line, &file)
.stdout(Stdio::null())
.status()
.await
.unwrap();
assert!(status.success());
assert_eq!(
std::fs::read_to_string(&file).unwrap(),
r#"{"approved":true}"#
);
}
}