Skip to main content

rustpython_host_env/
fileutils.rs

1// Python/fileutils.c in CPython
2#![allow(non_snake_case)]
3
4use alloc::ffi::CString;
5
6#[cfg(not(windows))]
7pub use rustix::fs::Stat as StatStruct;
8
9#[cfg(windows)]
10pub use windows::{StatStruct, fstat};
11
12#[cfg(not(windows))]
13pub fn fstat(fd: crate::crt_fd::Borrowed<'_>) -> std::io::Result<StatStruct> {
14    rustix::fs::fstat(fd).map_err(Into::into)
15}
16
17#[cfg(windows)]
18pub mod windows {
19    use crate::crt_fd;
20    use libc::{S_IFCHR, S_IFDIR, S_IFMT};
21    use std::ffi::OsStr;
22    use std::os::windows::io::AsRawHandle;
23    use std::path::Path;
24    use std::sync::OnceLock;
25    use windows_sys::Win32::Foundation::{
26        ERROR_INVALID_HANDLE, ERROR_NOT_SUPPORTED, FILETIME, FreeLibrary, SetLastError,
27    };
28    use windows_sys::Win32::Storage::FileSystem::{
29        BY_HANDLE_FILE_INFORMATION, FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_READONLY,
30        FILE_ATTRIBUTE_REPARSE_POINT, FILE_BASIC_INFO, FILE_ID_INFO, FILE_TYPE_CHAR,
31        FILE_TYPE_DISK, FILE_TYPE_PIPE, FILE_TYPE_UNKNOWN, FileBasicInfo, FileIdInfo,
32        GetFileInformationByHandle, GetFileInformationByHandleEx, GetFileType,
33    };
34    use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryW};
35    use windows_sys::Win32::System::SystemServices::IO_REPARSE_TAG_SYMLINK;
36    use windows_sys::core::PCWSTR;
37    use windows_sys::w;
38
39    pub const S_IFIFO: libc::c_int = 0o010000;
40    pub const S_IFLNK: libc::c_int = 0o120000;
41
42    pub const SECS_BETWEEN_EPOCHS: i64 = 11644473600; // Seconds between 1.1.1601 and 1.1.1970
43
44    #[derive(Clone, Copy, Default)]
45    pub struct StatStruct {
46        pub st_dev: libc::c_ulong,
47        pub st_ino: u64,
48        pub st_mode: libc::c_ushort,
49        pub st_nlink: i32,
50        pub st_uid: i32,
51        pub st_gid: i32,
52        pub st_rdev: libc::c_ulong,
53        pub st_size: u64,
54        pub st_atime: libc::time_t,
55        pub st_atime_nsec: i32,
56        pub st_mtime: libc::time_t,
57        pub st_mtime_nsec: i32,
58        pub st_ctime: libc::time_t,
59        pub st_ctime_nsec: i32,
60        pub st_birthtime: libc::time_t,
61        pub st_birthtime_nsec: i32,
62        pub st_file_attributes: libc::c_ulong,
63        pub st_reparse_tag: u32,
64        pub st_ino_high: u64,
65    }
66
67    impl StatStruct {
68        // update_st_mode_from_path in cpython
69        pub fn update_st_mode_from_path(&mut self, path: &OsStr, attr: u32) {
70            if attr & FILE_ATTRIBUTE_DIRECTORY == 0
71                && let Some(file_extension) =
72                    Path::new(path).extension().and_then(|ext| ext.to_str())
73                && (file_extension.eq_ignore_ascii_case("exe")
74                    || file_extension.eq_ignore_ascii_case("bat")
75                    || file_extension.eq_ignore_ascii_case("cmd")
76                    || file_extension.eq_ignore_ascii_case("com"))
77            {
78                self.st_mode |= 0o111;
79            }
80        }
81    }
82
83    // _Py_fstat_noraise in cpython
84    pub fn fstat(fd: crt_fd::Borrowed<'_>) -> std::io::Result<StatStruct> {
85        let h = match crt_fd::as_handle(fd) {
86            Ok(h) => h,
87            Err(_) => {
88                // An invalid fd is reported as a Win32 handle error so the
89                // OSError carries winerror = ERROR_INVALID_HANDLE.
90                unsafe { SetLastError(ERROR_INVALID_HANDLE) };
91                return Err(std::io::Error::last_os_error());
92            }
93        };
94        let h = h.as_raw_handle();
95        // reset stat?
96
97        let file_type = unsafe { GetFileType(h as _) };
98        if file_type == FILE_TYPE_UNKNOWN {
99            return Err(std::io::Error::last_os_error());
100        }
101        if file_type != FILE_TYPE_DISK {
102            let st_mode = if file_type == FILE_TYPE_CHAR {
103                S_IFCHR
104            } else if file_type == FILE_TYPE_PIPE {
105                S_IFIFO
106            } else {
107                0
108            } as u16;
109            return Ok(StatStruct {
110                st_mode,
111                ..Default::default()
112            });
113        }
114
115        let mut info = unsafe { core::mem::zeroed() };
116        let mut basic_info: FILE_BASIC_INFO = unsafe { core::mem::zeroed() };
117        let mut id_info: FILE_ID_INFO = unsafe { core::mem::zeroed() };
118
119        if unsafe { GetFileInformationByHandle(h as _, &mut info) } == 0
120            || unsafe {
121                GetFileInformationByHandleEx(
122                    h as _,
123                    FileBasicInfo,
124                    &mut basic_info as *mut _ as *mut _,
125                    core::mem::size_of_val(&basic_info) as u32,
126                )
127            } == 0
128        {
129            return Err(std::io::Error::last_os_error());
130        }
131
132        let p_id_info = if unsafe {
133            GetFileInformationByHandleEx(
134                h as _,
135                FileIdInfo,
136                &mut id_info as *mut _ as *mut _,
137                core::mem::size_of_val(&id_info) as u32,
138            )
139        } == 0
140        {
141            None
142        } else {
143            Some(&id_info)
144        };
145
146        Ok(attribute_data_to_stat(
147            &info,
148            0,
149            Some(&basic_info),
150            p_id_info,
151        ))
152    }
153
154    fn large_integer_to_time_t_nsec(input: i64) -> (libc::time_t, libc::c_int) {
155        let nsec_out = (input % 10_000_000) * 100; // FILETIME is in units of 100 nsec.
156        let time_out = ((input / 10_000_000) - SECS_BETWEEN_EPOCHS) as libc::time_t;
157        (time_out, nsec_out as _)
158    }
159
160    fn file_time_to_time_t_nsec(in_ptr: FILETIME) -> (libc::time_t, libc::c_int) {
161        let in_val: i64 = unsafe { core::mem::transmute_copy(&in_ptr) };
162        let nsec_out = (in_val % 10_000_000) * 100; // FILETIME is in units of 100 nsec.
163        let time_out = (in_val / 10_000_000) - SECS_BETWEEN_EPOCHS;
164        (time_out, nsec_out as _)
165    }
166
167    fn attribute_data_to_stat(
168        info: &BY_HANDLE_FILE_INFORMATION,
169        reparse_tag: u32,
170        basic_info: Option<&FILE_BASIC_INFO>,
171        id_info: Option<&FILE_ID_INFO>,
172    ) -> StatStruct {
173        let mut st_mode = attributes_to_mode(info.dwFileAttributes);
174        let st_size = ((info.nFileSizeHigh as u64) << 32) + info.nFileSizeLow as u64;
175        let st_dev: libc::c_ulong = if let Some(id_info) = id_info {
176            id_info.VolumeSerialNumber as _
177        } else {
178            info.dwVolumeSerialNumber
179        };
180        let st_rdev = 0;
181
182        let (st_birthtime, st_ctime, st_mtime, st_atime) = if let Some(basic_info) = basic_info {
183            (
184                large_integer_to_time_t_nsec(basic_info.CreationTime),
185                large_integer_to_time_t_nsec(basic_info.ChangeTime),
186                large_integer_to_time_t_nsec(basic_info.LastWriteTime),
187                large_integer_to_time_t_nsec(basic_info.LastAccessTime),
188            )
189        } else {
190            (
191                file_time_to_time_t_nsec(info.ftCreationTime),
192                (0, 0),
193                file_time_to_time_t_nsec(info.ftLastWriteTime),
194                file_time_to_time_t_nsec(info.ftLastAccessTime),
195            )
196        };
197        let st_nlink = info.nNumberOfLinks as i32;
198
199        let st_ino = if let Some(id_info) = id_info {
200            let file_id: [u64; 2] = unsafe { core::mem::transmute_copy(&id_info.FileId) };
201            file_id
202        } else {
203            let ino = ((info.nFileIndexHigh as u64) << 32) + info.nFileIndexLow as u64;
204            [ino, 0]
205        };
206
207        if info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0
208            && reparse_tag == IO_REPARSE_TAG_SYMLINK
209        {
210            st_mode = (st_mode & !(S_IFMT as u16)) | (S_IFLNK as u16);
211        }
212        let st_file_attributes = info.dwFileAttributes;
213
214        StatStruct {
215            st_dev,
216            st_ino: st_ino[0],
217            st_mode,
218            st_nlink,
219            st_uid: 0,
220            st_gid: 0,
221            st_rdev,
222            st_size,
223            st_atime: st_atime.0,
224            st_atime_nsec: st_atime.1,
225            st_mtime: st_mtime.0,
226            st_mtime_nsec: st_mtime.1,
227            st_ctime: st_ctime.0,
228            st_ctime_nsec: st_ctime.1,
229            st_birthtime: st_birthtime.0,
230            st_birthtime_nsec: st_birthtime.1,
231            st_file_attributes,
232            st_reparse_tag: reparse_tag,
233            st_ino_high: st_ino[1],
234        }
235    }
236
237    const fn attributes_to_mode(attr: u32) -> u16 {
238        let mut m = 0;
239        if attr & FILE_ATTRIBUTE_DIRECTORY != 0 {
240            m |= libc::S_IFDIR | 0o111; // IFEXEC for user,group,other
241        } else {
242            m |= libc::S_IFREG;
243        }
244        if attr & FILE_ATTRIBUTE_READONLY != 0 {
245            m |= 0o444;
246        } else {
247            m |= 0o666;
248        }
249        m as _
250    }
251
252    #[derive(Clone, Copy)]
253    #[repr(C)]
254    pub struct FILE_STAT_BASIC_INFORMATION {
255        pub FileId: i64,
256        pub CreationTime: i64,
257        pub LastAccessTime: i64,
258        pub LastWriteTime: i64,
259        pub ChangeTime: i64,
260        pub AllocationSize: i64,
261        pub EndOfFile: i64,
262        pub FileAttributes: u32,
263        pub ReparseTag: u32,
264        pub NumberOfLinks: u32,
265        pub DeviceType: u32,
266        pub DeviceCharacteristics: u32,
267        pub Reserved: u32,
268        pub VolumeSerialNumber: i64,
269        pub FileId128: [u64; 2],
270    }
271
272    #[allow(dead_code)]
273    #[derive(Clone, Copy)]
274    #[repr(C)]
275    pub enum FILE_INFO_BY_NAME_CLASS {
276        FileStatByNameInfo,
277        FileStatLxByNameInfo,
278        FileCaseSensitiveByNameInfo,
279        FileStatBasicByNameInfo,
280        MaximumFileInfoByNameClass,
281    }
282
283    // _Py_GetFileInformationByName in cpython
284    pub fn get_file_information_by_name(
285        file_name: &widestring::WideCStr,
286        file_information_class: FILE_INFO_BY_NAME_CLASS,
287    ) -> std::io::Result<FILE_STAT_BASIC_INFORMATION> {
288        static GET_FILE_INFORMATION_BY_NAME: OnceLock<
289            Option<
290                unsafe extern "system" fn(
291                    PCWSTR,
292                    FILE_INFO_BY_NAME_CLASS,
293                    *mut libc::c_void,
294                    u32,
295                ) -> i32,
296            >,
297        > = OnceLock::new();
298
299        let GetFileInformationByName = GET_FILE_INFORMATION_BY_NAME
300            .get_or_init(|| {
301                let library_name = w!("api-ms-win-core-file-l2-1-4.dll");
302                let module = unsafe { LoadLibraryW(library_name) };
303                if module.is_null() {
304                    return None;
305                }
306                let name = c"GetFileInformationByName";
307                if let Some(proc) = unsafe { GetProcAddress(module, name.as_ptr().cast()) } {
308                    Some(unsafe {
309                        core::mem::transmute::<
310                            unsafe extern "system" fn() -> isize,
311                            unsafe extern "system" fn(
312                                *const u16,
313                                FILE_INFO_BY_NAME_CLASS,
314                                *mut libc::c_void,
315                                u32,
316                            ) -> i32,
317                        >(proc)
318                    })
319                } else {
320                    unsafe { FreeLibrary(module) };
321                    None
322                }
323            })
324            .ok_or_else(|| std::io::Error::from_raw_os_error(ERROR_NOT_SUPPORTED as _))?;
325
326        let file_info_buffer_size = core::mem::size_of::<FILE_STAT_BASIC_INFORMATION>() as u32;
327        let mut file_info_buffer = core::mem::MaybeUninit::<FILE_STAT_BASIC_INFORMATION>::uninit();
328        unsafe {
329            if GetFileInformationByName(
330                file_name.as_ptr(),
331                file_information_class as _,
332                file_info_buffer.as_mut_ptr() as _,
333                file_info_buffer_size,
334            ) == 0
335            {
336                Err(std::io::Error::last_os_error())
337            } else {
338                Ok(file_info_buffer.assume_init())
339            }
340        }
341    }
342
343    #[must_use]
344    pub fn stat_basic_info_to_stat(info: &FILE_STAT_BASIC_INFORMATION) -> StatStruct {
345        use windows_sys::Win32::Storage::FileSystem;
346        use windows_sys::Win32::System::Ioctl;
347
348        const S_IFMT: u16 = self::S_IFMT as _;
349        const S_IFDIR: u16 = self::S_IFDIR as _;
350        const S_IFCHR: u16 = self::S_IFCHR as _;
351        const S_IFIFO: u16 = self::S_IFIFO as _;
352        const S_IFLNK: u16 = self::S_IFLNK as _;
353
354        let mut st_mode = attributes_to_mode(info.FileAttributes);
355        let st_size = info.EndOfFile as u64;
356        let st_birthtime = large_integer_to_time_t_nsec(info.CreationTime);
357        let st_ctime = large_integer_to_time_t_nsec(info.ChangeTime);
358        let st_mtime = large_integer_to_time_t_nsec(info.LastWriteTime);
359        let st_atime = large_integer_to_time_t_nsec(info.LastAccessTime);
360        let st_nlink = info.NumberOfLinks as _;
361        let st_dev = info.VolumeSerialNumber as u32;
362        // File systems with less than 128-bits zero pad into this field
363        let st_ino = info.FileId128;
364        // bpo-37834: Only actual symlinks set the S_IFLNK flag. But lstat() will
365        // open other name surrogate reparse points without traversing them. To
366        // detect/handle these, check st_file_attributes and st_reparse_tag.
367        let st_reparse_tag = info.ReparseTag;
368        if info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0
369            && info.ReparseTag == IO_REPARSE_TAG_SYMLINK
370        {
371            // set the bits that make this a symlink
372            st_mode = (st_mode & !S_IFMT) | S_IFLNK;
373        }
374        let st_file_attributes = info.FileAttributes;
375        match info.DeviceType {
376            FileSystem::FILE_DEVICE_DISK
377            | Ioctl::FILE_DEVICE_VIRTUAL_DISK
378            | Ioctl::FILE_DEVICE_DFS
379            | FileSystem::FILE_DEVICE_CD_ROM
380            | Ioctl::FILE_DEVICE_CONTROLLER
381            | Ioctl::FILE_DEVICE_DATALINK => {}
382            Ioctl::FILE_DEVICE_DISK_FILE_SYSTEM
383            | Ioctl::FILE_DEVICE_CD_ROM_FILE_SYSTEM
384            | Ioctl::FILE_DEVICE_NETWORK_FILE_SYSTEM => {
385                st_mode = (st_mode & !S_IFMT) | 0x6000; // _S_IFBLK
386            }
387            Ioctl::FILE_DEVICE_CONSOLE
388            | Ioctl::FILE_DEVICE_NULL
389            | Ioctl::FILE_DEVICE_KEYBOARD
390            | Ioctl::FILE_DEVICE_MODEM
391            | Ioctl::FILE_DEVICE_MOUSE
392            | Ioctl::FILE_DEVICE_PARALLEL_PORT
393            | Ioctl::FILE_DEVICE_PRINTER
394            | Ioctl::FILE_DEVICE_SCREEN
395            | Ioctl::FILE_DEVICE_SERIAL_PORT
396            | Ioctl::FILE_DEVICE_SOUND => {
397                st_mode = (st_mode & !S_IFMT) | S_IFCHR;
398            }
399            Ioctl::FILE_DEVICE_NAMED_PIPE => {
400                st_mode = (st_mode & !S_IFMT) | S_IFIFO;
401            }
402            _ => {
403                if info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0 {
404                    st_mode = (st_mode & !S_IFMT) | S_IFDIR;
405                }
406            }
407        }
408
409        StatStruct {
410            st_dev,
411            st_ino: st_ino[0],
412            st_mode,
413            st_nlink,
414            st_uid: 0,
415            st_gid: 0,
416            st_rdev: 0,
417            st_size,
418            st_atime: st_atime.0,
419            st_atime_nsec: st_atime.1,
420            st_mtime: st_mtime.0,
421            st_mtime_nsec: st_mtime.1,
422            st_ctime: st_ctime.0,
423            st_ctime_nsec: st_ctime.1,
424            st_birthtime: st_birthtime.0,
425            st_birthtime_nsec: st_birthtime.1,
426            st_file_attributes,
427            st_reparse_tag,
428            st_ino_high: st_ino[1],
429        }
430    }
431}
432
433/// C `FILE *` handle as returned by [`fopen`] and consumed by [`fclose`].
434pub type CFile = libc::FILE;
435
436/// Close a file opened with [`fopen`].
437///
438/// # Safety
439/// `fp` must be a non-null pointer returned by [`fopen`] and must not have been
440/// closed already.
441pub unsafe fn fclose(fp: *mut CFile) -> core::ffi::c_int {
442    unsafe { libc::fclose(fp) }
443}
444
445// _Py_fopen_obj in cpython (Python/fileutils.c:1757-1835)
446// Open a file using std::fs::File and convert to FILE*
447// Automatically handles path encoding and EINTR retries
448pub fn fopen(path: &std::path::Path, mode: &str) -> std::io::Result<*mut CFile> {
449    use std::fs::File;
450
451    // Currently only supports read mode
452    // Can be extended to support "wb", "w+b", etc. if needed
453    if mode != "rb" {
454        return Err(std::io::Error::new(
455            std::io::ErrorKind::InvalidInput,
456            format!("unsupported mode: {mode}"),
457        ));
458    }
459
460    // Open file using std::fs::File (handles path encoding and EINTR automatically)
461    let file = File::open(path)?;
462
463    #[cfg(windows)]
464    {
465        use std::os::windows::io::IntoRawHandle;
466
467        // Convert File handle to CRT file descriptor
468        let handle = file.into_raw_handle();
469        let fd = unsafe { libc::open_osfhandle(handle as isize, crate::os::O_RDONLY) };
470        if fd == -1 {
471            return Err(std::io::Error::last_os_error());
472        }
473
474        // Convert fd to FILE*
475        let mode_cstr = CString::new(mode).unwrap();
476        let fp = unsafe { libc::fdopen(fd, mode_cstr.as_ptr()) };
477        if fp.is_null() {
478            unsafe { libc::close(fd) };
479            return Err(std::io::Error::last_os_error());
480        }
481
482        // Set non-inheritable (Windows needs this explicitly)
483        if let Err(e) = set_inheritable(fd, false) {
484            unsafe { libc::fclose(fp) };
485            return Err(e);
486        }
487
488        Ok(fp)
489    }
490
491    #[cfg(not(windows))]
492    {
493        use std::os::fd::IntoRawFd;
494
495        // Convert File to raw fd
496        let fd = file.into_raw_fd();
497
498        // Convert fd to FILE*
499        let mode_cstr = CString::new(mode).unwrap();
500        let fp = unsafe { libc::fdopen(fd, mode_cstr.as_ptr()) };
501        if fp.is_null() {
502            unsafe { libc::close(fd) };
503            return Err(std::io::Error::last_os_error());
504        }
505
506        // Unix: O_CLOEXEC is already set by File::open, so non-inheritable is automatic
507        Ok(fp)
508    }
509}
510
511// set_inheritable in cpython (Python/fileutils.c:1443-1570)
512// Set the inheritable flag of the specified file descriptor
513// Only used on Windows; Unix automatically sets O_CLOEXEC
514#[cfg(windows)]
515fn set_inheritable(fd: libc::c_int, inheritable: bool) -> std::io::Result<()> {
516    use windows_sys::Win32::Foundation::{
517        HANDLE, HANDLE_FLAG_INHERIT, INVALID_HANDLE_VALUE, SetHandleInformation,
518    };
519
520    let handle = unsafe { libc::get_osfhandle(fd) };
521    if handle == INVALID_HANDLE_VALUE as isize {
522        return Err(std::io::Error::last_os_error());
523    }
524
525    let flags = if inheritable { HANDLE_FLAG_INHERIT } else { 0 };
526    let result = unsafe { SetHandleInformation(handle as HANDLE, HANDLE_FLAG_INHERIT, flags) };
527    if result == 0 {
528        return Err(std::io::Error::last_os_error());
529    }
530
531    Ok(())
532}