Skip to content

Commit f379ea8

Browse files
authored
winapi._findfirstfile,nt.chmod (#6401)
1 parent 04d55fa commit f379ea8

File tree

8 files changed

+122
-39
lines changed

8 files changed

+122
-39
lines changed

Lib/test/test_compileall.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,6 @@ def test_multiple_optimization_levels(self):
459459
except Exception:
460460
pass
461461

462-
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
463462
@os_helper.skip_unless_symlink
464463
def test_ignore_symlink_destination(self):
465464
# Create folders for allowed files, symlinks and prohibited area
@@ -933,7 +932,6 @@ def test_multiple_optimization_levels(self):
933932
except Exception:
934933
pass
935934

936-
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
937935
@os_helper.skip_unless_symlink
938936
def test_ignore_symlink_destination(self):
939937
# Create folders for allowed files, symlinks and prohibited area

Lib/test/test_ntpath.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -650,8 +650,6 @@ def test_realpath_relative(self, kwargs):
650650
os.symlink(ABSTFN, ntpath.relpath(ABSTFN + "1"))
651651
self.assertPathEqual(ntpath.realpath(ABSTFN + "1", **kwargs), ABSTFN)
652652

653-
# TODO: RUSTPYTHON
654-
@unittest.expectedFailure
655653
@os_helper.skip_unless_symlink
656654
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
657655
def test_realpath_broken_symlinks(self):
@@ -1385,6 +1383,7 @@ def test_nt_helpers(self):
13851383
self.assertIsInstance(b_final_path, bytes)
13861384
self.assertGreater(len(b_final_path), 0)
13871385

1386+
@unittest.expectedFailure # TODO: RUSTPYTHON
13881387
@unittest.skipIf(sys.platform != 'win32', "Can only test junctions with creation on win32.")
13891388
def test_isjunction(self):
13901389
with os_helper.temp_dir() as d:

Lib/test/test_os.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3144,7 +3144,6 @@ def test_create_junction(self):
31443144
self.assertEqual(os.path.normcase("\\\\?\\" + self.junction_target),
31453145
os.path.normcase(os.readlink(self.junction)))
31463146

3147-
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON; (AttributeError: module '_winapi' has no attribute 'CreateJunction')")
31483147
def test_unlink_removes_junction(self):
31493148
_winapi.CreateJunction(self.junction_target, self.junction)
31503149
self.assertTrue(os.path.exists(self.junction))

Lib/test/test_posix.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1063,7 +1063,6 @@ def check_lchmod_link(self, chmod_func, target, link, **kwargs):
10631063
self.assertEqual(os.stat(target).st_mode, target_mode)
10641064
self.assertEqual(os.lstat(link).st_mode, new_mode)
10651065

1066-
@unittest.expectedFailureIfWindows('TODO: RUSTPYTHON')
10671066
@os_helper.skip_unless_symlink
10681067
def test_chmod_file_symlink(self):
10691068
target = os_helper.TESTFN

Lib/test/test_shutil.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,6 @@ def test_rmtree_works_on_symlinks(self):
257257
self.assertTrue(os.path.exists(dir3))
258258
self.assertTrue(os.path.exists(file1))
259259

260-
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
261260
@unittest.skipUnless(_winapi, 'only relevant on Windows')
262261
def test_rmtree_fails_on_junctions_onerror(self):
263262
tmp = self.mkdtemp()
@@ -278,7 +277,6 @@ def onerror(*args):
278277
self.assertEqual(errors[0][1], link)
279278
self.assertIsInstance(errors[0][2][1], OSError)
280279

281-
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
282280
@unittest.skipUnless(_winapi, 'only relevant on Windows')
283281
def test_rmtree_fails_on_junctions_onexc(self):
284282
tmp = self.mkdtemp()
@@ -299,7 +297,6 @@ def onexc(*args):
299297
self.assertEqual(errors[0][1], link)
300298
self.assertIsInstance(errors[0][2], OSError)
301299

302-
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
303300
@unittest.skipUnless(_winapi, 'only relevant on Windows')
304301
def test_rmtree_works_on_junctions(self):
305302
tmp = self.mkdtemp()
@@ -659,7 +656,6 @@ def test_rmtree_on_symlink(self):
659656
finally:
660657
shutil.rmtree(TESTFN, ignore_errors=True)
661658

662-
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
663659
@unittest.skipUnless(_winapi, 'only relevant on Windows')
664660
def test_rmtree_on_junction(self):
665661
os.mkdir(TESTFN)
@@ -977,7 +973,6 @@ def _copy(src, dst):
977973
shutil.copytree(src_dir, dst_dir, copy_function=_copy)
978974
self.assertEqual(len(copied), 2)
979975

980-
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
981976
@os_helper.skip_unless_symlink
982977
def test_copytree_dangling_symlinks(self):
983978
src_dir = self.mkdtemp()
@@ -1051,7 +1046,6 @@ class TestCopy(BaseTest, unittest.TestCase):
10511046

10521047
### shutil.copymode
10531048

1054-
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
10551049
@os_helper.skip_unless_symlink
10561050
def test_copymode_follow_symlinks(self):
10571051
tmp_dir = self.mkdtemp()
@@ -2594,7 +2588,6 @@ def test_move_file_symlink_to_dir(self):
25942588
self.assertTrue(os.path.islink(final_link))
25952589
self.assertTrue(os.path.samefile(self.src_file, final_link))
25962590

2597-
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
25982591
@os_helper.skip_unless_symlink
25992592
@mock_rename
26002593
def test_move_dangling_symlink(self):

crates/common/src/windows.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use rustpython_wtf8::Wtf8;
12
use std::{
23
ffi::{OsStr, OsString},
34
os::windows::ffi::{OsStrExt, OsStringExt},
@@ -10,6 +11,7 @@ pub trait ToWideString {
1011
fn to_wide(&self) -> Vec<u16>;
1112
fn to_wide_with_nul(&self) -> Vec<u16>;
1213
}
14+
1315
impl<T> ToWideString for T
1416
where
1517
T: AsRef<OsStr>,
@@ -22,6 +24,24 @@ where
2224
}
2325
}
2426

