edit/src/sys/windows.rs
Leonard Hecker b277a1e67b
Make the ICU SONAME configurable (#495)
This PR was tested on Ubuntu with:
```
EDIT_CFG_ICUUC_SONAME=libicuuc.so.74
EDIT_CFG_ICUI18N_SONAME=libicui18n.so.74
EDIT_CFG_ICU_RENAMING_VERSION=74
cargo build --config .cargo/release.toml --release
```

Search & Replace now works flawlessly. I hope that package maintainers
will be able to make use of this when ingesting future versions of Edit.

Closes #172
2025-06-19 23:26:05 +02:00

771 lines
25 KiB
Rust

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::ffi::{OsString, c_char, c_void};
use std::fmt::Write as _;
use std::fs::{self, File};
use std::mem::MaybeUninit;
use std::os::windows::io::{AsRawHandle as _, FromRawHandle};
use std::path::{Path, PathBuf};
use std::ptr::{self, NonNull, null, null_mut};
use std::{mem, time};
use windows_sys::Win32::Storage::FileSystem;
use windows_sys::Win32::System::Diagnostics::Debug;
use windows_sys::Win32::System::{Console, IO, LibraryLoader, Memory, Threading};
use windows_sys::Win32::{Foundation, Globalization};
use windows_sys::w;
use crate::apperr;
use crate::arena::{Arena, ArenaString, scratch_arena};
use crate::helpers::*;
macro_rules! w_env {
($s:literal) => {{
const INPUT: &[u8] = env!($s).as_bytes();
const OUTPUT_LEN: usize = windows_sys::core::utf16_len(INPUT) + 1;
const OUTPUT: &[u16; OUTPUT_LEN] = {
let mut buffer = [0; OUTPUT_LEN];
let mut input_pos = 0;
let mut output_pos = 0;
while let Some((mut code_point, new_pos)) =
windows_sys::core::decode_utf8_char(INPUT, input_pos)
{
input_pos = new_pos;
if code_point <= 0xffff {
buffer[output_pos] = code_point as u16;
output_pos += 1;
} else {
code_point -= 0x10000;
buffer[output_pos] = 0xd800 + (code_point >> 10) as u16;
output_pos += 1;
buffer[output_pos] = 0xdc00 + (code_point & 0x3ff) as u16;
output_pos += 1;
}
}
&{ buffer }
};
OUTPUT.as_ptr()
}};
}
type ReadConsoleInputExW = unsafe extern "system" fn(
h_console_input: Foundation::HANDLE,
lp_buffer: *mut Console::INPUT_RECORD,
n_length: u32,
lp_number_of_events_read: *mut u32,
w_flags: u16,
) -> Foundation::BOOL;
unsafe extern "system" fn read_console_input_ex_placeholder(
_: Foundation::HANDLE,
_: *mut Console::INPUT_RECORD,
_: u32,
_: *mut u32,
_: u16,
) -> Foundation::BOOL {
panic!();
}
const CONSOLE_READ_NOWAIT: u16 = 0x0002;
const INVALID_CONSOLE_MODE: u32 = u32::MAX;
struct State {
read_console_input_ex: ReadConsoleInputExW,
stdin: Foundation::HANDLE,
stdout: Foundation::HANDLE,
stdin_cp_old: u32,
stdout_cp_old: u32,
stdin_mode_old: u32,
stdout_mode_old: u32,
leading_surrogate: u16,
inject_resize: bool,
wants_exit: bool,
}
static mut STATE: State = State {
read_console_input_ex: read_console_input_ex_placeholder,
stdin: null_mut(),
stdout: null_mut(),
stdin_cp_old: 0,
stdout_cp_old: 0,
stdin_mode_old: INVALID_CONSOLE_MODE,
stdout_mode_old: INVALID_CONSOLE_MODE,
leading_surrogate: 0,
inject_resize: false,
wants_exit: false,
};
extern "system" fn console_ctrl_handler(_ctrl_type: u32) -> Foundation::BOOL {
unsafe {
STATE.wants_exit = true;
IO::CancelIoEx(STATE.stdin, null());
}
1
}
/// Initializes the platform-specific state.
pub fn init() -> apperr::Result<Deinit> {
unsafe {
// Get the stdin and stdout handles first, so that if this function fails,
// we at least got something to use for `write_stdout`.
STATE.stdin = Console::GetStdHandle(Console::STD_INPUT_HANDLE);
STATE.stdout = Console::GetStdHandle(Console::STD_OUTPUT_HANDLE);
// Reopen stdin if it's redirected (= piped input).
if !ptr::eq(STATE.stdin, Foundation::INVALID_HANDLE_VALUE)
&& matches!(
FileSystem::GetFileType(STATE.stdin),
FileSystem::FILE_TYPE_DISK | FileSystem::FILE_TYPE_PIPE
)
{
STATE.stdin = FileSystem::CreateFileW(
w!("CONIN$"),
Foundation::GENERIC_READ | Foundation::GENERIC_WRITE,
FileSystem::FILE_SHARE_READ | FileSystem::FILE_SHARE_WRITE,
null_mut(),
FileSystem::OPEN_EXISTING,
0,
null_mut(),
);
}
if ptr::eq(STATE.stdin, Foundation::INVALID_HANDLE_VALUE)
|| ptr::eq(STATE.stdout, Foundation::INVALID_HANDLE_VALUE)
{
return Err(get_last_error());
}
unsafe fn load_read_func(module: *const u16) -> apperr::Result<ReadConsoleInputExW> {
unsafe {
get_module(module)
.and_then(|m| get_proc_address(m, c"ReadConsoleInputExW".as_ptr()))
}
}
// `kernel32.dll` doesn't exist on OneCore variants of Windows.
// NOTE: `kernelbase.dll` is NOT a stable API to rely on. In our case it's the best option though.
//
// This is written as two nested `match` statements so that we can return the error from the first
// `load_read_func` call if it fails. The kernel32.dll lookup may contain some valid information,
// while the kernelbase.dll lookup may not, since it's not a stable API.
STATE.read_console_input_ex = match load_read_func(w!("kernel32.dll")) {
Ok(func) => func,
Err(err) => match load_read_func(w!("kernelbase.dll")) {
Ok(func) => func,
Err(_) => return Err(err),
},
};
Ok(Deinit)
}
}
pub struct Deinit;
impl Drop for Deinit {
fn drop(&mut self) {
unsafe {
if STATE.stdin_cp_old != 0 {
Console::SetConsoleCP(STATE.stdin_cp_old);
STATE.stdin_cp_old = 0;
}
if STATE.stdout_cp_old != 0 {
Console::SetConsoleOutputCP(STATE.stdout_cp_old);
STATE.stdout_cp_old = 0;
}
if STATE.stdin_mode_old != INVALID_CONSOLE_MODE {
Console::SetConsoleMode(STATE.stdin, STATE.stdin_mode_old);
STATE.stdin_mode_old = INVALID_CONSOLE_MODE;
}
if STATE.stdout_mode_old != INVALID_CONSOLE_MODE {
Console::SetConsoleMode(STATE.stdout, STATE.stdout_mode_old);
STATE.stdout_mode_old = INVALID_CONSOLE_MODE;
}
}
}
}
/// Switches the terminal into raw mode, etc.
pub fn switch_modes() -> apperr::Result<()> {
unsafe {
check_bool_return(Console::SetConsoleCtrlHandler(Some(console_ctrl_handler), 1))?;
STATE.stdin_cp_old = Console::GetConsoleCP();
STATE.stdout_cp_old = Console::GetConsoleOutputCP();
check_bool_return(Console::GetConsoleMode(STATE.stdin, &raw mut STATE.stdin_mode_old))?;
check_bool_return(Console::GetConsoleMode(STATE.stdout, &raw mut STATE.stdout_mode_old))?;
check_bool_return(Console::SetConsoleCP(Globalization::CP_UTF8))?;
check_bool_return(Console::SetConsoleOutputCP(Globalization::CP_UTF8))?;
check_bool_return(Console::SetConsoleMode(
STATE.stdin,
Console::ENABLE_WINDOW_INPUT
| Console::ENABLE_EXTENDED_FLAGS
| Console::ENABLE_VIRTUAL_TERMINAL_INPUT,
))?;
check_bool_return(Console::SetConsoleMode(
STATE.stdout,
Console::ENABLE_PROCESSED_OUTPUT
| Console::ENABLE_WRAP_AT_EOL_OUTPUT
| Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING
| Console::DISABLE_NEWLINE_AUTO_RETURN,
))?;
Ok(())
}
}
/// During startup we need to get the window size from the terminal.
/// Because I didn't want to type a bunch of code, this function tells
/// [`read_stdin`] to inject a fake sequence, which gets picked up by
/// the input parser and provided to the TUI code.
pub fn inject_window_size_into_stdin() {
unsafe {
STATE.inject_resize = true;
}
}
fn get_console_size() -> Option<Size> {
unsafe {
let mut info: Console::CONSOLE_SCREEN_BUFFER_INFOEX = mem::zeroed();
info.cbSize = mem::size_of::<Console::CONSOLE_SCREEN_BUFFER_INFOEX>() as u32;
if Console::GetConsoleScreenBufferInfoEx(STATE.stdout, &mut info) == 0 {
return None;
}
let w = (info.srWindow.Right - info.srWindow.Left + 1).max(1) as CoordType;
let h = (info.srWindow.Bottom - info.srWindow.Top + 1).max(1) as CoordType;
Some(Size { width: w, height: h })
}
}
/// Reads from stdin.
///
/// # Returns
///
/// * `None` if there was an error reading from stdin.
/// * `Some("")` if the given timeout was reached.
/// * Otherwise, it returns the read, non-empty string.
pub fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option<ArenaString<'_>> {
let scratch = scratch_arena(Some(arena));
// On startup we're asked to inject a window size so that the UI system can layout the elements.
// --> Inject a fake sequence for our input parser.
let mut resize_event = None;
if unsafe { STATE.inject_resize } {
unsafe { STATE.inject_resize = false };
timeout = time::Duration::ZERO;
resize_event = get_console_size();
}
let read_poll = timeout != time::Duration::MAX; // there is a timeout -> don't block in read()
let input_buf = scratch.alloc_uninit_slice(4 * KIBI);
let mut input_buf_cap = input_buf.len();
let utf16_buf = scratch.alloc_uninit_slice(4 * KIBI);
let mut utf16_buf_len = 0;
// If there was a leftover leading surrogate from the last read, we prepend it to the buffer.
if unsafe { STATE.leading_surrogate } != 0 {
utf16_buf[0] = MaybeUninit::new(unsafe { STATE.leading_surrogate });
utf16_buf_len = 1;
input_buf_cap -= 1;
unsafe { STATE.leading_surrogate = 0 };
}
// Read until there's either a timeout or we have something to process.
loop {
if timeout != time::Duration::MAX {
let beg = time::Instant::now();
match unsafe { Threading::WaitForSingleObject(STATE.stdin, timeout.as_millis() as u32) }
{
// Ready to read? Continue with reading below.
Foundation::WAIT_OBJECT_0 => {}
// Timeout? Skip reading entirely.
Foundation::WAIT_TIMEOUT => break,
// Error? Tell the caller stdin is broken.
_ => return None,
}
timeout = timeout.saturating_sub(beg.elapsed());
}
// Read from stdin.
let input = unsafe {
// If we had a `inject_resize`, we don't want to block indefinitely for other pending input on startup,
// but are still interested in any other pending input that may be waiting for us.
let flags = if read_poll { CONSOLE_READ_NOWAIT } else { 0 };
let mut read = 0;
let ok = (STATE.read_console_input_ex)(
STATE.stdin,
input_buf[0].as_mut_ptr(),
input_buf_cap as u32,
&mut read,
flags,
);
if ok == 0 || STATE.wants_exit {
return None;
}
input_buf[..read as usize].assume_init_ref()
};
// Convert Win32 input records into UTF16.
for inp in input {
match inp.EventType as u32 {
Console::KEY_EVENT => {
let event = unsafe { &inp.Event.KeyEvent };
let ch = unsafe { event.uChar.UnicodeChar };
if event.bKeyDown != 0 && ch != 0 {
utf16_buf[utf16_buf_len] = MaybeUninit::new(ch);
utf16_buf_len += 1;
}
}
Console::WINDOW_BUFFER_SIZE_EVENT => {
let event = unsafe { &inp.Event.WindowBufferSizeEvent };
let w = event.dwSize.X as CoordType;
let h = event.dwSize.Y as CoordType;
// Windows is prone to sending broken/useless `WINDOW_BUFFER_SIZE_EVENT`s.
// E.g. starting conhost will emit 3 in a row. Skip rendering in that case.
if w > 0 && h > 0 {
resize_event = Some(Size { width: w, height: h });
}
}
_ => {}
}
}
if resize_event.is_some() || utf16_buf_len != 0 {
break;
}
}
const RESIZE_EVENT_FMT_MAX_LEN: usize = 16; // "\x1b[8;65535;65535t"
let resize_event_len = if resize_event.is_some() { RESIZE_EVENT_FMT_MAX_LEN } else { 0 };
// +1 to account for a potential `STATE.leading_surrogate`.
let utf8_max_len = (utf16_buf_len + 1) * 3;
let mut text = ArenaString::new_in(arena);
text.reserve(utf8_max_len + resize_event_len);
// Now prepend our previously extracted resize event.
if let Some(resize_event) = resize_event {
// If I read xterm's documentation correctly, CSI 18 t reports the window size in characters.
// CSI 8 ; height ; width t is the response. Of course, we didn't send the request,
// but we can use this fake response to trigger the editor to resize itself.
_ = write!(text, "\x1b[8;{};{}t", resize_event.height, resize_event.width);
}
// If the input ends with a lone lead surrogate, we need to remember it for the next read.
if utf16_buf_len > 0 {
unsafe {
let last_char = utf16_buf[utf16_buf_len - 1].assume_init();
if (0xD800..0xDC00).contains(&last_char) {
STATE.leading_surrogate = last_char;
utf16_buf_len -= 1;
}
}
}
// Convert the remaining input to UTF8, the sane encoding.
if utf16_buf_len > 0 {
unsafe {
let vec = text.as_mut_vec();
let spare = vec.spare_capacity_mut();
let len = Globalization::WideCharToMultiByte(
Globalization::CP_UTF8,
0,
utf16_buf[0].as_ptr(),
utf16_buf_len as i32,
spare.as_mut_ptr() as *mut _,
spare.len() as i32,
null(),
null_mut(),
);
if len > 0 {
vec.set_len(vec.len() + len as usize);
}
}
}
text.shrink_to_fit();
Some(text)
}
/// Writes a string to stdout.
///
/// Use this instead of `print!` or `println!` to avoid
/// the overhead of Rust's stdio handling. Don't need that.
pub fn write_stdout(text: &str) {
unsafe {
let mut offset = 0;
while offset < text.len() {
let ptr = text.as_ptr().add(offset);
let write = (text.len() - offset).min(GIBI) as u32;
let mut written = 0;
let ok = FileSystem::WriteFile(STATE.stdout, ptr, write, &mut written, null_mut());
offset += written as usize;
if ok == 0 || written == 0 {
break;
}
}
}
}
/// Check if the stdin handle is redirected to a file, etc.
///
/// # Returns
///
/// * `Some(file)` if stdin is redirected.
/// * Otherwise, `None`.
pub fn open_stdin_if_redirected() -> Option<File> {
unsafe {
let handle = Console::GetStdHandle(Console::STD_INPUT_HANDLE);
// Did we reopen stdin during `init()`?
if !std::ptr::eq(STATE.stdin, handle) { Some(File::from_raw_handle(handle)) } else { None }
}
}
pub fn drives() -> impl Iterator<Item = char> {
unsafe {
let mut mask = FileSystem::GetLogicalDrives();
std::iter::from_fn(move || {
let bit = mask.trailing_zeros();
if bit >= 26 {
None
} else {
mask &= !(1 << bit);
Some((b'A' + bit as u8) as char)
}
})
}
}
/// A unique identifier for a file.
pub enum FileId {
Id(FileSystem::FILE_ID_INFO),
Path(PathBuf),
}
impl PartialEq for FileId {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Id(left), Self::Id(right)) => {
// Lowers to an efficient word-wise comparison.
const SIZE: usize = std::mem::size_of::<FileSystem::FILE_ID_INFO>();
let a: &[u8; SIZE] = unsafe { mem::transmute(left) };
let b: &[u8; SIZE] = unsafe { mem::transmute(right) };
a == b
}
(Self::Path(left), Self::Path(right)) => left == right,
_ => false,
}
}
}
impl Eq for FileId {}
/// Returns a unique identifier for the given file by handle or path.
pub fn file_id(file: Option<&File>, path: &Path) -> apperr::Result<FileId> {
let file = match file {
Some(f) => f,
None => &File::open(path)?,
};
file_id_from_handle(file).or_else(|_| Ok(FileId::Path(std::fs::canonicalize(path)?)))
}
fn file_id_from_handle(file: &File) -> apperr::Result<FileId> {
unsafe {
let mut info = MaybeUninit::<FileSystem::FILE_ID_INFO>::uninit();
check_bool_return(FileSystem::GetFileInformationByHandleEx(
file.as_raw_handle(),
FileSystem::FileIdInfo,
info.as_mut_ptr() as *mut _,
mem::size_of::<FileSystem::FILE_ID_INFO>() as u32,
))?;
Ok(FileId::Id(info.assume_init()))
}
}
/// Canonicalizes the given path.
///
/// This differs from [`fs::canonicalize`] in that it strips the `\\?\` UNC
/// prefix on Windows. This is because it's confusing/ugly when displaying it.
pub fn canonicalize(path: &Path) -> std::io::Result<PathBuf> {
let mut path = fs::canonicalize(path)?;
let path = path.as_mut_os_string();
let mut path = mem::take(path).into_encoded_bytes();
if path.len() > 6 && &path[0..4] == br"\\?\" && path[4].is_ascii_uppercase() && path[5] == b':'
{
path.drain(0..4);
}
let path = unsafe { OsString::from_encoded_bytes_unchecked(path) };
let path = PathBuf::from(path);
Ok(path)
}
/// Reserves a virtual memory region of the given size.
/// To commit the memory, use [`virtual_commit`].
/// To release the memory, use [`virtual_release`].
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Don't forget to release the memory when you're done with it or you'll leak it.
pub unsafe fn virtual_reserve(size: usize) -> apperr::Result<NonNull<u8>> {
unsafe {
#[allow(unused_assignments, unused_mut)]
let mut base = null_mut();
// In debug builds, we use fixed addresses to aid in debugging.
// Makes it possible to immediately tell which address space a pointer belongs to.
#[cfg(all(debug_assertions, not(target_pointer_width = "32")))]
{
static mut S_BASE_GEN: usize = 0x0000100000000000; // 16 TiB
S_BASE_GEN += 0x0000001000000000; // 64 GiB
base = S_BASE_GEN as *mut _;
}
check_ptr_return(Memory::VirtualAlloc(
base,
size,
Memory::MEM_RESERVE,
Memory::PAGE_READWRITE,
) as *mut u8)
}
}
/// Releases a virtual memory region of the given size.
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Make sure to only pass pointers acquired from [`virtual_reserve`].
pub unsafe fn virtual_release(base: NonNull<u8>, _size: usize) {
unsafe {
// NOTE: `VirtualFree` fails if the pointer isn't
// a valid base address or if the size isn't zero.
Memory::VirtualFree(base.as_ptr() as *mut _, 0, Memory::MEM_RELEASE);
}
}
/// Commits a virtual memory region of the given size.
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Make sure to only pass pointers acquired from [`virtual_reserve`]
/// and to pass a size less than or equal to the size passed to [`virtual_reserve`].
pub unsafe fn virtual_commit(base: NonNull<u8>, size: usize) -> apperr::Result<()> {
unsafe {
check_ptr_return(Memory::VirtualAlloc(
base.as_ptr() as *mut _,
size,
Memory::MEM_COMMIT,
Memory::PAGE_READWRITE,
))
.map(|_| ())
}
}
unsafe fn get_module(name: *const u16) -> apperr::Result<NonNull<c_void>> {
unsafe { check_ptr_return(LibraryLoader::GetModuleHandleW(name)) }
}
unsafe fn load_library(name: *const u16) -> apperr::Result<NonNull<c_void>> {
unsafe {
check_ptr_return(LibraryLoader::LoadLibraryExW(
name,
null_mut(),
LibraryLoader::LOAD_LIBRARY_SEARCH_SYSTEM32,
))
}
}
/// Loads a function from a dynamic library.
///
/// # Safety
///
/// This function is highly unsafe as it requires you to know the exact type
/// of the function you're loading. No type checks whatsoever are performed.
//
// It'd be nice to constrain T to std::marker::FnPtr, but that's unstable.
pub unsafe fn get_proc_address<T>(
handle: NonNull<c_void>,
name: *const c_char,
) -> apperr::Result<T> {
unsafe {
let ptr = LibraryLoader::GetProcAddress(handle.as_ptr(), name as *const u8);
if let Some(ptr) = ptr { Ok(mem::transmute_copy(&ptr)) } else { Err(get_last_error()) }
}
}
pub struct LibIcu {
pub libicuuc: NonNull<c_void>,
pub libicui18n: NonNull<c_void>,
}
pub fn load_icu() -> apperr::Result<LibIcu> {
const fn const_ptr_u16_eq(a: *const u16, b: *const u16) -> bool {
unsafe {
let mut a = a;
let mut b = b;
loop {
if *a != *b {
return false;
}
if *a == 0 {
return true;
}
a = a.add(1);
b = b.add(1);
}
}
}
const LIBICUUC: *const u16 = w_env!("EDIT_CFG_ICUUC_SONAME");
const LIBICUI18N: *const u16 = w_env!("EDIT_CFG_ICUI18N_SONAME");
if const { const_ptr_u16_eq(LIBICUUC, LIBICUI18N) } {
let icu = unsafe { load_library(LIBICUUC)? };
Ok(LibIcu { libicuuc: icu, libicui18n: icu })
} else {
let libicuuc = unsafe { load_library(LIBICUUC)? };
let libicui18n = unsafe { load_library(LIBICUI18N)? };
Ok(LibIcu { libicuuc, libicui18n })
}
}
/// Returns a list of preferred languages for the current user.
pub fn preferred_languages(arena: &Arena) -> Vec<ArenaString<'_>, &Arena> {
// If the GetUserPreferredUILanguages() don't fit into 512 characters,
// honestly, just give up. How many languages do you realistically need?
const LEN: usize = 512;
let scratch = scratch_arena(Some(arena));
let mut res = Vec::new_in(arena);
// Get the list of preferred languages via `GetUserPreferredUILanguages`.
let langs = unsafe {
let buf = scratch.alloc_uninit_slice(LEN);
let mut len = buf.len() as u32;
let mut num = 0;
let ok = Globalization::GetUserPreferredUILanguages(
Globalization::MUI_LANGUAGE_NAME,
&mut num,
buf[0].as_mut_ptr(),
&mut len,
);
if ok == 0 || num == 0 {
len = 0;
}
// Drop the terminating double-null character.
len = len.saturating_sub(1);
buf[..len as usize].assume_init_ref()
};
// Convert UTF16 to UTF8.
let langs = wide_to_utf8(&scratch, langs);
// Split the null-delimited string into individual chunks
// and copy them into the given arena.
res.extend(
langs
.split_terminator('\0')
.filter(|s| !s.is_empty())
.map(|s| ArenaString::from_str(arena, s)),
);
res
}
fn wide_to_utf8<'a>(arena: &'a Arena, wide: &[u16]) -> ArenaString<'a> {
let mut res = ArenaString::new_in(arena);
res.reserve(wide.len() * 3);
let len = unsafe {
Globalization::WideCharToMultiByte(
Globalization::CP_UTF8,
0,
wide.as_ptr(),
wide.len() as i32,
res.as_mut_ptr() as *mut _,
res.capacity() as i32,
null(),
null_mut(),
)
};
if len > 0 {
unsafe { res.as_mut_vec().set_len(len as usize) };
}
res.shrink_to_fit();
res
}
#[cold]
fn get_last_error() -> apperr::Error {
unsafe { gle_to_apperr(Foundation::GetLastError()) }
}
#[inline]
const fn gle_to_apperr(gle: u32) -> apperr::Error {
apperr::Error::new_sys(if gle == 0 { 0x8000FFFF } else { 0x80070000 | gle })
}
#[inline]
pub(crate) fn io_error_to_apperr(err: std::io::Error) -> apperr::Error {
gle_to_apperr(err.raw_os_error().unwrap_or(0) as u32)
}
/// Formats a platform error code into a human-readable string.
pub fn apperr_format(f: &mut std::fmt::Formatter<'_>, code: u32) -> std::fmt::Result {
unsafe {
let mut ptr: *mut u8 = null_mut();
let len = Debug::FormatMessageA(
Debug::FORMAT_MESSAGE_ALLOCATE_BUFFER
| Debug::FORMAT_MESSAGE_FROM_SYSTEM
| Debug::FORMAT_MESSAGE_IGNORE_INSERTS,
null(),
code,
0,
&mut ptr as *mut *mut _ as *mut _,
0,
null_mut(),
);
write!(f, "Error {code:#08x}")?;
if len > 0 {
let msg = str_from_raw_parts(ptr, len as usize);
let msg = msg.trim_ascii();
let msg = msg.replace(['\r', '\n'], " ");
write!(f, ": {msg}")?;
Foundation::LocalFree(ptr as *mut _);
}
Ok(())
}
}
/// Checks if the given error is a "file not found" error.
pub fn apperr_is_not_found(err: apperr::Error) -> bool {
err == gle_to_apperr(Foundation::ERROR_FILE_NOT_FOUND)
}
fn check_bool_return(ret: Foundation::BOOL) -> apperr::Result<()> {
if ret == 0 { Err(get_last_error()) } else { Ok(()) }
}
fn check_ptr_return<T>(ret: *mut T) -> apperr::Result<NonNull<T>> {
NonNull::new(ret).ok_or_else(get_last_error)
}