From 660b9e5177daa24f9807a19779ef4860834eee88 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Tue, 9 Dec 2025 00:32:10 +0900 Subject: [PATCH 1/3] use ToWide --- crates/common/src/fileutils.rs | 4 +--- crates/vm/src/stdlib/codecs.rs | 8 ++++---- crates/vm/src/stdlib/nt.rs | 10 +++------- crates/vm/src/stdlib/sys.rs | 13 +++---------- crates/vm/src/windows.rs | 7 +++---- 5 files changed, 14 insertions(+), 28 deletions(-) diff --git a/crates/common/src/fileutils.rs b/crates/common/src/fileutils.rs index 3e03f0299ad..a1c6eec8178 100644 --- a/crates/common/src/fileutils.rs +++ b/crates/common/src/fileutils.rs @@ -26,7 +26,6 @@ pub mod windows { use crate::windows::ToWideString; use libc::{S_IFCHR, S_IFDIR, S_IFMT}; use std::ffi::{CString, OsStr, OsString}; - use std::os::windows::ffi::OsStrExt; use std::os::windows::io::AsRawHandle; use std::sync::OnceLock; use windows_sys::Win32::Foundation::{ @@ -75,8 +74,7 @@ pub mod windows { pub fn update_st_mode_from_path(&mut self, path: &OsStr, attr: u32) { if attr & FILE_ATTRIBUTE_DIRECTORY == 0 { let file_extension = path - .encode_wide() - .collect::>() + .to_wide() .split(|&c| c == '.' as u16) .next_back() .and_then(|s| String::from_utf16(s).ok()); diff --git a/crates/vm/src/stdlib/codecs.rs b/crates/vm/src/stdlib/codecs.rs index dee5b17b822..5f1b721dfb4 100644 --- a/crates/vm/src/stdlib/codecs.rs +++ b/crates/vm/src/stdlib/codecs.rs @@ -237,7 +237,7 @@ mod _codecs { #[cfg(windows)] #[pyfunction] fn mbcs_encode(args: MbcsEncodeArgs, vm: &VirtualMachine) -> PyResult<(Vec, usize)> { - use std::os::windows::ffi::OsStrExt; + use crate::common::windows::ToWideString; use windows_sys::Win32::Globalization::{ CP_ACP, WC_NO_BEST_FIT_CHARS, WideCharToMultiByte, }; @@ -259,7 +259,7 @@ mod _codecs { } // Convert UTF-8 string to UTF-16 - let wide: Vec = std::ffi::OsStr::new(s).encode_wide().collect(); + let wide: Vec = std::ffi::OsStr::new(s).to_wide(); // Get the required buffer size let size = unsafe { @@ -439,7 +439,7 @@ mod _codecs { #[cfg(windows)] #[pyfunction] fn oem_encode(args: OemEncodeArgs, vm: &VirtualMachine) -> PyResult<(Vec, usize)> { - use std::os::windows::ffi::OsStrExt; + use crate::common::windows::ToWideString; use windows_sys::Win32::Globalization::{ CP_OEMCP, WC_NO_BEST_FIT_CHARS, WideCharToMultiByte, }; @@ -461,7 +461,7 @@ mod _codecs { } // Convert UTF-8 string to UTF-16 - let wide: Vec = std::ffi::OsStr::new(s).encode_wide().collect(); + let wide: Vec = std::ffi::OsStr::new(s).to_wide(); // Get the required buffer size let size = unsafe { diff --git a/crates/vm/src/stdlib/nt.rs b/crates/vm/src/stdlib/nt.rs index 2304a15e4fd..4a1a13c9014 100644 --- a/crates/vm/src/stdlib/nt.rs +++ b/crates/vm/src/stdlib/nt.rs @@ -15,7 +15,7 @@ pub(crate) mod module { use crate::{ PyResult, TryFromObject, VirtualMachine, builtins::{PyBaseExceptionRef, PyDictRef, PyListRef, PyStrRef, PyTupleRef}, - common::{crt_fd, os::last_os_error, suppress_iph}, + common::{crt_fd, os::last_os_error, suppress_iph, windows::ToWideString}, convert::ToPyException, function::{Either, OptionalArg}, ospath::OsPath, @@ -23,11 +23,7 @@ pub(crate) mod module { }; use libc::intptr_t; use std::os::windows::io::AsRawHandle; - use std::{ - env, fs, io, - mem::MaybeUninit, - os::windows::ffi::{OsStrExt, OsStringExt}, - }; + use std::{env, fs, io, mem::MaybeUninit, os::windows::ffi::OsStringExt}; use windows_sys::Win32::{ Foundation::{self, INVALID_HANDLE_VALUE}, Storage::FileSystem, @@ -541,7 +537,7 @@ pub(crate) mod module { #[pyfunction] fn _path_splitroot(path: OsPath, vm: &VirtualMachine) -> PyResult<(String, String)> { - let orig: Vec<_> = path.path.encode_wide().collect(); + let orig: Vec<_> = path.path.to_wide(); if orig.is_empty() { return Ok(("".to_owned(), "".to_owned())); } diff --git a/crates/vm/src/stdlib/sys.rs b/crates/vm/src/stdlib/sys.rs index 602fdff1eb1..53ad06dd4e9 100644 --- a/crates/vm/src/stdlib/sys.rs +++ b/crates/vm/src/stdlib/sys.rs @@ -12,6 +12,7 @@ mod sys { common::{ ascii, hash::{PyHash, PyUHash}, + windows::ToWideString, }, convert::ToPyObject, frame::FrameRef, @@ -22,8 +23,6 @@ mod sys { vm::{Settings, VirtualMachine}, }; use num_traits::ToPrimitive; - #[cfg(windows)] - use std::os::windows::ffi::OsStrExt; use std::{ env::{self, VarError}, io::Read, @@ -553,10 +552,7 @@ mod sys { fn get_kernel32_version() -> std::io::Result<(u32, u32, u32)> { unsafe { // Create a wide string for "kernel32.dll" - let module_name: Vec = std::ffi::OsStr::new("kernel32.dll") - .encode_wide() - .chain(Some(0)) - .collect(); + let module_name: Vec = std::ffi::OsStr::new("kernel32.dll").to_wide_with_nul(); let h_kernel32 = GetModuleHandleW(module_name.as_ptr()); if h_kernel32.is_null() { return Err(std::io::Error::last_os_error()); @@ -593,10 +589,7 @@ mod sys { } // Prepare an empty sub-block string (L"") as required by VerQueryValueW - let sub_block: Vec = std::ffi::OsStr::new("") - .encode_wide() - .chain(Some(0)) - .collect(); + let sub_block: Vec = std::ffi::OsStr::new("").to_wide_with_nul(); let mut ffi_ptr: *mut VS_FIXEDFILEINFO = std::ptr::null_mut(); let mut ffi_len: u32 = 0; diff --git a/crates/vm/src/windows.rs b/crates/vm/src/windows.rs index 6825ee5841b..d91c436db6c 100644 --- a/crates/vm/src/windows.rs +++ b/crates/vm/src/windows.rs @@ -7,6 +7,7 @@ use crate::{ convert::{ToPyObject, ToPyResult}, stdlib::os::errno_err, }; +use rustpython_common::windows::ToWideString; use std::ffi::OsStr; use windows::Win32::Foundation::HANDLE; use windows_sys::Win32::Foundation::{HANDLE as RAW_HANDLE, INVALID_HANDLE_VALUE}; @@ -235,13 +236,12 @@ fn attributes_from_dir( windows_sys::Win32::Storage::FileSystem::BY_HANDLE_FILE_INFORMATION, u32, )> { - use std::os::windows::ffi::OsStrExt; use windows_sys::Win32::Storage::FileSystem::{ BY_HANDLE_FILE_INFORMATION, FILE_ATTRIBUTE_REPARSE_POINT, FindClose, FindFirstFileW, WIN32_FIND_DATAW, }; - let wide: Vec = path.encode_wide().chain(std::iter::once(0)).collect(); + let wide: Vec = path.to_wide_with_nul(); let mut find_data: WIN32_FIND_DATAW = unsafe { std::mem::zeroed() }; let handle = unsafe { FindFirstFileW(wide.as_ptr(), &mut find_data) }; @@ -269,7 +269,6 @@ fn attributes_from_dir( /// Ported from win32_xstat_slow_impl fn win32_xstat_slow_impl(path: &OsStr, traverse: bool) -> std::io::Result { - use std::os::windows::ffi::OsStrExt; use windows_sys::Win32::{ Foundation::{ CloseHandle, ERROR_ACCESS_DENIED, ERROR_CANT_ACCESS_FILE, ERROR_INVALID_FUNCTION, @@ -287,7 +286,7 @@ fn win32_xstat_slow_impl(path: &OsStr, traverse: bool) -> std::io::Result = path.encode_wide().chain(std::iter::once(0)).collect(); + let wide: Vec = path.to_wide_with_nul(); let access = FILE_READ_ATTRIBUTES; let mut flags = FILE_FLAG_BACKUP_SEMANTICS; From 656b825fac3975b02fe1543f34105e3241d16f56 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Tue, 9 Dec 2025 00:34:25 +0900 Subject: [PATCH 2/3] Unconstructible --- crates/vm/src/stdlib/os.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/vm/src/stdlib/os.rs b/crates/vm/src/stdlib/os.rs index 45d4e41bcba..2620fcb2755 100644 --- a/crates/vm/src/stdlib/os.rs +++ b/crates/vm/src/stdlib/os.rs @@ -168,7 +168,7 @@ pub(super) mod _os { ospath::{IOErrorBuilder, OsPath, OsPathOrFd, OutputMode}, protocol::PyIterReturn, recursion::ReprGuard, - types::{IterNext, Iterable, PyStructSequence, Representable, SelfIter}, + types::{IterNext, Iterable, PyStructSequence, Representable, SelfIter, Unconstructible}, utils::ToCString, vm::VirtualMachine, }; @@ -474,7 +474,7 @@ pub(super) mod _os { ino: AtomicCell>, } - #[pyclass(with(Representable))] + #[pyclass(with(Representable, Unconstructible))] impl DirEntry { #[pygetset] fn name(&self, vm: &VirtualMachine) -> PyResult { @@ -652,6 +652,7 @@ pub(super) mod _os { } } } + impl Unconstructible for DirEntry {} #[pyattr] #[pyclass(name = "ScandirIter")] @@ -661,7 +662,7 @@ pub(super) mod _os { mode: OutputMode, } - #[pyclass(with(IterNext, Iterable))] + #[pyclass(with(IterNext, Iterable, Unconstructible))] impl ScandirIterator { #[pymethod] fn close(&self) { @@ -679,6 +680,7 @@ pub(super) mod _os { zelf.close() } } + impl Unconstructible for ScandirIterator {} impl SelfIter for ScandirIterator {} impl IterNext for ScandirIterator { fn next(zelf: &crate::Py, vm: &VirtualMachine) -> PyResult { From 06c4bf7b200741c82e588ed7e332b06bdbbbdc78 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Tue, 9 Dec 2025 00:33:02 +0900 Subject: [PATCH 3/3] fix remove --- Lib/test/test_os.py | 8 -------- Lib/test/test_shutil.py | 1 - crates/vm/src/stdlib/os.rs | 29 +++++++++++++++++++++++++---- crates/vm/src/stdlib/sys.rs | 2 +- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index e751b6423d6..a19366039ef 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -736,7 +736,6 @@ def test_file_attributes(self): result.st_file_attributes & stat.FILE_ATTRIBUTE_DIRECTORY, stat.FILE_ATTRIBUTE_DIRECTORY) - @unittest.expectedFailureIfWindows('TODO: RUSTPYTHON; os.stat (PermissionError: [Errno 5] Access is denied.)') @unittest.skipUnless(sys.platform == "win32", "Win32 specific tests") def test_access_denied(self): # Default to FindFirstFile WIN32_FIND_DATA when access is @@ -759,7 +758,6 @@ def test_access_denied(self): self.assertNotEqual(result.st_size, 0) self.assertTrue(os.path.isfile(fname)) - @unittest.expectedFailureIfWindows('TODO: RUSTPYTHON; os.stat (PermissionError: [Errno 1] Incorrect function.)') @unittest.skipUnless(sys.platform == "win32", "Win32 specific tests") def test_stat_block_device(self): # bpo-38030: os.stat fails for block devices @@ -1796,7 +1794,6 @@ def test_mode(self): self.assertEqual(os.stat(path).st_mode & 0o777, 0o555) self.assertEqual(os.stat(parent).st_mode & 0o777, 0o775) - @unittest.expectedFailureIfWindows('TODO: RUSTPYTHON; os.umask not implemented yet for all platforms') @unittest.skipIf( support.is_emscripten or support.is_wasi, "Emscripten's/WASI's umask is a stub." @@ -2116,7 +2113,6 @@ def test_urandom_fd_closed(self): """ rc, out, err = assert_python_ok('-Sc', code) - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON; (ModuleNotFoundError: No module named 'os'") def test_urandom_fd_reopened(self): # Issue #21207: urandom() should detect its fd to /dev/urandom # changed to something else, and reopen it. @@ -3221,7 +3217,6 @@ def test_getfinalpathname_handles(self): self.assertEqual(0, handle_delta) - @unittest.expectedFailureIfWindows('TODO: RUSTPYTHON; os.stat (PermissionError: [Errno 5] Access is denied.)') @support.requires_subprocess() def test_stat_unlink_race(self): # bpo-46785: the implementation of os.stat() falls back to reading @@ -4927,7 +4922,6 @@ def setUp(self): self.addCleanup(os_helper.rmtree, self.path) os.mkdir(self.path) - @unittest.expectedFailure # TODO: RUSTPYTHON; (AssertionError: TypeError not raised by DirEntry) def test_uninstantiable(self): self.assertRaises(TypeError, os.DirEntry) @@ -4976,7 +4970,6 @@ def assert_stat_equal(self, stat1, stat2, skip_fields): else: self.assertEqual(stat1, stat2) - @unittest.expectedFailure # TODO: RUSTPYTHON; (AssertionError: TypeError not raised by ScandirIter) def test_uninstantiable(self): scandir_iter = os.scandir(self.path) self.assertRaises(TypeError, type(scandir_iter)) @@ -5230,7 +5223,6 @@ def test_fd(self): st = os.stat(entry.name, dir_fd=fd, follow_symlinks=False) self.assertEqual(entry.stat(follow_symlinks=False), st) - @unittest.expectedFailureIfWindows('TODO: RUSTPYTHON; (AssertionError: FileNotFoundError not raised by scandir)') @unittest.skipIf(support.is_wasi, "WASI maps '' to cwd") def test_empty_path(self): self.assertRaises(FileNotFoundError, os.scandir, '') diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 124430c8922..0abf7ca61a1 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -235,7 +235,6 @@ def onexc(*args): self.assertEqual(errors[0][1], link) self.assertIsInstance(errors[0][2], OSError) - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") @os_helper.skip_unless_symlink def test_rmtree_works_on_symlinks(self): tmp = self.mkdtemp() diff --git a/crates/vm/src/stdlib/os.rs b/crates/vm/src/stdlib/os.rs index 2620fcb2755..d6b10341da7 100644 --- a/crates/vm/src/stdlib/os.rs +++ b/crates/vm/src/stdlib/os.rs @@ -152,6 +152,8 @@ impl ToPyObject for crt_fd::Borrowed<'_> { #[pymodule(sub)] pub(super) mod _os { use super::{DirFd, FollowSymlinks, SupportFunc, errno_err}; + #[cfg(windows)] + use crate::common::windows::ToWideString; use crate::{ AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, builtins::{ @@ -292,10 +294,29 @@ pub(super) mod _os { #[pyfunction(name = "unlink")] fn remove(path: OsPath, dir_fd: DirFd<'_, 0>, vm: &VirtualMachine) -> PyResult<()> { let [] = dir_fd.0; - let is_junction = cfg!(windows) - && fs::metadata(&path).is_ok_and(|meta| meta.file_type().is_dir()) - && fs::symlink_metadata(&path).is_ok_and(|meta| meta.file_type().is_symlink()); - let res = if is_junction { + #[cfg(windows)] + let is_dir_link = { + // On Windows, we need to check if it's a directory symlink/junction + // using GetFileAttributesW, which doesn't follow symlinks. + // This is similar to CPython's Py_DeleteFileW. + use windows_sys::Win32::Storage::FileSystem::{ + FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_REPARSE_POINT, GetFileAttributesW, + INVALID_FILE_ATTRIBUTES, + }; + let wide_path: Vec = path.path.as_os_str().to_wide_with_nul(); + let attrs = unsafe { GetFileAttributesW(wide_path.as_ptr()) }; + if attrs != INVALID_FILE_ATTRIBUTES { + let is_dir = (attrs & FILE_ATTRIBUTE_DIRECTORY) != 0; + let is_reparse = (attrs & FILE_ATTRIBUTE_REPARSE_POINT) != 0; + is_dir && is_reparse + } else { + false + } + }; + #[cfg(not(windows))] + let is_dir_link = false; + + let res = if is_dir_link { fs::remove_dir(&path) } else { fs::remove_file(&path) diff --git a/crates/vm/src/stdlib/sys.rs b/crates/vm/src/stdlib/sys.rs index 53ad06dd4e9..45b1d566058 100644 --- a/crates/vm/src/stdlib/sys.rs +++ b/crates/vm/src/stdlib/sys.rs @@ -12,7 +12,6 @@ mod sys { common::{ ascii, hash::{PyHash, PyUHash}, - windows::ToWideString, }, convert::ToPyObject, frame::FrameRef, @@ -550,6 +549,7 @@ mod sys { #[cfg(windows)] fn get_kernel32_version() -> std::io::Result<(u32, u32, u32)> { + use crate::common::windows::ToWideString; unsafe { // Create a wide string for "kernel32.dll" let module_name: Vec = std::ffi::OsStr::new("kernel32.dll").to_wide_with_nul();