From 325f8cb4eae8037cbe3d6faae528090d9953afbe Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 4 Jun 2026 16:58:11 +0200 Subject: [PATCH 1/2] Enable VT output mode --- src/main.rs | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/src/main.rs b/src/main.rs index b0838c7..3c63b89 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,7 @@ use itertools::Itertools as _; use uucore::display::Quotable as _; use uucore::{Args, error::strip_errno, locale}; use windows_sys::Win32::Globalization::CP_UTF8; -use windows_sys::Win32::System::Console::{GetConsoleOutputCP, SetConsoleOutputCP}; +use windows_sys::Win32::System::Console; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -61,7 +61,7 @@ fn main() { // The good news is that this just so happens to not negatively affect ntfind, // because through ulib it incorrectly checks the input CP instead of the output one. // ntsort just hardcodes to CP_OEMCP, so it also isn't affected. - let _restore_cp = set_console_cp_utf8(); + set_console_modes(); let utils = util_map(); let mut args = uucore::args_os(); @@ -236,25 +236,31 @@ fn get_canonical_util_name(util_name: &str) -> &str { } } -fn set_console_cp_utf8() -> RestoreConsoleCp { - let mut cp = unsafe { GetConsoleOutputCP() }; - if cp == CP_UTF8 { - cp = 0; - } - - if cp != 0 { - unsafe { SetConsoleOutputCP(CP_UTF8) }; - } - - RestoreConsoleCp(cp) -} +/// Sets the console code page and output modes. +/// +/// It currently doesn't bother to restore the original values on exit, because coreutils +/// relies on process::exit() and there are no exit hooks. It could be fixed if anyone ever +/// complains about it (and as always, it's probably going to be about ShiftJIS again). +/// +/// Technically this should also set the input mode, but +/// since we don't clean up on exit, that'd be quite risky. +fn set_console_modes() { + unsafe { + let cp = Console::GetConsoleOutputCP(); + if cp != 0 && cp != CP_UTF8 { + Console::SetConsoleOutputCP(CP_UTF8); + } -struct RestoreConsoleCp(u32); + let stdout = Console::GetStdHandle(Console::STD_OUTPUT_HANDLE); + let mut stdout_mode = 0; -impl Drop for RestoreConsoleCp { - fn drop(&mut self) { - if self.0 != 0 { - unsafe { SetConsoleOutputCP(self.0) }; + const EXPECTED_OUTPUT_MODE: u32 = Console::ENABLE_PROCESSED_OUTPUT + | Console::ENABLE_WRAP_AT_EOL_OUTPUT + | Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING; + if Console::GetConsoleMode(stdout, &raw mut stdout_mode) != 0 + && stdout_mode != EXPECTED_OUTPUT_MODE + { + Console::SetConsoleMode(stdout, EXPECTED_OUTPUT_MODE); } } } From 71f0fc10bccfa7f9b8a240ebe63238d76795aa6e Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 8 Jun 2026 20:59:44 +0200 Subject: [PATCH 2/2] Restore modes on exit --- deps/ntfind/src/argv.rs | 24 ++++---- deps/ntfind/src/buffer.rs | 52 +++++++++-------- deps/ntfind/src/io.rs | 27 +++++---- deps/ntfind/src/lib.rs | 25 ++++++--- deps/ntfind/src/main.rs | 8 ++- src/main.rs | 114 ++++++++++++++++++++++++++------------ 6 files changed, 157 insertions(+), 93 deletions(-) diff --git a/deps/ntfind/src/argv.rs b/deps/ntfind/src/argv.rs index 49fecdb..e373477 100644 --- a/deps/ntfind/src/argv.rs +++ b/deps/ntfind/src/argv.rs @@ -18,7 +18,7 @@ use windows_sys::Win32::System::Environment::GetCommandLineW; use crate::MSG_USAGE; use crate::buffer::WideString; -use crate::io::{exit, stderr_handle, write_stderr_str, write_stdout_str, write_to_handle}; +use crate::io::{StatusResult, stderr_handle, write_stderr_str, write_stdout_str, write_to_handle}; use crate::path::{directory_exists, full_path, path_state, query_device_len}; unsafe extern "C" { @@ -51,7 +51,7 @@ impl Default for FindState { } /// ARGUMENT_LEXEMIZER::PrepareToParse + DoParsing + MULTIPLE_PATH_ARGUMENT. -pub fn parse_command_line() -> (FindState, Vec) { +pub fn parse_command_line() -> StatusResult<(FindState, Vec)> { // QUIRK / BUG: ulib has two `ARGUMENT_LEXEMIZER::PutSeparators` overloads. // The PCWSTRING one folds `_WhiteSpace` and `_SwitchChars` into `_SeparatorString`, // while the PCSTR one has those two lines commented out. find.cxx passes a narrow string. @@ -59,7 +59,7 @@ pub fn parse_command_line() -> (FindState, Vec) { // Technically this could be trivially folded into the loop below. // I left it like it works in the original for now. - let lexemes = prepare_to_parse(); + let lexemes = prepare_to_parse()?; // ARGUMENT_LEXEMIZER::DoParsing, heavily modified & inlined. @@ -124,10 +124,10 @@ pub fn parse_command_line() -> (FindState, Vec) { if lexemes.next().is_some() { if seen.invalid_switch { write_stderr_str("FIND: Invalid switch\r\n"); - exit(2); + return Err(2); } else { write_stderr_str("FIND: Parameter format not correct\r\n"); - exit(2); + return Err(2); } } @@ -137,17 +137,17 @@ pub fn parse_command_line() -> (FindState, Vec) { w.push_wide(&failed_pattern); w.push_str("\r\n"); write_to_handle(stderr_handle(), &w); - exit(2); + return Err(2); } if seen.help { write_stdout_str(MSG_USAGE); - exit(0); + return Err(0); } if !seen.string_pattern { write_stderr_str("FIND: Parameter format not correct\r\n"); - exit(2); + return Err(2); } state.case_sensitive = !seen.case_insensitive; @@ -156,7 +156,7 @@ pub fn parse_command_line() -> (FindState, Vec) { state.output_line_numbers = seen.display_numbers; state.skip_offline_files = !seen.offline && !seen.off; - (state, paths) + Ok((state, paths)) } fn raw_command_line() -> &'static [u16] { @@ -194,7 +194,7 @@ impl Lexeme { } /// Contains logic from `ARGUMENT_LEXEMIZER::PrepareToParse`. -fn prepare_to_parse() -> Vec { +fn prepare_to_parse() -> StatusResult> { let cmd_line = raw_command_line(); let mut lexemes = Vec::new(); let mut pos = 0; @@ -231,7 +231,7 @@ fn prepare_to_parse() -> Vec { } None => { write_stderr_str("FIND: Parameter format not correct\r\n"); - exit(2); + return Err(2); } } } @@ -244,7 +244,7 @@ fn prepare_to_parse() -> Vec { lexemes.push(collapse_quoted_lexeme(&cmd_line[tok_start..pos])); } - lexemes + Ok(lexemes) } fn collapse_quoted_lexeme(raw: &'static [u16]) -> Lexeme { diff --git a/deps/ntfind/src/buffer.rs b/deps/ntfind/src/buffer.rs index f6e6ca5..77545ac 100644 --- a/deps/ntfind/src/buffer.rs +++ b/deps/ntfind/src/buffer.rs @@ -15,7 +15,8 @@ use windows_sys::Win32::Globalization::{ }; use windows_sys::Win32::Storage::FileSystem::{FILE_TYPE_DISK, GetFileType, ReadFile}; -use crate::io::{exit, input_code_page, write_stderr_str}; +use crate::io::StatusResult; +use crate::io::{input_code_page, write_stderr_str}; /// A helper for wchar_t string buffers. It's rather crude in this state, but it gets the job done. /// The original find would rely heavily on ulib's `PATH` type instead. @@ -196,29 +197,29 @@ impl BufferStream { } } - pub fn read_line(&mut self) -> Option<&[u16]> { + pub fn read_line(&mut self) -> StatusResult> { if self.stream_type == StreamType::Unknown { - self.detect_stream_type(); + self.detect_stream_type()?; } let line = if self.stream_type == StreamType::Unicode { - self.read_line_unicode()? + self.read_line_unicode() } else { - self.read_line_ansi()? + self.read_line_ansi() }; - Some(line.strip_suffix(&[0x0D]).unwrap_or(line)) + line.map(|opt| opt.map(|line| line.strip_suffix(&[0x0D]).unwrap_or(line))) } /// Detect whether the stream is ANSI or UTF-16LE. /// This replicates `BUFFER_STREAM::DetermineStreamType` and `BUFFER_STREAM::GetBuffer`. - fn detect_stream_type(&mut self) { + fn detect_stream_type(&mut self) -> StatusResult<()> { unsafe { self.stream_type = StreamType::Ansi; - self.fill_buffer(); + self.fill_buffer()?; if self.buffer.is_empty() { - return; + return Ok(()); } // QUIRK / BUG: @@ -245,11 +246,11 @@ impl BufferStream { if self.buffer.len() >= 2 { self.beg = 2; // skip BOM } - return; + return Ok(()); } if is_unicode == 0 { - return; + return Ok(()); } let flags_match = if GetFileType(self.handle) == FILE_TYPE_DISK { @@ -264,20 +265,22 @@ impl BufferStream { if flags_match && !self.buffer.iter().any(|&b| IsDBCSLeadByte(b) != 0) { self.stream_type = StreamType::Unicode; } + + Ok(()) } } - fn read_line_ansi(&mut self) -> Option<&[u16]> { + fn read_line_ansi(&mut self) -> StatusResult> { loop { if self.eof { - return if self.beg >= self.buffer.len() { + return Ok(if self.beg >= self.buffer.len() { None } else { let beg = self.beg; let end = self.buffer.len(); self.beg = self.buffer.len(); Some(self.convert_to_wide(beg, end)) - }; + }); } // Look for a line terminator and if found, yield that line. @@ -289,16 +292,16 @@ impl BufferStream { let end = self.scan + off; self.beg = end + 1; self.scan = self.beg; - return Some(self.convert_to_wide(beg, end)); + return Ok(Some(self.convert_to_wide(beg, end))); } // `buffer[beg..]` has no terminator. Remember that for the next scan. self.scan = self.buffer.len(); - self.fill_buffer(); + self.fill_buffer()?; } } - fn read_line_unicode(&mut self) -> Option<&[u16]> { + fn read_line_unicode(&mut self) -> StatusResult> { // QUIRK / BUG: // ulib's `BUFFER_STREAM::ReadString` does this, right after finding a newline with `wcscspn`: // if((BytesInBuffer & 0xfffe) != BytesInBuffer){ @@ -328,12 +331,12 @@ impl BufferStream { }; if self.eof { - return if beg >= wide.len() { + return Ok(if beg >= wide.len() { None } else { self.beg = self.buffer.len(); Some(&wide[beg..]) - }; + }); } // Look for a line terminator and if found, yield that line. @@ -341,16 +344,16 @@ impl BufferStream { let end = scan + off; self.beg = (end + 1) * 2; self.scan = self.beg; - return Some(&wide[beg..end]); + return Ok(Some(&wide[beg..end])); } // `buffer[beg..]` has no terminator. Remember that for the next scan. self.scan = self.buffer.len(); - self.fill_buffer(); + self.fill_buffer()?; } } - fn fill_buffer(&mut self) { + fn fill_buffer(&mut self) -> StatusResult<()> { unsafe { // Move the partial line to the beginning of the buffer. // The idea is that read() calls will either be very slow, and this doesn't matter, @@ -378,14 +381,15 @@ impl BufferStream { let err = GetLastError(); if err == ERROR_BROKEN_PIPE { self.eof = true; - return; + return Ok(()); } write_stderr_str("Unable to read file\r\n"); - exit(2); + return Err(2); } self.buffer.set_len(self.buffer.len() + bytes_read as usize); self.eof = bytes_read == 0; + Ok(()) } } diff --git a/deps/ntfind/src/io.rs b/deps/ntfind/src/io.rs index 89b1f7b..53250eb 100644 --- a/deps/ntfind/src/io.rs +++ b/deps/ntfind/src/io.rs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use core::hint::unreachable_unchecked; use core::ptr; use alloc::vec::Vec; @@ -12,10 +11,24 @@ use windows_sys::Win32::System::Console::{ GetConsoleCP, GetConsoleMode, GetStdHandle, STD_ERROR_HANDLE, STD_HANDLE, STD_OUTPUT_HANDLE, WriteConsoleW, }; -use windows_sys::Win32::System::Threading::{GetCurrentProcess, TerminateProcess}; use crate::buffer::WideString; +pub type StatusResult = core::result::Result; + +pub trait IntoStatus { + fn into_status(self) -> i32; +} + +impl IntoStatus for core::result::Result { + fn into_status(self) -> i32 { + match self { + Ok(_) => 0, + Err(code) => code, + } + } +} + pub struct OutputHandle { handle: HANDLE, is_console: bool, @@ -157,13 +170,3 @@ pub fn write_stdout_str(s: &str) { pub fn write_stderr_str(s: &str) { write_str(stderr_handle(), s); } - -/// Our own std::process::exit. It had not other place to live at. -pub fn exit(code: u32) -> ! { - unsafe { - // TerminateProcess is technically preferable nowadays, because it genuinely exits faster. - // Since we're an executable we have nothing to loose by using it. - TerminateProcess(GetCurrentProcess(), code); - unreachable_unchecked(); - } -} diff --git a/deps/ntfind/src/lib.rs b/deps/ntfind/src/lib.rs index e896fa9..abcab68 100644 --- a/deps/ntfind/src/lib.rs +++ b/deps/ntfind/src/lib.rs @@ -25,7 +25,10 @@ use windows_sys::Win32::System::Console::{GetStdHandle, STD_INPUT_HANDLE}; use crate::argv::parse_command_line; use crate::buffer::{BufferStream, WideString}; -use crate::io::{exit, io_init, stderr_handle, stdout_handle, write_stderr_str, write_to_handle}; +use crate::io::{ + IntoStatus as _, StatusResult, io_init, stderr_handle, stdout_handle, write_stderr_str, + write_to_handle, +}; use crate::path::is_drive_path; const MSG_USAGE: &str = concat!( @@ -46,14 +49,18 @@ const MSG_USAGE: &str = concat!( "or piped from another command.\r\n" ); -pub fn ntfind_main() -> ! { +pub fn ntfind_main() -> i32 { + main().into_status() +} + +fn main() -> StatusResult<()> { io_init(); - let (mut state, mut files) = parse_command_line(); + let (mut state, mut files) = parse_command_line()?; if files.is_empty() { let h_stdin = unsafe { GetStdHandle(STD_INPUT_HANDLE) }; - let lines_found = search_stream(&mut state, h_stdin); + let lines_found = search_stream(&mut state, h_stdin)?; if !state.output_lines { let mut out = WideString::new(); @@ -132,7 +139,7 @@ pub fn ntfind_main() -> ! { write_to_handle(stdout_handle(), &out); } - let lines_found = search_stream(&mut state, h_file); + let lines_found = search_stream(&mut state, h_file)?; if !state.output_lines { out.clear(); @@ -153,10 +160,10 @@ pub fn ntfind_main() -> ! { } } - exit(if state.found_any { 0 } else { 1 }); + if state.found_any { Ok(()) } else { Err(1) } } -fn search_stream(state: &mut argv::FindState, handle: HANDLE) -> u32 { +fn search_stream(state: &mut argv::FindState, handle: HANDLE) -> StatusResult { let mut line_count: u32 = 0; let mut found_count: u32 = 0; let pattern_len = state.pattern.len(); @@ -168,7 +175,7 @@ fn search_stream(state: &mut argv::FindState, handle: HANDLE) -> u32 { NORM_IGNORECASE }; - while let Some(line) = reader.read_line() { + while let Some(line) = reader.read_line()? { line_count += 1; // Look for pattern in the current line. A 0-length pattern ("") never matches. @@ -217,7 +224,7 @@ fn search_stream(state: &mut argv::FindState, handle: HANDLE) -> u32 { state.found_any = true; } - found_count + Ok(found_count) } /// Matches `IsDos5CompatibleFileName` in file.cxx. diff --git a/deps/ntfind/src/main.rs b/deps/ntfind/src/main.rs index 9a375a8..0080369 100644 --- a/deps/ntfind/src/main.rs +++ b/deps/ntfind/src/main.rs @@ -3,7 +3,13 @@ #![no_main] +use windows_sys::Win32::System::Threading::{GetCurrentProcess, TerminateProcess}; + #[unsafe(no_mangle)] extern "C" fn main() -> ! { - find::ntfind_main(); + unsafe { + let status = ntfind_main(); + TerminateProcess(GetCurrentProcess(), status as u32); + core::hint::unreachable_unchecked(); + } } diff --git a/src/main.rs b/src/main.rs index 3c63b89..42355e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,14 +12,21 @@ use std::cmp; use std::ffi::{OsStr, OsString}; use std::io::{self, Write as _, stderr}; use std::path::{Path, PathBuf}; -use std::process; +use std::sync::atomic::AtomicU32; use clap::Command; use itertools::Itertools as _; use uucore::display::Quotable as _; +use uucore::windows_sys::Win32::System::Threading::GetCurrentProcess; use uucore::{Args, error::strip_errno, locale}; use windows_sys::Win32::Globalization::CP_UTF8; use windows_sys::Win32::System::Console; +use windows_sys::Win32::System::Threading::TerminateProcess; + +unsafe extern "C" { + fn atexit(cb: extern "C" fn()) -> i32; + unsafe fn ntsort_main(argc: i32, argv: *const *const u8) -> i32; +} const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -51,7 +58,7 @@ Currently defined functions: && e.kind() != io::ErrorKind::BrokenPipe { let _ = writeln!(io::stderr(), "coreutils: {}", strip_errno(&e)); - process::exit(1); + exit(1); } } @@ -69,7 +76,7 @@ fn main() { let binary = binary_path(&mut args); let binary_as_util = name(&binary).unwrap_or_else(|| { usage(&utils, ""); - process::exit(0); + exit(0); }); // binary name ends with util name? @@ -100,7 +107,7 @@ fn main() { // we should fail with additional args https://github.com/uutils/coreutils/issues/11383#issuecomment-4082564058 if args.next().is_some() { let _ = writeln!(io::stderr(), "coreutils: invalid argument"); - process::exit(1); + exit(1); } let mut out = io::stdout().lock(); for util in utils.keys() { @@ -108,19 +115,19 @@ fn main() { && e.kind() != io::ErrorKind::BrokenPipe { let _ = writeln!(io::stderr(), "coreutils: {}", strip_errno(&e)); - process::exit(1); + exit(1); } } - process::exit(0); + exit(0); } "--version" | "-V" => { if let Err(e) = writeln!(io::stdout(), "coreutils {VERSION} (multi-call binary)") && e.kind() != io::ErrorKind::BrokenPipe { let _ = writeln!(io::stderr(), "coreutils: {}", strip_errno(&e)); - process::exit(1); + exit(1); } - process::exit(0); + exit(0); } // Not a special command: fallthrough to calling a util _ => {} @@ -134,7 +141,7 @@ fn main() { // Could be something like: // #[cfg(not(feature = "only_english"))] setup_localization_or_exit(util); - process::exit(uumain(vec![util_os].into_iter().chain(args))); + exit(uumain(vec![util_os].into_iter().chain(args))); } None => { if util == "--help" || util == "-h" { @@ -153,13 +160,13 @@ fn main() { .chain(args), ); io::stdout().flush().expect("could not flush stdout"); - process::exit(code); + exit(code); } None => not_found(&util_os), } } usage(&utils, binary_as_util); - process::exit(0); + exit(0); } else if util.starts_with('-') { // Argument looks like an option but wasn't recognized unrecognized_option(binary_as_util, &util_os); @@ -176,7 +183,7 @@ fn main() { } else { let _ = writeln!(io::stderr(), "coreutils: missing argument"); } - process::exit(1); + exit(1); } } @@ -198,7 +205,7 @@ fn not_found(util: &OsStr) -> ! { "coreutils: unknown program '{}'", util.maybe_quote() ); - process::exit(1); + exit(1); } fn unrecognized_option(binary_name: &str, option: &OsStr) -> ! { @@ -207,7 +214,7 @@ fn unrecognized_option(binary_name: &str, option: &OsStr) -> ! { "{binary_name}: unrecognized option '{}'", option.to_string_lossy() ); - process::exit(1); + exit(1); } fn setup_localization_or_exit(util_name: &str) { @@ -220,7 +227,7 @@ fn setup_localization_or_exit(util_name: &str) { } => eprintln!("Localization parse error at {snippet}: {err_msg}"), other => eprintln!("Could not init the localization system: {other}"), } - process::exit(99) + exit(99) }); } @@ -236,37 +243,69 @@ fn get_canonical_util_name(util_name: &str) -> &str { } } +const EXPECTED_OUTPUT_MODE: u32 = Console::ENABLE_PROCESSED_OUTPUT + | Console::ENABLE_WRAP_AT_EOL_OUTPUT + | Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING; + +static ORIGINAL_OUTPUT_CP: AtomicU32 = AtomicU32::new(CP_UTF8); +static ORIGINAL_OUTPUT_MODE: AtomicU32 = AtomicU32::new(EXPECTED_OUTPUT_MODE); + /// Sets the console code page and output modes. -/// -/// It currently doesn't bother to restore the original values on exit, because coreutils -/// relies on process::exit() and there are no exit hooks. It could be fixed if anyone ever -/// complains about it (and as always, it's probably going to be about ShiftJIS again). -/// -/// Technically this should also set the input mode, but -/// since we don't clean up on exit, that'd be quite risky. fn set_console_modes() { unsafe { - let cp = Console::GetConsoleOutputCP(); - if cp != 0 && cp != CP_UTF8 { + let mut cp = Console::GetConsoleOutputCP(); + if cp == 0 { + cp = CP_UTF8; + } + if cp != CP_UTF8 { Console::SetConsoleOutputCP(CP_UTF8); } let stdout = Console::GetStdHandle(Console::STD_OUTPUT_HANDLE); - let mut stdout_mode = 0; - - const EXPECTED_OUTPUT_MODE: u32 = Console::ENABLE_PROCESSED_OUTPUT - | Console::ENABLE_WRAP_AT_EOL_OUTPUT - | Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING; - if Console::GetConsoleMode(stdout, &raw mut stdout_mode) != 0 - && stdout_mode != EXPECTED_OUTPUT_MODE - { + let mut mode = 0; + if Console::GetConsoleMode(stdout, &raw mut mode) == 0 { + mode = EXPECTED_OUTPUT_MODE; + } + if mode != EXPECTED_OUTPUT_MODE { Console::SetConsoleMode(stdout, EXPECTED_OUTPUT_MODE); } + + ORIGINAL_OUTPUT_CP.store(cp, std::sync::atomic::Ordering::Relaxed); + ORIGINAL_OUTPUT_MODE.store(mode, std::sync::atomic::Ordering::Relaxed); + + atexit(restore_console_modes); } } -unsafe extern "C" { - unsafe fn ntsort_main(argc: i32, argv: *const *const u8) -> i32; +extern "C" fn restore_console_modes() { + unsafe { + let cp = ORIGINAL_OUTPUT_CP.load(std::sync::atomic::Ordering::Relaxed); + let mode = ORIGINAL_OUTPUT_MODE.load(std::sync::atomic::Ordering::Relaxed); + + if cp != CP_UTF8 { + Console::SetConsoleOutputCP(cp); + } + + if mode != EXPECTED_OUTPUT_MODE { + let stdout = Console::GetStdHandle(Console::STD_OUTPUT_HANDLE); + Console::SetConsoleMode(stdout, mode); + } + } +} + +/// Restores the console modes and then exits. +/// +/// NOTE: Regular uutils/coreutils calls std::process::exit, which flushes stdout buffers. +/// However, the utils are designed such that they can be used in-proc so +/// if they didn't flush themselves, they would be considered buggy anyway. +/// +/// TerminateProcess is used because it is technically superior to ExitProcess. +fn exit(code: i32) -> ! { + unsafe { + restore_console_modes(); + TerminateProcess(GetCurrentProcess(), code as u32); + std::hint::unreachable_unchecked(); + } } fn find_uumain(args: T) -> i32 { @@ -317,7 +356,12 @@ fn sort_uumain(args: T) -> i32 { arg.push("\0"); } let ptrs: Vec<_> = args.iter().map(|v| v.as_encoded_bytes().as_ptr()).collect(); - unsafe { ntsort_main(ptrs.len() as i32, ptrs.as_ptr()) } + + unsafe { + // NOTE: ntsort calls exit() on /? and on error. + atexit(restore_console_modes); + ntsort_main(ptrs.len() as i32, ptrs.as_ptr()) + } } else { sort::uumain(args.into_iter()) }