Skip to content

Commit 7313b7c

Browse files
committed
winapi._findfirstfile,nt.chmod
1 parent 0054394 commit 7313b7c

File tree

3 files changed

+93
-21
lines changed

3 files changed

+93
-21
lines changed

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/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+
if meta.file_type().is_symlink() {
185+
return Err(vm.new_not_implemented_error(
186+
"chmod: follow_symlinks=False is not supported on Windows for symlinks"
187+
.to_owned(),
188+
));
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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>> {

0 commit comments

Comments
 (0)