27+
impl ToWideString for OsStr {
28+
fn to_wide(&self) -> Vec<u16> {
29+
self.encode_wide().collect()
30+
}
31+
fn to_wide_with_nul(&self) -> Vec<u16> {
32+
self.encode_wide().chain(Some(0)).collect()
33+
}
34+
}
35+
36+
impl ToWideString for Wtf8 {
37+
fn to_wide(&self) -> Vec<u16> {
38+
self.encode_wide().collect()
39+
}
40+
fn to_wide_with_nul(&self) -> Vec<u16> {
41+
self.encode_wide().chain(Some(0)).collect()
42+
}
43+
}
44+
2545
pub trait FromWideString
2646
where
2747
Self: Sized,

crates/vm/src/stdlib/nt.rs

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub(crate) mod module {
1919
convert::ToPyException,
2020
function::{Either, OptionalArg},
2121
ospath::OsPath,
22-
stdlib::os::{_os, DirFd, FollowSymlinks, SupportFunc, TargetIsDirectory},
22+
stdlib::os::{_os, DirFd, SupportFunc, TargetIsDirectory},
2323
};
2424

2525
use libc::intptr_t;
@@ -137,25 +137,104 @@ pub(crate) mod module {
137137
environ
138138
}
139139

140-
#[pyfunction]
141-
fn chmod(
140+
#[derive(FromArgs)]
141+
struct ChmodArgs {
142+
#[pyarg(any)]
142143
path: OsPath,
143-
dir_fd: DirFd<'_, 0>,
144+
#[pyarg(any)]
144145
mode: u32,
145-
follow_symlinks: FollowSymlinks,
146-
vm: &VirtualMachine,
147-
) -> PyResult<()> {
146+
#[pyarg(flatten)]
147+
dir_fd: DirFd<'static, 0>,
148+
#[pyarg(named, name = "follow_symlinks", optional)]
149+
follow_symlinks: OptionalArg<bool>,
150+
}
151+
152+
#[pyfunction]
153+
fn chmod(args: ChmodArgs, vm: &VirtualMachine) -> PyResult<()> {
154+
let ChmodArgs {
155+
path,
156+
mode,
157+
dir_fd,
158+
follow_symlinks,
159+
} = args;
148160
const S_IWRITE: u32 = 128;
149161
let [] = dir_fd.0;
150-
let metadata = if follow_symlinks.0 {
151-
fs::metadata(&path)
152-
} else {
153-
fs::symlink_metadata(&path)
162+
163+
// On Windows, os.chmod behavior differs based on whether follow_symlinks is explicitly provided:
164+
// - Not provided (default): use SetFileAttributesW on the path directly (doesn't follow symlinks)
165+
// - Explicitly True: resolve symlink first, then apply permissions to target
166+
// - Explicitly False: raise NotImplementedError (Windows can't change symlink permissions)
167+
let actual_path: std::borrow::Cow<'_, std::path::Path> = match follow_symlinks.into_option()
168+
{
169+
None => {
170+
// Default behavior: don't resolve symlinks, operate on path directly
171+
std::borrow::Cow::Borrowed(path.as_ref())
172+
}
173+
Some(true) => {
174+
// Explicitly follow symlinks: resolve the path first
175+
match fs::canonicalize(&path) {
176+
Ok(p) => std::borrow::Cow::Owned(p),
177+
Err(_) => std::borrow::Cow::Borrowed(path.as_ref()),
178+
}
179+
}
180+
Some(false) => {
181+
// follow_symlinks=False on Windows - not supported for symlinks
182+
// Check if path is a symlink
183+
if let Ok(meta) = fs::symlink_metadata(&path)
184+
&& meta.file_type().is_symlink()
185+
{
186+
return Err(vm.new_not_implemented_error(
187+
"chmod: follow_symlinks=False is not supported on Windows for symlinks"
188+
.to_owned(),
189+
));
190+
}
191+
std::borrow::Cow::Borrowed(path.as_ref())
192+
}
154193
};
155-
let meta = metadata.map_err(|err| err.to_pyexception(vm))?;
194+
195+
// Use symlink_metadata to avoid following dangling symlinks
196+
let meta = fs::symlink_metadata(&actual_path).map_err(|err| err.to_pyexception(vm))?;
156197
let mut permissions = meta.permissions();
157198
permissions.set_readonly(mode & S_IWRITE == 0);
158-
fs::set_permissions(&path, permissions).map_err(|err| err.to_pyexception(vm))
199+
fs::set_permissions(&*actual_path, permissions).map_err(|err| err.to_pyexception(vm))
200+
}
201+
202+
/// Get the real file name (with correct case) without accessing the file.
203+
/// Uses FindFirstFileW to get the name as stored on the filesystem.
204+
#[pyfunction]
205+
fn _findfirstfile(path: OsPath, vm: &VirtualMachine) -> PyResult<PyStrRef> {
206+
use crate::common::windows::ToWideString;
207+
use std::os::windows::ffi::OsStringExt;
208+
use windows_sys::Win32::Storage::FileSystem::{
209+
FindClose, FindFirstFileW, WIN32_FIND_DATAW,
210+
};
211+
212+
let wide_path = path.as_ref().to_wide_with_nul();
213+
let mut find_data: WIN32_FIND_DATAW = unsafe { std::mem::zeroed() };
214+
215+
let handle = unsafe { FindFirstFileW(wide_path.as_ptr(), &mut find_data) };
216+
if handle == INVALID_HANDLE_VALUE {
217+
return Err(vm.new_os_error(format!(
218+
"FindFirstFileW failed for path: {}",
219+
path.as_ref().display()
220+
)));
221+
}
222+
223+
unsafe { FindClose(handle) };
224+
225+
// Convert the filename from the find data to a Rust string
226+
// cFileName is a null-terminated wide string
227+
let len = find_data
228+
.cFileName
229+
.iter()
230+
.position(|&c| c == 0)
231+
.unwrap_or(find_data.cFileName.len());
232+
let filename = std::ffi::OsString::from_wide(&find_data.cFileName[..len]);
233+
let filename_str = filename
234+
.to_str()
235+
.ok_or_else(|| vm.new_unicode_decode_error("filename contains invalid UTF-8"))?;
236+
237+
Ok(vm.ctx.new_str(filename_str).to_owned())
159238
}
160239

161240
// cwait is available on MSVC only (according to CPython)

crates/vm/src/stdlib/winapi.rs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ mod _winapi {
316316

317317
#[pyfunction]
318318
fn NeedCurrentDirectoryForExePath(exe_name: PyStrRef) -> bool {
319-
let exe_name = exe_name.as_str().to_wide_with_nul();
319+
let exe_name = exe_name.as_wtf8().to_wide_with_nul();
320320
let return_value = unsafe {
321321
windows_sys::Win32::System::Environment::NeedCurrentDirectoryForExePathW(
322322
exe_name.as_ptr(),
@@ -334,7 +334,7 @@ mod _winapi {
334334
let src_path = std::path::Path::new(src_path.as_str());
335335
let dest_path = std::path::Path::new(dest_path.as_str());
336336

337-
junction::create(dest_path, src_path).map_err(|e| e.to_pyexception(vm))
337+
junction::create(src_path, dest_path).map_err(|e| e.to_pyexception(vm))
338338
}
339339

340340
fn getenvironment(env: ArgMapping, vm: &VirtualMachine) -> PyResult<Vec<u16>> {
@@ -485,9 +485,9 @@ mod _winapi {
485485
// TODO: ctypes.LibraryLoader.LoadLibrary
486486
#[allow(dead_code)]
487487
fn LoadLibrary(path: PyStrRef, vm: &VirtualMachine) -> PyResult<isize> {
488-
let path = path.as_str().to_wide_with_nul();
488+
let path_wide = path.as_wtf8().to_wide_with_nul();
489489
let handle =
490-
unsafe { windows_sys::Win32::System::LibraryLoader::LoadLibraryW(path.as_ptr()) };
490+
unsafe { windows_sys::Win32::System::LibraryLoader::LoadLibraryW(path_wide.as_ptr()) };
491491
if handle.is_null() {
492492
return Err(vm.new_runtime_error("LoadLibrary failed"));
493493
}
@@ -520,7 +520,7 @@ mod _winapi {
520520
name: PyStrRef,
521521
vm: &VirtualMachine,
522522
) -> PyResult<isize> {
523-
let name_wide = name.as_str().to_wide_with_nul();
523+
let name_wide = name.as_wtf8().to_wide_with_nul();
524524
let handle = unsafe {
525525
windows_sys::Win32::System::Threading::OpenMutexW(
526526
desired_access,
@@ -565,13 +565,9 @@ mod _winapi {
565565
return Err(vm.new_value_error("unsupported flags"));
566566
}
567567

568-
// Use encode_wide() which properly handles WTF-8 (including surrogates)
569-
let locale_wide: Vec<u16> = locale
570-
.as_wtf8()
571-
.encode_wide()
572-
.chain(std::iter::once(0))
573-
.collect();
574-
let src_wide: Vec<u16> = src.as_wtf8().encode_wide().collect();
568+
// Use ToWideString which properly handles WTF-8 (including surrogates)
569+
let locale_wide = locale.as_wtf8().to_wide_with_nul();
570+
let src_wide = src.as_wtf8().to_wide();
575571

576572
if src_wide.len() > i32::MAX as usize {
577573
return Err(vm.new_overflow_error("input string is too long".to_string()));
@@ -648,7 +644,7 @@ mod _winapi {
648644
fn CreateNamedPipe(args: CreateNamedPipeArgs, vm: &VirtualMachine) -> PyResult<WinHandle> {
649645
use windows_sys::Win32::System::Pipes::CreateNamedPipeW;
650646

651-
let name_wide = args.name.as_str().to_wide_with_nul();
647+
let name_wide = args.name.as_wtf8().to_wide_with_nul();
652648

653649
let handle = unsafe {
654650
CreateNamedPipeW(

0 commit comments

Comments
 (0)