@@ -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)
0 commit comments