@@ -191,9 +191,9 @@ pub(crate) mod module {
191191 }
192192
193193 #[ derive( FromArgs ) ]
194- struct ChmodArgs {
194+ struct ChmodArgs < ' a > {
195195 #[ pyarg( any) ]
196- path : OsPath ,
196+ path : OsPathOrFd < ' a > ,
197197 #[ pyarg( any) ]
198198 mode : u32 ,
199199 #[ pyarg( flatten) ]
@@ -202,17 +202,85 @@ pub(crate) mod module {
202202 follow_symlinks : OptionalArg < bool > ,
203203 }
204204
205+ const S_IWRITE : u32 = 128 ;
206+
207+ fn fchmod_impl ( fd : i32 , mode : u32 , vm : & VirtualMachine ) -> PyResult < ( ) > {
208+ use windows_sys:: Win32 :: Storage :: FileSystem :: {
209+ FILE_BASIC_INFO , FileBasicInfo , GetFileInformationByHandleEx ,
210+ SetFileInformationByHandle ,
211+ } ;
212+
213+ // Get Windows HANDLE from fd
214+ let borrowed = unsafe { crt_fd:: Borrowed :: borrow_raw ( fd) } ;
215+ let handle = crt_fd:: as_handle ( borrowed) . map_err ( |e| e. to_pyexception ( vm) ) ?;
216+ let hfile = handle. as_raw_handle ( ) as Foundation :: HANDLE ;
217+
218+ // Get current file info
219+ let mut info: FILE_BASIC_INFO = unsafe { std:: mem:: zeroed ( ) } ;
220+ let ret = unsafe {
221+ GetFileInformationByHandleEx (
222+ hfile,
223+ FileBasicInfo ,
224+ & mut info as * mut _ as * mut _ ,
225+ std:: mem:: size_of :: < FILE_BASIC_INFO > ( ) as u32 ,
226+ )
227+ } ;
228+ if ret == 0 {
229+ return Err ( vm. new_last_os_error ( ) ) ;
230+ }
231+
232+ // Modify readonly attribute based on S_IWRITE bit
233+ if mode & S_IWRITE != 0 {
234+ info. FileAttributes &= !FileSystem :: FILE_ATTRIBUTE_READONLY ;
235+ } else {
236+ info. FileAttributes |= FileSystem :: FILE_ATTRIBUTE_READONLY ;
237+ }
238+
239+ // Set the new attributes
240+ let ret = unsafe {
241+ SetFileInformationByHandle (
242+ hfile,
243+ FileBasicInfo ,
244+ & info as * const _ as * const _ ,
245+ std:: mem:: size_of :: < FILE_BASIC_INFO > ( ) as u32 ,
246+ )
247+ } ;
248+ if ret == 0 {
249+ return Err ( vm. new_last_os_error ( ) ) ;
250+ }
251+
252+ Ok ( ( ) )
253+ }
254+
205255 #[ pyfunction]
206- fn chmod ( args : ChmodArgs , vm : & VirtualMachine ) -> PyResult < ( ) > {
256+ fn fchmod ( fd : i32 , mode : u32 , vm : & VirtualMachine ) -> PyResult < ( ) > {
257+ fchmod_impl ( fd, mode, vm)
258+ }
259+
260+ #[ pyfunction]
261+ fn chmod ( args : ChmodArgs < ' _ > , vm : & VirtualMachine ) -> PyResult < ( ) > {
207262 let ChmodArgs {
208263 path,
209264 mode,
210265 dir_fd,
211266 follow_symlinks,
212267 } = args;
213- const S_IWRITE : u32 = 128 ;
214268 let [ ] = dir_fd. 0 ;
215269
270+ // If path is a file descriptor, use fchmod
271+ if let OsPathOrFd :: Fd ( fd) = path {
272+ if follow_symlinks. into_option ( ) . is_some ( ) {
273+ return Err ( vm. new_value_error (
274+ "chmod: follow_symlinks is not supported with fd argument" . to_owned ( ) ,
275+ ) ) ;
276+ }
277+ return fchmod_impl ( fd. as_raw ( ) , mode, vm) ;
278+ }
279+
280+ let OsPathOrFd :: Path ( path) = path else {
281+ unreachable ! ( )
282+ } ;
283+
216284 // On Windows, os.chmod behavior differs based on whether follow_symlinks is explicitly provided:
217285 // - Not provided (default): use SetFileAttributesW on the path directly (doesn't follow symlinks)
218286 // - Explicitly True: resolve symlink first, then apply permissions to target
0 commit comments