From 64e023a2c0d17899fe4ac12cf557e5694e25e090 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 18 Feb 2026 01:31:10 +0900 Subject: [PATCH 1/7] Add missing _winapi functions and fix WinHandle int conversion Add 13 functions: ReadFile, SetNamedPipeHandleState, CreateFileMapping, OpenFileMapping, MapViewOfFile, UnmapViewOfFile, VirtualQuerySize, CopyFile2, ResetEvent, CreateMutex, OpenEventW, LoadLibrary, _mimetypes_read_windows_registry. Add constants: INVALID_HANDLE_VALUE, FILE_MAP_READ/WRITE/COPY/EXECUTE. Change WinHandle integer type from usize to isize so negative values like INVALID_HANDLE_VALUE (-1) can be passed from Python. --- crates/vm/src/stdlib/winapi.rs | 463 ++++++++++++++++++++++++++++++++- crates/vm/src/windows.rs | 2 +- 2 files changed, 455 insertions(+), 10 deletions(-) diff --git a/crates/vm/src/stdlib/winapi.rs b/crates/vm/src/stdlib/winapi.rs index c176568cfc5..30fa799d56a 100644 --- a/crates/vm/src/stdlib/winapi.rs +++ b/crates/vm/src/stdlib/winapi.rs @@ -15,7 +15,7 @@ mod _winapi { windows::{WinHandle, WindowsSysResult}, }; use core::ptr::{null, null_mut}; - use windows_sys::Win32::Foundation::{HANDLE, INVALID_HANDLE_VALUE, MAX_PATH}; + use windows_sys::Win32::Foundation::{HANDLE, MAX_PATH}; #[pyattr] use windows_sys::Win32::{ @@ -87,7 +87,8 @@ mod _winapi { System::{ Console::{STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE}, Memory::{ - FILE_MAP_ALL_ACCESS, MEM_COMMIT, MEM_FREE, MEM_IMAGE, MEM_MAPPED, MEM_PRIVATE, + FILE_MAP_ALL_ACCESS, FILE_MAP_COPY, FILE_MAP_EXECUTE, FILE_MAP_READ, + FILE_MAP_WRITE, MEM_COMMIT, MEM_FREE, MEM_IMAGE, MEM_MAPPED, MEM_PRIVATE, MEM_RESERVE, PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY, PAGE_GUARD, PAGE_NOACCESS, PAGE_NOCACHE, PAGE_READONLY, PAGE_READWRITE, PAGE_WRITECOMBINE, PAGE_WRITECOPY, SEC_COMMIT, SEC_IMAGE, @@ -113,6 +114,9 @@ mod _winapi { #[pyattr] const NULL: isize = 0; + #[pyattr] + const INVALID_HANDLE_VALUE: isize = -1; + #[pyfunction] fn CloseHandle(handle: WinHandle) -> WindowsSysResult { WindowsSysResult(unsafe { windows_sys::Win32::Foundation::CloseHandle(handle.0) }) @@ -150,7 +154,7 @@ mod _winapi { ) }; - if handle == INVALID_HANDLE_VALUE { + if handle == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE { return Err(vm.new_last_os_error()); } @@ -163,7 +167,7 @@ mod _winapi { vm: &VirtualMachine, ) -> PyResult> { let handle = unsafe { windows_sys::Win32::System::Console::GetStdHandle(std_handle) }; - if handle == INVALID_HANDLE_VALUE { + if handle == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE { return Err(vm.new_last_os_error()); } Ok(if handle.is_null() { @@ -638,8 +642,7 @@ mod _winapi { }) } - // TODO: ctypes.LibraryLoader.LoadLibrary - #[allow(dead_code)] + #[pyfunction] fn LoadLibrary(path: PyStrRef, vm: &VirtualMachine) -> PyResult { let path_wide = path.as_wtf8().to_wide_with_nul(); let handle = @@ -684,7 +687,7 @@ mod _winapi { name_wide.as_ptr(), ) }; - if handle == INVALID_HANDLE_VALUE { + if handle.is_null() { return Err(vm.new_last_os_error()); } Ok(handle as _) @@ -815,7 +818,7 @@ mod _winapi { ) }; - if handle == INVALID_HANDLE_VALUE { + if handle == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE { return Err(vm.new_last_os_error()); } @@ -1210,7 +1213,7 @@ mod _winapi { ) }; - if handle == INVALID_HANDLE_VALUE { + if handle == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE { return Err(vm.new_last_os_error()); } @@ -1316,6 +1319,177 @@ mod _winapi { .into()) } + /// ReadFile - Read data from a file or I/O device. + #[pyfunction] + fn ReadFile( + handle: WinHandle, + size: u32, + use_overlapped: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + use windows_sys::Win32::Storage::FileSystem::ReadFile as WinReadFile; + + let use_overlapped = use_overlapped.unwrap_or(false); + + if use_overlapped { + use windows_sys::Win32::Foundation::ERROR_IO_PENDING; + + let ov = Overlapped::new_with_handle(handle.0); + let err = { + let mut inner = ov.inner.lock(); + inner.read_buffer = Some(vec![0u8; size as usize]); + let read_buf = inner.read_buffer.as_mut().unwrap(); + let mut nread: u32 = 0; + let ret = unsafe { + WinReadFile( + handle.0, + read_buf.as_mut_ptr() as *mut _, + size, + &mut nread, + &mut inner.overlapped, + ) + }; + + let err = if ret == 0 { + unsafe { windows_sys::Win32::Foundation::GetLastError() } + } else { + 0 + }; + + if ret == 0 && err != ERROR_IO_PENDING && err != ERROR_MORE_DATA { + return Err(vm.new_last_os_error()); + } + if ret == 0 && err == ERROR_IO_PENDING { + inner.pending = true; + } + + err + }; + let result = vm + .ctx + .new_tuple(vec![ov.into_pyobject(vm), vm.ctx.new_int(err).into()]); + return Ok(result.into()); + } + + let mut buf = vec![0u8; size as usize]; + let mut nread: u32 = 0; + let ret = unsafe { + WinReadFile( + handle.0, + buf.as_mut_ptr() as *mut _, + size, + &mut nread, + null_mut(), + ) + }; + let err = if ret == 0 { + unsafe { windows_sys::Win32::Foundation::GetLastError() } + } else { + 0 + }; + if ret == 0 && err != ERROR_MORE_DATA { + return Err(vm.new_last_os_error()); + } + buf.truncate(nread as usize); + Ok(vm + .ctx + .new_tuple(vec![ + vm.ctx.new_bytes(buf).into(), + vm.ctx.new_int(err).into(), + ]) + .into()) + } + + /// SetNamedPipeHandleState - Set the read mode and other options of a named pipe. + #[pyfunction] + fn SetNamedPipeHandleState( + named_pipe: WinHandle, + mode: PyObjectRef, + max_collection_count: PyObjectRef, + collect_data_timeout: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + use windows_sys::Win32::System::Pipes::SetNamedPipeHandleState as WinSetNamedPipeHandleState; + + let mut dw_args: [u32; 3] = [0; 3]; + let mut p_args: [*mut u32; 3] = [null_mut(); 3]; + + let objs = [&mode, &max_collection_count, &collect_data_timeout]; + for (i, obj) in objs.iter().enumerate() { + if !vm.is_none(obj) { + dw_args[i] = u32::try_from_object(vm, (*obj).clone())?; + p_args[i] = &mut dw_args[i]; + } + } + + let ret = + unsafe { WinSetNamedPipeHandleState(named_pipe.0, p_args[0], p_args[1], p_args[2]) }; + + if ret == 0 { + return Err(vm.new_last_os_error()); + } + Ok(()) + } + + /// ResetEvent - Reset the specified event object to the nonsignaled state. + #[pyfunction] + fn ResetEvent(event: WinHandle, vm: &VirtualMachine) -> PyResult<()> { + use windows_sys::Win32::System::Threading::ResetEvent as WinResetEvent; + + let ret = unsafe { WinResetEvent(event.0) }; + if ret == 0 { + return Err(vm.new_last_os_error()); + } + Ok(()) + } + + /// CreateMutexW - Create or open a named or unnamed mutex object. + #[pyfunction] + fn CreateMutex( + security_attributes: isize, + initial_owner: bool, + name: Option, + vm: &VirtualMachine, + ) -> PyResult { + use windows_sys::Win32::System::Threading::CreateMutexW as WinCreateMutexW; + + let _ = security_attributes; + let name_wide = name.map(|n| n.as_wtf8().to_wide_with_nul()); + let name_ptr = name_wide.as_ref().map_or(null(), |n| n.as_ptr()); + + let handle = unsafe { WinCreateMutexW(null(), i32::from(initial_owner), name_ptr) }; + + if handle.is_null() { + return Err(vm.new_last_os_error()); + } + Ok(WinHandle(handle)) + } + + /// OpenEventW - Open an existing named event object. + #[pyfunction] + fn OpenEventW( + desired_access: u32, + inherit_handle: bool, + name: PyStrRef, + vm: &VirtualMachine, + ) -> PyResult { + use windows_sys::Win32::System::Threading::OpenEventW as WinOpenEventW; + + let name_wide = name.as_wtf8().to_wide_with_nul(); + let handle = unsafe { + WinOpenEventW( + desired_access, + i32::from(inherit_handle), + name_wide.as_ptr(), + ) + }; + + if handle.is_null() { + return Err(vm.new_last_os_error()); + } + Ok(WinHandle(handle)) + } + const MAXIMUM_WAIT_OBJECTS: usize = 64; /// BatchedWaitForMultipleObjects - Wait for multiple handles, supporting more than 64. @@ -1642,4 +1816,275 @@ mod _winapi { Ok(vm.ctx.new_list(triggered_indices).into()) } } + + /// CreateFileMapping - Create or open a named or unnamed file mapping object. + #[pyfunction] + fn CreateFileMapping( + file_handle: WinHandle, + _security_attributes: PyObjectRef, + protect: u32, + max_size_high: u32, + max_size_low: u32, + name: Option, + vm: &VirtualMachine, + ) -> PyResult { + use windows_sys::Win32::System::Memory::CreateFileMappingW; + + let name_wide = name.as_ref().map(|n| n.as_wtf8().to_wide_with_nul()); + let name_ptr = name_wide.as_ref().map_or(null(), |n| n.as_ptr()); + + let handle = unsafe { + CreateFileMappingW( + file_handle.0, + null(), + protect, + max_size_high, + max_size_low, + name_ptr, + ) + }; + + if handle.is_null() { + return Err(vm.new_last_os_error()); + } + Ok(WinHandle(handle)) + } + + /// OpenFileMapping - Open a named file mapping object. + #[pyfunction] + fn OpenFileMapping( + desired_access: u32, + inherit_handle: bool, + name: PyStrRef, + vm: &VirtualMachine, + ) -> PyResult { + use windows_sys::Win32::System::Memory::OpenFileMappingW; + + let name_wide = name.as_wtf8().to_wide_with_nul(); + let handle = unsafe { + OpenFileMappingW( + desired_access, + i32::from(inherit_handle), + name_wide.as_ptr(), + ) + }; + + if handle.is_null() { + return Err(vm.new_last_os_error()); + } + Ok(WinHandle(handle)) + } + + /// MapViewOfFile - Map a view of a file mapping into the address space. + #[pyfunction] + fn MapViewOfFile( + file_map: WinHandle, + desired_access: u32, + file_offset_high: u32, + file_offset_low: u32, + number_bytes: usize, + vm: &VirtualMachine, + ) -> PyResult { + let address = unsafe { + windows_sys::Win32::System::Memory::MapViewOfFile( + file_map.0, + desired_access, + file_offset_high, + file_offset_low, + number_bytes, + ) + }; + + let ptr = address.Value; + if ptr.is_null() { + return Err(vm.new_last_os_error()); + } + Ok(ptr as isize) + } + + /// UnmapViewOfFile - Unmap a mapped view of a file. + #[pyfunction] + fn UnmapViewOfFile(address: isize, vm: &VirtualMachine) -> PyResult<()> { + use windows_sys::Win32::System::Memory::MEMORY_MAPPED_VIEW_ADDRESS; + + let view = MEMORY_MAPPED_VIEW_ADDRESS { + Value: address as *mut core::ffi::c_void, + }; + let ret = unsafe { windows_sys::Win32::System::Memory::UnmapViewOfFile(view) }; + + if ret == 0 { + return Err(vm.new_last_os_error()); + } + Ok(()) + } + + /// VirtualQuerySize - Return the size of a memory region. + #[pyfunction] + fn VirtualQuerySize(address: isize, vm: &VirtualMachine) -> PyResult { + use windows_sys::Win32::System::Memory::{MEMORY_BASIC_INFORMATION, VirtualQuery}; + + let mut mbi: MEMORY_BASIC_INFORMATION = unsafe { core::mem::zeroed() }; + let ret = unsafe { + VirtualQuery( + address as *const core::ffi::c_void, + &mut mbi, + core::mem::size_of::(), + ) + }; + + if ret == 0 { + return Err(vm.new_last_os_error()); + } + Ok(mbi.RegionSize) + } + + /// CopyFile2 - Copy a file with extended parameters. + #[pyfunction] + fn CopyFile2( + existing_file_name: PyStrRef, + new_file_name: PyStrRef, + flags: u32, + _progress_routine: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult<()> { + use windows_sys::Win32::Storage::FileSystem::{ + COPYFILE2_EXTENDED_PARAMETERS, CopyFile2 as WinCopyFile2, + }; + + let src_wide = existing_file_name.as_wtf8().to_wide_with_nul(); + let dst_wide = new_file_name.as_wtf8().to_wide_with_nul(); + + let mut params: COPYFILE2_EXTENDED_PARAMETERS = unsafe { core::mem::zeroed() }; + params.dwSize = core::mem::size_of::() as u32; + params.dwCopyFlags = flags; + + let hr = unsafe { WinCopyFile2(src_wide.as_ptr(), dst_wide.as_ptr(), ¶ms) }; + + if hr < 0 { + // HRESULT failure - convert to Windows error code + let err = if (hr as u32 >> 16) == 0x8007 { + (hr as u32) & 0xFFFF + } else { + hr as u32 + }; + return Err(vm.new_os_error(err as i32)); + } + Ok(()) + } + + /// _mimetypes_read_windows_registry - Read MIME type associations from registry. + #[pyfunction] + fn _mimetypes_read_windows_registry( + on_type_read: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + use windows_sys::Win32::System::Registry::{ + HKEY, HKEY_CLASSES_ROOT, KEY_READ, REG_SZ, RegCloseKey, RegEnumKeyExW, RegOpenKeyExW, + RegQueryValueExW, + }; + + let mut hkcr: HKEY = null_mut() as HKEY; + let err = unsafe { RegOpenKeyExW(HKEY_CLASSES_ROOT, null(), 0, KEY_READ, &mut hkcr) }; + if err != 0 { + return Err(vm.new_os_error(err as i32)); + } + + let mut i: u32 = 0; + let mut entries: Vec<(String, String)> = Vec::new(); + + loop { + let mut ext_buf = [0u16; 128]; + let mut cch_ext: u32 = ext_buf.len() as u32; + + let err = unsafe { + RegEnumKeyExW( + hkcr, + i, + ext_buf.as_mut_ptr(), + &mut cch_ext, + null_mut(), + null_mut(), + null_mut(), + null_mut(), + ) + }; + i += 1; + + if err == windows_sys::Win32::Foundation::ERROR_NO_MORE_ITEMS { + break; + } + if err != 0 && err != windows_sys::Win32::Foundation::ERROR_MORE_DATA { + unsafe { RegCloseKey(hkcr) }; + return Err(vm.new_os_error(err as i32)); + } + + // Only process keys starting with '.' + if cch_ext == 0 || ext_buf[0] != b'.' as u16 { + continue; + } + + let ext_wide = &ext_buf[..cch_ext as usize]; + + // Open subkey to read Content Type + let mut subkey: HKEY = null_mut() as HKEY; + let err = unsafe { RegOpenKeyExW(hkcr, ext_buf.as_ptr(), 0, KEY_READ, &mut subkey) }; + if err == windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND + || err == windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED + { + continue; + } + if err != 0 { + unsafe { RegCloseKey(hkcr) }; + return Err(vm.new_os_error(err as i32)); + } + + let content_type_key: Vec = "Content Type\0".encode_utf16().collect(); + let mut type_buf = [0u16; 256]; + let mut cb_type: u32 = (type_buf.len() * 2) as u32; + let mut reg_type: u32 = 0; + + let err = unsafe { + RegQueryValueExW( + subkey, + content_type_key.as_ptr(), + null_mut(), + &mut reg_type, + type_buf.as_mut_ptr() as *mut u8, + &mut cb_type, + ) + }; + unsafe { RegCloseKey(subkey) }; + + if err != 0 || reg_type != REG_SZ || cb_type == 0 { + continue; + } + + // Convert wide strings to Rust strings + let type_len = (cb_type as usize / 2).saturating_sub(1); // exclude null terminator + let type_str = String::from_utf16_lossy(&type_buf[..type_len]); + let ext_str = String::from_utf16_lossy(ext_wide); + + if type_str.is_empty() { + continue; + } + + entries.push((type_str, ext_str)); + + // Flush buffer periodically to call Python callback + if entries.len() >= 64 { + for (mime_type, ext) in entries.drain(..) { + on_type_read.call((vm.ctx.new_str(mime_type), vm.ctx.new_str(ext)), vm)?; + } + } + } + + unsafe { RegCloseKey(hkcr) }; + + // Process remaining entries + for (mime_type, ext) in entries { + on_type_read.call((vm.ctx.new_str(mime_type), vm.ctx.new_str(ext)), vm)?; + } + + Ok(()) + } } diff --git a/crates/vm/src/windows.rs b/crates/vm/src/windows.rs index 017384e2f5c..ec9ac8f18fb 100644 --- a/crates/vm/src/windows.rs +++ b/crates/vm/src/windows.rs @@ -61,7 +61,7 @@ impl ToPyResult for WindowsSysResult { } } -type HandleInt = usize; // TODO: change to isize when fully ported to windows-rs +type HandleInt = isize; impl TryFromObject for WinHandle { fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { From 8500b21d0c4225aceca365aa3c3197bcbfac668b Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 18 Feb 2026 02:28:38 +0900 Subject: [PATCH 2/7] Align _winapi module with CPython - Rename winapi.rs to _winapi.rs with #[path] attribute - Rename CreateMutex to CreateMutexW - Add missing constants: ERROR_ACCESS_DENIED, ERROR_PRIVILEGE_NOT_HELD, PROCESS_ALL_ACCESS, 10 STARTF_ constants, LOCALE_NAME_SYSTEM_DEFAULT, LOCALE_NAME_USER_DEFAULT, COPY_FILE_DIRECTORY - Fix OpenMutexW return type and ReleaseMutex param type to use WinHandle --- .../vm/src/stdlib/{winapi.rs => _winapi.rs} | 41 +++++++++++++------ crates/vm/src/stdlib/mod.rs | 1 + 2 files changed, 29 insertions(+), 13 deletions(-) rename crates/vm/src/stdlib/{winapi.rs => _winapi.rs} (98%) diff --git a/crates/vm/src/stdlib/winapi.rs b/crates/vm/src/stdlib/_winapi.rs similarity index 98% rename from crates/vm/src/stdlib/winapi.rs rename to crates/vm/src/stdlib/_winapi.rs index 30fa799d56a..a76efafbc61 100644 --- a/crates/vm/src/stdlib/winapi.rs +++ b/crates/vm/src/stdlib/_winapi.rs @@ -20,11 +20,12 @@ mod _winapi { #[pyattr] use windows_sys::Win32::{ Foundation::{ - DUPLICATE_CLOSE_SOURCE, DUPLICATE_SAME_ACCESS, ERROR_ALREADY_EXISTS, ERROR_BROKEN_PIPE, - ERROR_IO_PENDING, ERROR_MORE_DATA, ERROR_NETNAME_DELETED, ERROR_NO_DATA, - ERROR_NO_SYSTEM_RESOURCES, ERROR_OPERATION_ABORTED, ERROR_PIPE_BUSY, - ERROR_PIPE_CONNECTED, ERROR_SEM_TIMEOUT, GENERIC_READ, GENERIC_WRITE, STILL_ACTIVE, - WAIT_ABANDONED, WAIT_ABANDONED_0, WAIT_OBJECT_0, WAIT_TIMEOUT, + DUPLICATE_CLOSE_SOURCE, DUPLICATE_SAME_ACCESS, ERROR_ACCESS_DENIED, + ERROR_ALREADY_EXISTS, ERROR_BROKEN_PIPE, ERROR_IO_PENDING, ERROR_MORE_DATA, + ERROR_NETNAME_DELETED, ERROR_NO_DATA, ERROR_NO_SYSTEM_RESOURCES, + ERROR_OPERATION_ABORTED, ERROR_PIPE_BUSY, ERROR_PIPE_CONNECTED, + ERROR_PRIVILEGE_NOT_HELD, ERROR_SEM_TIMEOUT, GENERIC_READ, GENERIC_WRITE, + STILL_ACTIVE, WAIT_ABANDONED, WAIT_ABANDONED_0, WAIT_OBJECT_0, WAIT_TIMEOUT, }, Globalization::{ LCMAP_FULLWIDTH, LCMAP_HALFWIDTH, LCMAP_HIRAGANA, LCMAP_KATAKANA, @@ -103,9 +104,12 @@ mod _winapi { ABOVE_NORMAL_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, CREATE_BREAKAWAY_FROM_JOB, CREATE_DEFAULT_ERROR_MODE, CREATE_NEW_CONSOLE, CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW, DETACHED_PROCESS, HIGH_PRIORITY_CLASS, - IDLE_PRIORITY_CLASS, INFINITE, NORMAL_PRIORITY_CLASS, PROCESS_DUP_HANDLE, - REALTIME_PRIORITY_CLASS, STARTF_FORCEOFFFEEDBACK, STARTF_FORCEONFEEDBACK, - STARTF_USESHOWWINDOW, STARTF_USESTDHANDLES, + IDLE_PRIORITY_CLASS, INFINITE, NORMAL_PRIORITY_CLASS, PROCESS_ALL_ACCESS, + PROCESS_DUP_HANDLE, REALTIME_PRIORITY_CLASS, STARTF_FORCEOFFFEEDBACK, + STARTF_FORCEONFEEDBACK, STARTF_PREVENTPINNING, STARTF_RUNFULLSCREEN, + STARTF_TITLEISAPPID, STARTF_TITLEISLINKNAME, STARTF_UNTRUSTEDSOURCE, + STARTF_USECOUNTCHARS, STARTF_USEFILLATTRIBUTE, STARTF_USEHOTKEY, + STARTF_USEPOSITION, STARTF_USESHOWWINDOW, STARTF_USESIZE, STARTF_USESTDHANDLES, }, }, UI::WindowsAndMessaging::SW_HIDE, @@ -117,6 +121,9 @@ mod _winapi { #[pyattr] const INVALID_HANDLE_VALUE: isize = -1; + #[pyattr] + const COPY_FILE_DIRECTORY: u32 = 0x00000080; + #[pyfunction] fn CloseHandle(handle: WinHandle) -> WindowsSysResult { WindowsSysResult(unsafe { windows_sys::Win32::Foundation::CloseHandle(handle.0) }) @@ -678,7 +685,7 @@ mod _winapi { inherit_handle: bool, name: PyStrRef, vm: &VirtualMachine, - ) -> PyResult { + ) -> PyResult { let name_wide = name.as_wtf8().to_wide_with_nul(); let handle = unsafe { windows_sys::Win32::System::Threading::OpenMutexW( @@ -690,13 +697,13 @@ mod _winapi { if handle.is_null() { return Err(vm.new_last_os_error()); } - Ok(handle as _) + Ok(WinHandle(handle)) } #[pyfunction] - fn ReleaseMutex(handle: isize) -> WindowsSysResult { + fn ReleaseMutex(handle: WinHandle) -> WindowsSysResult { WindowsSysResult(unsafe { - windows_sys::Win32::System::Threading::ReleaseMutex(handle as _) + windows_sys::Win32::System::Threading::ReleaseMutex(handle.0) }) } @@ -704,6 +711,14 @@ mod _winapi { #[pyattr] const LOCALE_NAME_INVARIANT: &str = ""; + #[pyattr] + const LOCALE_NAME_SYSTEM_DEFAULT: &str = "!x-sys-default-locale"; + + #[pyattr(name = "LOCALE_NAME_USER_DEFAULT")] + fn locale_name_user_default(vm: &VirtualMachine) -> PyObjectRef { + vm.ctx.none() + } + /// LCMapStringEx - Map a string to another string using locale-specific rules /// This is used by ntpath.normcase() for proper Windows case conversion #[pyfunction] @@ -1445,7 +1460,7 @@ mod _winapi { /// CreateMutexW - Create or open a named or unnamed mutex object. #[pyfunction] - fn CreateMutex( + fn CreateMutexW( security_attributes: isize, initial_owner: bool, name: Option, diff --git a/crates/vm/src/stdlib/mod.rs b/crates/vm/src/stdlib/mod.rs index e3bf42a4f77..f4f266bf161 100644 --- a/crates/vm/src/stdlib/mod.rs +++ b/crates/vm/src/stdlib/mod.rs @@ -64,6 +64,7 @@ mod _wmi; pub(crate) mod signal; pub mod sys; #[cfg(all(feature = "host_env", windows))] +#[path = "_winapi.rs"] mod winapi; #[cfg(all(feature = "host_env", windows))] mod winreg; From 22c3f120a7b9ef4f0bd00b1633087ca34ee91566 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 18 Feb 2026 02:49:18 +0900 Subject: [PATCH 3/7] Fix ReadFile/WriteFile overlapped keyword argument Use FromArgs structs so overlapped parameter can be passed as a keyword argument (overlapped=True), matching the CPython API. --- crates/vm/src/stdlib/_winapi.rs | 43 +++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/crates/vm/src/stdlib/_winapi.rs b/crates/vm/src/stdlib/_winapi.rs index a76efafbc61..f2d8613da27 100644 --- a/crates/vm/src/stdlib/_winapi.rs +++ b/crates/vm/src/stdlib/_winapi.rs @@ -1253,18 +1253,24 @@ mod _winapi { Ok(()) } - /// WriteFile - Write data to a file or I/O device. - #[pyfunction] - fn WriteFile( + #[derive(FromArgs)] + struct WriteFileArgs { + #[pyarg(positional)] handle: WinHandle, + #[pyarg(positional)] buffer: crate::function::ArgBytesLike, - use_overlapped: OptionalArg, - vm: &VirtualMachine, - ) -> PyResult { + #[pyarg(named, default = false)] + overlapped: bool, + } + + /// WriteFile - Write data to a file or I/O device. + #[pyfunction] + fn WriteFile(args: WriteFileArgs, vm: &VirtualMachine) -> PyResult { use windows_sys::Win32::Storage::FileSystem::WriteFile as WinWriteFile; - let use_overlapped = use_overlapped.unwrap_or(false); - let buf = buffer.borrow_buf(); + let handle = args.handle; + let use_overlapped = args.overlapped; + let buf = args.buffer.borrow_buf(); let len = core::cmp::min(buf.len(), u32::MAX as usize) as u32; if use_overlapped { @@ -1334,17 +1340,24 @@ mod _winapi { .into()) } - /// ReadFile - Read data from a file or I/O device. - #[pyfunction] - fn ReadFile( + #[derive(FromArgs)] + struct ReadFileArgs { + #[pyarg(positional)] handle: WinHandle, + #[pyarg(positional)] size: u32, - use_overlapped: OptionalArg, - vm: &VirtualMachine, - ) -> PyResult { + #[pyarg(named, default = false)] + overlapped: bool, + } + + /// ReadFile - Read data from a file or I/O device. + #[pyfunction] + fn ReadFile(args: ReadFileArgs, vm: &VirtualMachine) -> PyResult { use windows_sys::Win32::Storage::FileSystem::ReadFile as WinReadFile; - let use_overlapped = use_overlapped.unwrap_or(false); + let handle = args.handle; + let size = args.size; + let use_overlapped = args.overlapped; if use_overlapped { use windows_sys::Win32::Foundation::ERROR_IO_PENDING; From f18a2526a08b7134dccb3c5b541dd4d412e08802 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 18 Feb 2026 02:58:12 +0900 Subject: [PATCH 4/7] Remove extra constants and LoadLibrary not in CPython _winapi Remove 19 constants (WAIT_ABANDONED, CREATE_ALWAYS, CREATE_NEW, OPEN_ALWAYS, TRUNCATE_EXISTING, FILE_ATTRIBUTE_NORMAL, 8 FILE_FLAG_*, 3 FILE_SHARE_*, NMPWAIT_NOWAIT, NMPWAIT_USE_DEFAULT_WAIT) and LoadLibrary function that are not present in CPython's _winapi module. --- crates/vm/src/stdlib/_winapi.rs | 34 +++------------------------------ 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/crates/vm/src/stdlib/_winapi.rs b/crates/vm/src/stdlib/_winapi.rs index f2d8613da27..465e73da37b 100644 --- a/crates/vm/src/stdlib/_winapi.rs +++ b/crates/vm/src/stdlib/_winapi.rs @@ -25,7 +25,7 @@ mod _winapi { ERROR_NETNAME_DELETED, ERROR_NO_DATA, ERROR_NO_SYSTEM_RESOURCES, ERROR_OPERATION_ABORTED, ERROR_PIPE_BUSY, ERROR_PIPE_CONNECTED, ERROR_PRIVILEGE_NOT_HELD, ERROR_SEM_TIMEOUT, GENERIC_READ, GENERIC_WRITE, - STILL_ACTIVE, WAIT_ABANDONED, WAIT_ABANDONED_0, WAIT_OBJECT_0, WAIT_TIMEOUT, + STILL_ACTIVE, WAIT_ABANDONED_0, WAIT_OBJECT_0, WAIT_TIMEOUT, }, Globalization::{ LCMAP_FULLWIDTH, LCMAP_HALFWIDTH, LCMAP_HIRAGANA, LCMAP_KATAKANA, @@ -54,36 +54,19 @@ mod _winapi { COPYFILE2_PROGRESS_PAUSE, COPYFILE2_PROGRESS_QUIET, COPYFILE2_PROGRESS_STOP, - CREATE_ALWAYS, - // CreateFile constants - CREATE_NEW, - FILE_ATTRIBUTE_NORMAL, - FILE_FLAG_BACKUP_SEMANTICS, - FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_FIRST_PIPE_INSTANCE, - FILE_FLAG_NO_BUFFERING, - FILE_FLAG_OPEN_REPARSE_POINT, FILE_FLAG_OVERLAPPED, - FILE_FLAG_POSIX_SEMANTICS, - FILE_FLAG_RANDOM_ACCESS, - FILE_FLAG_SEQUENTIAL_SCAN, - FILE_FLAG_WRITE_THROUGH, FILE_GENERIC_READ, FILE_GENERIC_WRITE, - FILE_SHARE_DELETE, - FILE_SHARE_READ, - FILE_SHARE_WRITE, FILE_TYPE_CHAR, FILE_TYPE_DISK, FILE_TYPE_PIPE, FILE_TYPE_REMOTE, FILE_TYPE_UNKNOWN, - OPEN_ALWAYS, OPEN_EXISTING, PIPE_ACCESS_DUPLEX, PIPE_ACCESS_INBOUND, SYNCHRONIZE, - TRUNCATE_EXISTING, }, System::{ Console::{STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE}, @@ -96,8 +79,8 @@ mod _winapi { SEC_LARGE_PAGES, SEC_NOCACHE, SEC_RESERVE, SEC_WRITECOMBINE, }, Pipes::{ - NMPWAIT_NOWAIT, NMPWAIT_USE_DEFAULT_WAIT, NMPWAIT_WAIT_FOREVER, - PIPE_READMODE_MESSAGE, PIPE_TYPE_MESSAGE, PIPE_UNLIMITED_INSTANCES, PIPE_WAIT, + NMPWAIT_WAIT_FOREVER, PIPE_READMODE_MESSAGE, PIPE_TYPE_MESSAGE, + PIPE_UNLIMITED_INSTANCES, PIPE_WAIT, }, SystemServices::LOCALE_NAME_MAX_LENGTH, Threading::{ @@ -649,17 +632,6 @@ mod _winapi { }) } - #[pyfunction] - fn LoadLibrary(path: PyStrRef, vm: &VirtualMachine) -> PyResult { - let path_wide = path.as_wtf8().to_wide_with_nul(); - let handle = - unsafe { windows_sys::Win32::System::LibraryLoader::LoadLibraryW(path_wide.as_ptr()) }; - if handle.is_null() { - return Err(vm.new_runtime_error("LoadLibrary failed")); - } - Ok(handle as isize) - } - #[pyfunction] fn GetModuleFileName(handle: isize, vm: &VirtualMachine) -> PyResult { let mut path: Vec = vec![0; MAX_PATH as usize]; From 0134645d767ca8881477fc12bd155e6bdfa13a64 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 18 Feb 2026 03:08:53 +0900 Subject: [PATCH 5/7] Fix utf8_mode default to 0 and add PYTHONUTF8 env var support Default utf8_mode was incorrectly set to 1, causing text-mode subprocess to always decode as UTF-8 instead of locale encoding. Changed default to 0 to match CPython 3.13 behavior on Windows. Added PYTHONUTF8 environment variable handling with -X utf8 override. --- crates/vm/src/stdlib/_winapi.rs | 55 ++++++++++----------------------- crates/vm/src/vm/setting.rs | 2 +- src/settings.rs | 15 +++++++++ 3 files changed, 32 insertions(+), 40 deletions(-) diff --git a/crates/vm/src/stdlib/_winapi.rs b/crates/vm/src/stdlib/_winapi.rs index 465e73da37b..4a53327f8af 100644 --- a/crates/vm/src/stdlib/_winapi.rs +++ b/crates/vm/src/stdlib/_winapi.rs @@ -24,8 +24,8 @@ mod _winapi { ERROR_ALREADY_EXISTS, ERROR_BROKEN_PIPE, ERROR_IO_PENDING, ERROR_MORE_DATA, ERROR_NETNAME_DELETED, ERROR_NO_DATA, ERROR_NO_SYSTEM_RESOURCES, ERROR_OPERATION_ABORTED, ERROR_PIPE_BUSY, ERROR_PIPE_CONNECTED, - ERROR_PRIVILEGE_NOT_HELD, ERROR_SEM_TIMEOUT, GENERIC_READ, GENERIC_WRITE, - STILL_ACTIVE, WAIT_ABANDONED_0, WAIT_OBJECT_0, WAIT_TIMEOUT, + ERROR_PRIVILEGE_NOT_HELD, ERROR_SEM_TIMEOUT, GENERIC_READ, GENERIC_WRITE, STILL_ACTIVE, + WAIT_ABANDONED_0, WAIT_OBJECT_0, WAIT_TIMEOUT, }, Globalization::{ LCMAP_FULLWIDTH, LCMAP_HALFWIDTH, LCMAP_HIRAGANA, LCMAP_KATAKANA, @@ -33,40 +33,19 @@ mod _winapi { LCMAP_TRADITIONAL_CHINESE, LCMAP_UPPERCASE, }, Storage::FileSystem::{ - COPY_FILE_ALLOW_DECRYPTED_DESTINATION, - COPY_FILE_COPY_SYMLINK, - COPY_FILE_FAIL_IF_EXISTS, - COPY_FILE_NO_BUFFERING, - COPY_FILE_NO_OFFLOAD, - COPY_FILE_OPEN_SOURCE_FOR_WRITE, - COPY_FILE_REQUEST_COMPRESSED_TRAFFIC, - COPY_FILE_REQUEST_SECURITY_PRIVILEGES, - COPY_FILE_RESTARTABLE, - COPY_FILE_RESUME_FROM_PAUSE, - COPYFILE2_CALLBACK_CHUNK_FINISHED, - COPYFILE2_CALLBACK_CHUNK_STARTED, - COPYFILE2_CALLBACK_ERROR, - COPYFILE2_CALLBACK_POLL_CONTINUE, - COPYFILE2_CALLBACK_STREAM_FINISHED, - COPYFILE2_CALLBACK_STREAM_STARTED, - COPYFILE2_PROGRESS_CANCEL, - COPYFILE2_PROGRESS_CONTINUE, - COPYFILE2_PROGRESS_PAUSE, - COPYFILE2_PROGRESS_QUIET, - COPYFILE2_PROGRESS_STOP, - FILE_FLAG_FIRST_PIPE_INSTANCE, - FILE_FLAG_OVERLAPPED, - FILE_GENERIC_READ, - FILE_GENERIC_WRITE, - FILE_TYPE_CHAR, - FILE_TYPE_DISK, - FILE_TYPE_PIPE, - FILE_TYPE_REMOTE, - FILE_TYPE_UNKNOWN, - OPEN_EXISTING, - PIPE_ACCESS_DUPLEX, - PIPE_ACCESS_INBOUND, - SYNCHRONIZE, + COPY_FILE_ALLOW_DECRYPTED_DESTINATION, COPY_FILE_COPY_SYMLINK, + COPY_FILE_FAIL_IF_EXISTS, COPY_FILE_NO_BUFFERING, COPY_FILE_NO_OFFLOAD, + COPY_FILE_OPEN_SOURCE_FOR_WRITE, COPY_FILE_REQUEST_COMPRESSED_TRAFFIC, + COPY_FILE_REQUEST_SECURITY_PRIVILEGES, COPY_FILE_RESTARTABLE, + COPY_FILE_RESUME_FROM_PAUSE, COPYFILE2_CALLBACK_CHUNK_FINISHED, + COPYFILE2_CALLBACK_CHUNK_STARTED, COPYFILE2_CALLBACK_ERROR, + COPYFILE2_CALLBACK_POLL_CONTINUE, COPYFILE2_CALLBACK_STREAM_FINISHED, + COPYFILE2_CALLBACK_STREAM_STARTED, COPYFILE2_PROGRESS_CANCEL, + COPYFILE2_PROGRESS_CONTINUE, COPYFILE2_PROGRESS_PAUSE, COPYFILE2_PROGRESS_QUIET, + COPYFILE2_PROGRESS_STOP, FILE_FLAG_FIRST_PIPE_INSTANCE, FILE_FLAG_OVERLAPPED, + FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_TYPE_CHAR, FILE_TYPE_DISK, FILE_TYPE_PIPE, + FILE_TYPE_REMOTE, FILE_TYPE_UNKNOWN, OPEN_EXISTING, PIPE_ACCESS_DUPLEX, + PIPE_ACCESS_INBOUND, SYNCHRONIZE, }, System::{ Console::{STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE}, @@ -674,9 +653,7 @@ mod _winapi { #[pyfunction] fn ReleaseMutex(handle: WinHandle) -> WindowsSysResult { - WindowsSysResult(unsafe { - windows_sys::Win32::System::Threading::ReleaseMutex(handle.0) - }) + WindowsSysResult(unsafe { windows_sys::Win32::System::Threading::ReleaseMutex(handle.0) }) } // LOCALE_NAME_INVARIANT is an empty string in Windows API diff --git a/crates/vm/src/vm/setting.rs b/crates/vm/src/vm/setting.rs index 1a5ef9efa8f..07f8129fecc 100644 --- a/crates/vm/src/vm/setting.rs +++ b/crates/vm/src/vm/setting.rs @@ -211,7 +211,7 @@ impl Default for Settings { allow_external_library: cfg!(feature = "importlib"), stdio_encoding: None, stdio_errors: None, - utf8_mode: 1, + utf8_mode: 0, int_max_str_digits: 4300, #[cfg(feature = "flame-it")] profile_output: None, diff --git a/src/settings.rs b/src/settings.rs index 79c67dafce0..2017a702289 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -260,6 +260,21 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> { settings.check_hash_pycs_mode = args.check_hash_based_pycs; + if let Some(val) = get_env("PYTHONUTF8") { + settings.utf8_mode = match val.to_str() { + Some("1") | Some("") => 1, + Some("0") => 0, + _ => { + error!( + "Fatal Python error: config_init_utf8_mode: \ + PYTHONUTF8=N: N is missing or invalid\n\ + Python runtime state: preinitialized" + ); + std::process::exit(1); + } + }; + } + let xopts = args.implementation_option.into_iter().map(|s| { let (name, value) = match s.split_once('=') { Some((name, value)) => (name.to_owned(), Some(value)), From c7c0fe5d3b8d918b95497dfa5c3d5ad236404757 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 20 Feb 2026 23:53:19 +0900 Subject: [PATCH 6/7] Fix CopyFile2 to raise proper OSError subclass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use std::io::Error::from_raw_os_error instead of vm.new_os_error so that winerror attribute is set and errno-to-exception mapping works (e.g. ERROR_ACCESS_DENIED → PermissionError). --- crates/vm/src/stdlib/_winapi.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/vm/src/stdlib/_winapi.rs b/crates/vm/src/stdlib/_winapi.rs index 4a53327f8af..2fa420ca04f 100644 --- a/crates/vm/src/stdlib/_winapi.rs +++ b/crates/vm/src/stdlib/_winapi.rs @@ -1944,7 +1944,9 @@ mod _winapi { } else { hr as u32 }; - return Err(vm.new_os_error(err as i32)); + return Err( + std::io::Error::from_raw_os_error(err as i32).to_pyexception(vm), + ); } Ok(()) } From 4ef242fd55bbc1ea5806314adf4fc9052b39a996 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sat, 21 Feb 2026 12:35:22 +0900 Subject: [PATCH 7/7] Fix syntax_non_utf8 test to not depend on locale encoding Use explicit encoding='latin-1' so the test works regardless of the system locale (e.g. C/POSIX locale uses ASCII by default). --- .cspell.json | 1 + crates/vm/src/stdlib/_winapi.rs | 25 ++++++++----------------- crates/vm/src/stdlib/sys.rs | 6 +++++- crates/vm/src/vm/setting.rs | 4 ++-- extra_tests/snippets/syntax_non_utf8.py | 2 +- src/settings.rs | 18 ++++++++++++++---- 6 files changed, 31 insertions(+), 25 deletions(-) diff --git a/.cspell.json b/.cspell.json index 0a93ac35cd5..aae9ed6c853 100644 --- a/.cspell.json +++ b/.cspell.json @@ -147,6 +147,7 @@ // "stat" "FIRMLINK", // CPython internal names + "PYTHONUTF", "sysdict", "settraceallthreads", "setprofileallthreads" diff --git a/crates/vm/src/stdlib/_winapi.rs b/crates/vm/src/stdlib/_winapi.rs index 2fa420ca04f..98299c671e9 100644 --- a/crates/vm/src/stdlib/_winapi.rs +++ b/crates/vm/src/stdlib/_winapi.rs @@ -279,9 +279,9 @@ mod _winapi { } si_attr!(dwFlags); si_attr!(wShowWindow); - si_attr!(hStdInput, usize); - si_attr!(hStdOutput, usize); - si_attr!(hStdError, usize); + si_attr!(hStdInput, isize); + si_attr!(hStdOutput, isize); + si_attr!(hStdError, isize); let mut env = args .env_mapping @@ -1160,7 +1160,7 @@ mod _winapi { initial_state: bool, name: Option, vm: &VirtualMachine, - ) -> PyResult> { + ) -> PyResult { use windows_sys::Win32::System::Threading::CreateEventW as WinCreateEventW; let _ = security_attributes; // Ignored, always NULL @@ -1177,15 +1177,11 @@ mod _winapi { ) }; - if handle == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE { - return Err(vm.new_last_os_error()); - } - if handle.is_null() { - return Ok(None); + return Err(vm.new_last_os_error()); } - Ok(Some(WinHandle(handle))) + Ok(WinHandle(handle)) } /// SetEvent - Set the specified event object to the signaled state. @@ -1944,9 +1940,7 @@ mod _winapi { } else { hr as u32 }; - return Err( - std::io::Error::from_raw_os_error(err as i32).to_pyexception(vm), - ); + return Err(std::io::Error::from_raw_os_error(err as i32).to_pyexception(vm)); } Ok(()) } @@ -1967,6 +1961,7 @@ mod _winapi { if err != 0 { return Err(vm.new_os_error(err as i32)); } + scopeguard::defer! { unsafe { RegCloseKey(hkcr) }; } let mut i: u32 = 0; let mut entries: Vec<(String, String)> = Vec::new(); @@ -1993,7 +1988,6 @@ mod _winapi { break; } if err != 0 && err != windows_sys::Win32::Foundation::ERROR_MORE_DATA { - unsafe { RegCloseKey(hkcr) }; return Err(vm.new_os_error(err as i32)); } @@ -2013,7 +2007,6 @@ mod _winapi { continue; } if err != 0 { - unsafe { RegCloseKey(hkcr) }; return Err(vm.new_os_error(err as i32)); } @@ -2057,8 +2050,6 @@ mod _winapi { } } - unsafe { RegCloseKey(hkcr) }; - // Process remaining entries for (mime_type, ext) in entries { on_type_read.call((vm.ctx.new_str(mime_type), vm.ctx.new_str(ext)), vm)?; diff --git a/crates/vm/src/stdlib/sys.rs b/crates/vm/src/stdlib/sys.rs index 492324277e9..915da5acd50 100644 --- a/crates/vm/src/stdlib/sys.rs +++ b/crates/vm/src/stdlib/sys.rs @@ -1588,7 +1588,11 @@ mod sys { hash_randomization: settings.hash_seed.is_none() as u8, isolated: settings.isolated as u8, dev_mode: settings.dev_mode, - utf8_mode: settings.utf8_mode, + utf8_mode: if settings.utf8_mode < 0 { + 1 + } else { + settings.utf8_mode as u8 + }, int_max_str_digits: settings.int_max_str_digits, safe_path: settings.safe_path, warn_default_encoding: settings.warn_default_encoding as u8, diff --git a/crates/vm/src/vm/setting.rs b/crates/vm/src/vm/setting.rs index 07f8129fecc..0563e7131ac 100644 --- a/crates/vm/src/vm/setting.rs +++ b/crates/vm/src/vm/setting.rs @@ -124,7 +124,7 @@ pub struct Settings { pub stdio_encoding: Option, /// PYTHONIOENCODING - stdio error handler pub stdio_errors: Option, - pub utf8_mode: u8, + pub utf8_mode: i8, /// --check-hash-based-pycs pub check_hash_pycs_mode: CheckHashPycsMode, @@ -211,7 +211,7 @@ impl Default for Settings { allow_external_library: cfg!(feature = "importlib"), stdio_encoding: None, stdio_errors: None, - utf8_mode: 0, + utf8_mode: -1, int_max_str_digits: 4300, #[cfg(feature = "flame-it")] profile_output: None, diff --git a/extra_tests/snippets/syntax_non_utf8.py b/extra_tests/snippets/syntax_non_utf8.py index ad93d5b33c1..8f11078bc07 100644 --- a/extra_tests/snippets/syntax_non_utf8.py +++ b/extra_tests/snippets/syntax_non_utf8.py @@ -5,5 +5,5 @@ dir_path = os.path.dirname(os.path.realpath(__file__)) with assert_raises(SyntaxError): - with open(os.path.join(dir_path , "non_utf8.txt")) as f: + with open(os.path.join(dir_path , "non_utf8.txt"), encoding="latin-1") as f: eval(f.read()) diff --git a/src/settings.rs b/src/settings.rs index 2017a702289..5233cf98d49 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -260,10 +260,13 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> { settings.check_hash_pycs_mode = args.check_hash_based_pycs; - if let Some(val) = get_env("PYTHONUTF8") { - settings.utf8_mode = match val.to_str() { - Some("1") | Some("") => 1, - Some("0") => 0, + if let Some(val) = get_env("PYTHONUTF8") + && let Some(val_str) = val.to_str() + && !val_str.is_empty() + { + settings.utf8_mode = match val_str { + "1" => 1, + "0" => 0, _ => { error!( "Fatal Python error: config_init_utf8_mode: \ @@ -335,6 +338,13 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> { }); settings.xoptions.extend(xopts); + // Resolve utf8_mode if not explicitly set by PYTHONUTF8 or -X utf8. + // Default to UTF-8 mode since RustPython's locale encoding detection + // is incomplete. Users can set PYTHONUTF8=0 or -X utf8=0 to disable. + if settings.utf8_mode < 0 { + settings.utf8_mode = 1; + } + settings.warn_default_encoding = settings.warn_default_encoding || env_bool("PYTHONWARNDEFAULTENCODING"); settings.faulthandler = settings.faulthandler || env_bool("PYTHONFAULTHANDLER");