1use std::ffi::{CStr, OsString, c_void};
5use std::fmt::Write as _;
6use std::fs::{self, File};
7use std::mem::MaybeUninit;
8use std::os::windows::io::{AsRawHandle as _, FromRawHandle};
9use std::path::{Path, PathBuf};
10use std::ptr::{self, NonNull, null, null_mut};
11use std::{mem, time};
12
13use windows_sys::Win32::Storage::FileSystem;
14use windows_sys::Win32::System::Diagnostics::Debug;
15use windows_sys::Win32::System::{Console, IO, LibraryLoader, Memory, Threading};
16use windows_sys::Win32::{Foundation, Globalization};
17use windows_sys::w;
18
19use crate::apperr;
20use crate::arena::{Arena, ArenaString, scratch_arena};
21use crate::helpers::*;
22
23type ReadConsoleInputExW = unsafe extern "system" fn(
24 h_console_input: Foundation::HANDLE,
25 lp_buffer: *mut Console::INPUT_RECORD,
26 n_length: u32,
27 lp_number_of_events_read: *mut u32,
28 w_flags: u16,
29) -> Foundation::BOOL;
30
31unsafe extern "system" fn read_console_input_ex_placeholder(
32 _: Foundation::HANDLE,
33 _: *mut Console::INPUT_RECORD,
34 _: u32,
35 _: *mut u32,
36 _: u16,
37) -> Foundation::BOOL {
38 panic!();
39}
40
41const CONSOLE_READ_NOWAIT: u16 = 0x0002;
42
43const INVALID_CONSOLE_MODE: u32 = u32::MAX;
44
45struct State {
46 read_console_input_ex: ReadConsoleInputExW,
47 stdin: Foundation::HANDLE,
48 stdout: Foundation::HANDLE,
49 stdin_cp_old: u32,
50 stdout_cp_old: u32,
51 stdin_mode_old: u32,
52 stdout_mode_old: u32,
53 leading_surrogate: u16,
54 inject_resize: bool,
55 wants_exit: bool,
56}
57
58static mut STATE: State = State {
59 read_console_input_ex: read_console_input_ex_placeholder,
60 stdin: null_mut(),
61 stdout: null_mut(),
62 stdin_cp_old: 0,
63 stdout_cp_old: 0,
64 stdin_mode_old: INVALID_CONSOLE_MODE,
65 stdout_mode_old: INVALID_CONSOLE_MODE,
66 leading_surrogate: 0,
67 inject_resize: false,
68 wants_exit: false,
69};
70
71extern "system" fn console_ctrl_handler(_ctrl_type: u32) -> Foundation::BOOL {
72 unsafe {
73 STATE.wants_exit = true;
74 IO::CancelIoEx(STATE.stdin, null());
75 }
76 1
77}
78
79pub fn init() -> apperr::Result<Deinit> {
81 unsafe {
82 STATE.stdin = Console::GetStdHandle(Console::STD_INPUT_HANDLE);
85 STATE.stdout = Console::GetStdHandle(Console::STD_OUTPUT_HANDLE);
86
87 if !ptr::eq(STATE.stdin, Foundation::INVALID_HANDLE_VALUE)
89 && matches!(
90 FileSystem::GetFileType(STATE.stdin),
91 FileSystem::FILE_TYPE_DISK | FileSystem::FILE_TYPE_PIPE
92 )
93 {
94 STATE.stdin = FileSystem::CreateFileW(
95 w!("CONIN$"),
96 Foundation::GENERIC_READ | Foundation::GENERIC_WRITE,
97 FileSystem::FILE_SHARE_READ | FileSystem::FILE_SHARE_WRITE,
98 null_mut(),
99 FileSystem::OPEN_EXISTING,
100 0,
101 null_mut(),
102 );
103 }
104
105 if ptr::eq(STATE.stdin, Foundation::INVALID_HANDLE_VALUE)
106 || ptr::eq(STATE.stdout, Foundation::INVALID_HANDLE_VALUE)
107 {
108 return Err(get_last_error());
109 }
110
111 unsafe fn load_read_func(module: *const u16) -> apperr::Result<ReadConsoleInputExW> {
112 unsafe { get_module(module).and_then(|m| get_proc_address(m, c"ReadConsoleInputExW")) }
113 }
114
115 STATE.read_console_input_ex = match load_read_func(w!("kernel32.dll")) {
122 Ok(func) => func,
123 Err(err) => match load_read_func(w!("kernelbase.dll")) {
124 Ok(func) => func,
125 Err(_) => return Err(err),
126 },
127 };
128
129 Ok(Deinit)
130 }
131}
132
133pub struct Deinit;
134
135impl Drop for Deinit {
136 fn drop(&mut self) {
137 unsafe {
138 if STATE.stdin_cp_old != 0 {
139 Console::SetConsoleCP(STATE.stdin_cp_old);
140 STATE.stdin_cp_old = 0;
141 }
142 if STATE.stdout_cp_old != 0 {
143 Console::SetConsoleOutputCP(STATE.stdout_cp_old);
144 STATE.stdout_cp_old = 0;
145 }
146 if STATE.stdin_mode_old != INVALID_CONSOLE_MODE {
147 Console::SetConsoleMode(STATE.stdin, STATE.stdin_mode_old);
148 STATE.stdin_mode_old = INVALID_CONSOLE_MODE;
149 }
150 if STATE.stdout_mode_old != INVALID_CONSOLE_MODE {
151 Console::SetConsoleMode(STATE.stdout, STATE.stdout_mode_old);
152 STATE.stdout_mode_old = INVALID_CONSOLE_MODE;
153 }
154 }
155 }
156}
157
158pub fn switch_modes() -> apperr::Result<()> {
160 unsafe {
161 check_bool_return(Console::SetConsoleCtrlHandler(Some(console_ctrl_handler), 1))?;
162
163 STATE.stdin_cp_old = Console::GetConsoleCP();
164 STATE.stdout_cp_old = Console::GetConsoleOutputCP();
165 check_bool_return(Console::GetConsoleMode(STATE.stdin, &raw mut STATE.stdin_mode_old))?;
166 check_bool_return(Console::GetConsoleMode(STATE.stdout, &raw mut STATE.stdout_mode_old))?;
167
168 check_bool_return(Console::SetConsoleCP(Globalization::CP_UTF8))?;
169 check_bool_return(Console::SetConsoleOutputCP(Globalization::CP_UTF8))?;
170 check_bool_return(Console::SetConsoleMode(
171 STATE.stdin,
172 Console::ENABLE_WINDOW_INPUT
173 | Console::ENABLE_EXTENDED_FLAGS
174 | Console::ENABLE_VIRTUAL_TERMINAL_INPUT,
175 ))?;
176 check_bool_return(Console::SetConsoleMode(
177 STATE.stdout,
178 Console::ENABLE_PROCESSED_OUTPUT
179 | Console::ENABLE_WRAP_AT_EOL_OUTPUT
180 | Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING
181 | Console::DISABLE_NEWLINE_AUTO_RETURN,
182 ))?;
183
184 Ok(())
185 }
186}
187
188pub fn inject_window_size_into_stdin() {
193 unsafe {
194 STATE.inject_resize = true;
195 }
196}
197
198fn get_console_size() -> Option<Size> {
199 unsafe {
200 let mut info: Console::CONSOLE_SCREEN_BUFFER_INFOEX = mem::zeroed();
201 info.cbSize = mem::size_of::<Console::CONSOLE_SCREEN_BUFFER_INFOEX>() as u32;
202 if Console::GetConsoleScreenBufferInfoEx(STATE.stdout, &mut info) == 0 {
203 return None;
204 }
205
206 let w = (info.srWindow.Right - info.srWindow.Left + 1).max(1) as CoordType;
207 let h = (info.srWindow.Bottom - info.srWindow.Top + 1).max(1) as CoordType;
208 Some(Size { width: w, height: h })
209 }
210}
211
212pub fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option<ArenaString<'_>> {
220 let scratch = scratch_arena(Some(arena));
221
222 let mut resize_event = None;
225 if unsafe { STATE.inject_resize } {
226 unsafe { STATE.inject_resize = false };
227 timeout = time::Duration::ZERO;
228 resize_event = get_console_size();
229 }
230
231 let read_poll = timeout != time::Duration::MAX; let input_buf = scratch.alloc_uninit_slice(4 * KIBI);
233 let mut input_buf_cap = input_buf.len();
234 let utf16_buf = scratch.alloc_uninit_slice(4 * KIBI);
235 let mut utf16_buf_len = 0;
236
237 if unsafe { STATE.leading_surrogate } != 0 {
239 utf16_buf[0] = MaybeUninit::new(unsafe { STATE.leading_surrogate });
240 utf16_buf_len = 1;
241 input_buf_cap -= 1;
242 unsafe { STATE.leading_surrogate = 0 };
243 }
244
245 loop {
247 if timeout != time::Duration::MAX {
248 let beg = time::Instant::now();
249
250 match unsafe { Threading::WaitForSingleObject(STATE.stdin, timeout.as_millis() as u32) }
251 {
252 Foundation::WAIT_OBJECT_0 => {}
254 Foundation::WAIT_TIMEOUT => break,
256 _ => return None,
258 }
259
260 timeout = timeout.saturating_sub(beg.elapsed());
261 }
262
263 let input = unsafe {
265 let flags = if read_poll { CONSOLE_READ_NOWAIT } else { 0 };
268 let mut read = 0;
269 let ok = (STATE.read_console_input_ex)(
270 STATE.stdin,
271 input_buf[0].as_mut_ptr(),
272 input_buf_cap as u32,
273 &mut read,
274 flags,
275 );
276 if ok == 0 || STATE.wants_exit {
277 return None;
278 }
279 input_buf[..read as usize].assume_init_ref()
280 };
281
282 for inp in input {
284 match inp.EventType as u32 {
285 Console::KEY_EVENT => {
286 let event = unsafe { &inp.Event.KeyEvent };
287 let ch = unsafe { event.uChar.UnicodeChar };
288 if event.bKeyDown != 0 && ch != 0 {
289 utf16_buf[utf16_buf_len] = MaybeUninit::new(ch);
290 utf16_buf_len += 1;
291 }
292 }
293 Console::WINDOW_BUFFER_SIZE_EVENT => {
294 let event = unsafe { &inp.Event.WindowBufferSizeEvent };
295 let w = event.dwSize.X as CoordType;
296 let h = event.dwSize.Y as CoordType;
297 if w > 0 && h > 0 {
300 resize_event = Some(Size { width: w, height: h });
301 }
302 }
303 _ => {}
304 }
305 }
306
307 if resize_event.is_some() || utf16_buf_len != 0 {
308 break;
309 }
310 }
311
312 const RESIZE_EVENT_FMT_MAX_LEN: usize = 16; let resize_event_len = if resize_event.is_some() { RESIZE_EVENT_FMT_MAX_LEN } else { 0 };
314 let utf8_max_len = (utf16_buf_len + 1) * 3;
316 let mut text = ArenaString::new_in(arena);
317 text.reserve(utf8_max_len + resize_event_len);
318
319 if let Some(resize_event) = resize_event {
321 _ = write!(text, "\x1b[8;{};{}t", resize_event.height, resize_event.width);
325 }
326
327 if utf16_buf_len > 0 {
329 unsafe {
330 let last_char = utf16_buf[utf16_buf_len - 1].assume_init();
331 if (0xD800..0xDC00).contains(&last_char) {
332 STATE.leading_surrogate = last_char;
333 utf16_buf_len -= 1;
334 }
335 }
336 }
337
338 if utf16_buf_len > 0 {
340 unsafe {
341 let vec = text.as_mut_vec();
342 let spare = vec.spare_capacity_mut();
343
344 let len = Globalization::WideCharToMultiByte(
345 Globalization::CP_UTF8,
346 0,
347 utf16_buf[0].as_ptr(),
348 utf16_buf_len as i32,
349 spare.as_mut_ptr() as *mut _,
350 spare.len() as i32,
351 null(),
352 null_mut(),
353 );
354
355 if len > 0 {
356 vec.set_len(vec.len() + len as usize);
357 }
358 }
359 }
360
361 text.shrink_to_fit();
362 Some(text)
363}
364
365pub fn write_stdout(text: &str) {
370 unsafe {
371 let mut offset = 0;
372
373 while offset < text.len() {
374 let ptr = text.as_ptr().add(offset);
375 let write = (text.len() - offset).min(GIBI) as u32;
376 let mut written = 0;
377 let ok = FileSystem::WriteFile(STATE.stdout, ptr, write, &mut written, null_mut());
378 offset += written as usize;
379 if ok == 0 || written == 0 {
380 break;
381 }
382 }
383 }
384}
385
386pub fn open_stdin_if_redirected() -> Option<File> {
393 unsafe {
394 let handle = Console::GetStdHandle(Console::STD_INPUT_HANDLE);
395 if !std::ptr::eq(STATE.stdin, handle) { Some(File::from_raw_handle(handle)) } else { None }
397 }
398}
399
400pub fn drives() -> impl Iterator<Item = char> {
401 unsafe {
402 let mut mask = FileSystem::GetLogicalDrives();
403 std::iter::from_fn(move || {
404 let bit = mask.trailing_zeros();
405 if bit >= 26 {
406 None
407 } else {
408 mask &= !(1 << bit);
409 Some((b'A' + bit as u8) as char)
410 }
411 })
412 }
413}
414
415pub enum FileId {
417 Id(FileSystem::FILE_ID_INFO),
418 Path(PathBuf),
419}
420
421impl PartialEq for FileId {
422 fn eq(&self, other: &Self) -> bool {
423 match (self, other) {
424 (Self::Id(left), Self::Id(right)) => {
425 const SIZE: usize = std::mem::size_of::<FileSystem::FILE_ID_INFO>();
427 let a: &[u8; SIZE] = unsafe { mem::transmute(left) };
428 let b: &[u8; SIZE] = unsafe { mem::transmute(right) };
429 a == b
430 }
431 (Self::Path(left), Self::Path(right)) => left == right,
432 _ => false,
433 }
434 }
435}
436
437impl Eq for FileId {}
438
439pub fn file_id(file: Option<&File>, path: &Path) -> apperr::Result<FileId> {
441 let file = match file {
442 Some(f) => f,
443 None => &File::open(path)?,
444 };
445
446 file_id_from_handle(file).or_else(|_| Ok(FileId::Path(std::fs::canonicalize(path)?)))
447}
448
449fn file_id_from_handle(file: &File) -> apperr::Result<FileId> {
450 unsafe {
451 let mut info = MaybeUninit::<FileSystem::FILE_ID_INFO>::uninit();
452 check_bool_return(FileSystem::GetFileInformationByHandleEx(
453 file.as_raw_handle(),
454 FileSystem::FileIdInfo,
455 info.as_mut_ptr() as *mut _,
456 mem::size_of::<FileSystem::FILE_ID_INFO>() as u32,
457 ))?;
458 Ok(FileId::Id(info.assume_init()))
459 }
460}
461
462pub fn canonicalize(path: &Path) -> std::io::Result<PathBuf> {
467 let mut path = fs::canonicalize(path)?;
468 let path = path.as_mut_os_string();
469 let mut path = mem::take(path).into_encoded_bytes();
470
471 if path.len() > 6 && &path[0..4] == br"\\?\" && path[4].is_ascii_uppercase() && path[5] == b':'
472 {
473 path.drain(0..4);
474 }
475
476 let path = unsafe { OsString::from_encoded_bytes_unchecked(path) };
477 let path = PathBuf::from(path);
478 Ok(path)
479}
480
481pub unsafe fn virtual_reserve(size: usize) -> apperr::Result<NonNull<u8>> {
490 unsafe {
491 #[allow(unused_assignments, unused_mut)]
492 let mut base = null_mut();
493
494 #[cfg(all(debug_assertions, not(target_pointer_width = "32")))]
497 {
498 static mut S_BASE_GEN: usize = 0x0000100000000000; S_BASE_GEN += 0x0000001000000000; base = S_BASE_GEN as *mut _;
501 }
502
503 check_ptr_return(Memory::VirtualAlloc(
504 base,
505 size,
506 Memory::MEM_RESERVE,
507 Memory::PAGE_READWRITE,
508 ) as *mut u8)
509 }
510}
511
512pub unsafe fn virtual_release(base: NonNull<u8>, _size: usize) {
519 unsafe {
520 Memory::VirtualFree(base.as_ptr() as *mut _, 0, Memory::MEM_RELEASE);
523 }
524}
525
526pub unsafe fn virtual_commit(base: NonNull<u8>, size: usize) -> apperr::Result<()> {
534 unsafe {
535 check_ptr_return(Memory::VirtualAlloc(
536 base.as_ptr() as *mut _,
537 size,
538 Memory::MEM_COMMIT,
539 Memory::PAGE_READWRITE,
540 ))
541 .map(|_| ())
542 }
543}
544
545unsafe fn get_module(name: *const u16) -> apperr::Result<NonNull<c_void>> {
546 unsafe { check_ptr_return(LibraryLoader::GetModuleHandleW(name)) }
547}
548
549unsafe fn load_library(name: *const u16) -> apperr::Result<NonNull<c_void>> {
550 unsafe {
551 check_ptr_return(LibraryLoader::LoadLibraryExW(
552 name,
553 null_mut(),
554 LibraryLoader::LOAD_LIBRARY_SEARCH_SYSTEM32,
555 ))
556 }
557}
558
559pub unsafe fn get_proc_address<T>(handle: NonNull<c_void>, name: &CStr) -> apperr::Result<T> {
568 unsafe {
569 let ptr = LibraryLoader::GetProcAddress(handle.as_ptr(), name.as_ptr() as *const u8);
570 if let Some(ptr) = ptr { Ok(mem::transmute_copy(&ptr)) } else { Err(get_last_error()) }
571 }
572}
573
574pub fn load_libicuuc() -> apperr::Result<NonNull<c_void>> {
576 unsafe { load_library(w!("icuuc.dll")) }
577}
578
579pub fn load_libicui18n() -> apperr::Result<NonNull<c_void>> {
581 unsafe { load_library(w!("icuin.dll")) }
582}
583
584pub fn preferred_languages(arena: &Arena) -> Vec<ArenaString<'_>, &Arena> {
586 const LEN: usize = 512;
589
590 let scratch = scratch_arena(Some(arena));
591 let mut res = Vec::new_in(arena);
592
593 let langs = unsafe {
595 let buf = scratch.alloc_uninit_slice(LEN);
596 let mut len = buf.len() as u32;
597 let mut num = 0;
598
599 let ok = Globalization::GetUserPreferredUILanguages(
600 Globalization::MUI_LANGUAGE_NAME,
601 &mut num,
602 buf[0].as_mut_ptr(),
603 &mut len,
604 );
605
606 if ok == 0 || num == 0 {
607 len = 0;
608 }
609
610 len = len.saturating_sub(1);
612
613 buf[..len as usize].assume_init_ref()
614 };
615
616 let langs = wide_to_utf8(&scratch, langs);
618
619 res.extend(
622 langs
623 .split_terminator('\0')
624 .filter(|s| !s.is_empty())
625 .map(|s| ArenaString::from_str(arena, s)),
626 );
627 res
628}
629
630fn wide_to_utf8<'a>(arena: &'a Arena, wide: &[u16]) -> ArenaString<'a> {
631 let mut res = ArenaString::new_in(arena);
632 res.reserve(wide.len() * 3);
633
634 let len = unsafe {
635 Globalization::WideCharToMultiByte(
636 Globalization::CP_UTF8,
637 0,
638 wide.as_ptr(),
639 wide.len() as i32,
640 res.as_mut_ptr() as *mut _,
641 res.capacity() as i32,
642 null(),
643 null_mut(),
644 )
645 };
646 if len > 0 {
647 unsafe { res.as_mut_vec().set_len(len as usize) };
648 }
649
650 res.shrink_to_fit();
651 res
652}
653
654#[cold]
655fn get_last_error() -> apperr::Error {
656 unsafe { gle_to_apperr(Foundation::GetLastError()) }
657}
658
659#[inline]
660const fn gle_to_apperr(gle: u32) -> apperr::Error {
661 apperr::Error::new_sys(if gle == 0 { 0x8000FFFF } else { 0x80070000 | gle })
662}
663
664#[inline]
665pub(crate) fn io_error_to_apperr(err: std::io::Error) -> apperr::Error {
666 gle_to_apperr(err.raw_os_error().unwrap_or(0) as u32)
667}
668
669pub fn apperr_format(f: &mut std::fmt::Formatter<'_>, code: u32) -> std::fmt::Result {
671 unsafe {
672 let mut ptr: *mut u8 = null_mut();
673 let len = Debug::FormatMessageA(
674 Debug::FORMAT_MESSAGE_ALLOCATE_BUFFER
675 | Debug::FORMAT_MESSAGE_FROM_SYSTEM
676 | Debug::FORMAT_MESSAGE_IGNORE_INSERTS,
677 null(),
678 code,
679 0,
680 &mut ptr as *mut *mut _ as *mut _,
681 0,
682 null_mut(),
683 );
684
685 write!(f, "Error {code:#08x}")?;
686
687 if len > 0 {
688 let msg = str_from_raw_parts(ptr, len as usize);
689 let msg = msg.trim_ascii();
690 let msg = msg.replace(['\r', '\n'], " ");
691 write!(f, ": {msg}")?;
692 Foundation::LocalFree(ptr as *mut _);
693 }
694
695 Ok(())
696 }
697}
698
699pub fn apperr_is_not_found(err: apperr::Error) -> bool {
701 err == gle_to_apperr(Foundation::ERROR_FILE_NOT_FOUND)
702}
703
704fn check_bool_return(ret: Foundation::BOOL) -> apperr::Result<()> {
705 if ret == 0 { Err(get_last_error()) } else { Ok(()) }
706}
707
708fn check_ptr_return<T>(ret: *mut T) -> apperr::Result<NonNull<T>> {
709 NonNull::new(ret).ok_or_else(get_last_error)
710}