diff --git a/Cargo.lock b/Cargo.lock index d96db01ccb7..20fa9a531bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1975,9 +1975,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.28" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ "bitflags 2.5.0", "errno", diff --git a/Lib/test/test_genericpath.py b/Lib/test/test_genericpath.py index aef6bf9d168..1ff7f75ad3e 100644 --- a/Lib/test/test_genericpath.py +++ b/Lib/test/test_genericpath.py @@ -242,7 +242,6 @@ def _test_samefile_on_link_func(self, func): create_file(test_fn2) self.assertFalse(self.pathmodule.samefile(test_fn1, test_fn2)) - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON; properly implement stat st_dev/st_ino") @os_helper.skip_unless_symlink def test_samefile_on_symlink(self): self._test_samefile_on_link_func(os.symlink) @@ -285,7 +284,6 @@ def _test_samestat_on_link_func(self, func): self.assertFalse(self.pathmodule.samestat(os.stat(test_fn1), os.stat(test_fn2))) - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON; properly implement stat st_dev/st_ino") @os_helper.skip_unless_symlink def test_samestat_on_symlink(self): self._test_samestat_on_link_func(os.symlink) @@ -314,30 +312,6 @@ class TestGenericTest(GenericTest, unittest.TestCase): # and is only meant to be inherited by others. pathmodule = genericpath - # TODO: RUSTPYTHON - if sys.platform == "win32": - @unittest.expectedFailure - def test_samefile(self): - super().test_samefile() - - # TODO: RUSTPYTHON - if sys.platform == "win32": - @unittest.expectedFailure - def test_samefile_on_link(self): - super().test_samefile_on_link() - - # TODO: RUSTPYTHON - if sys.platform == "win32": - @unittest.expectedFailure - def test_samestat(self): - super().test_samestat() - - # TODO: RUSTPYTHON - if sys.platform == "win32": - @unittest.expectedFailure - def test_samestat_on_link(self): - super().test_samestat_on_link() - def test_invalid_paths(self): for attr in GenericTest.common_attributes: # os.path.commonprefix doesn't raise ValueError diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 8c01cdc79e2..1260098842b 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -912,26 +912,6 @@ def test_expandvars(self): # TODO: RUSTPYTHON; remove when done def test_expandvars_nonascii(self): # TODO: RUSTPYTHON; remove when done super().test_expandvars_nonascii() - # TODO: RUSTPYTHON - @unittest.expectedFailure - def test_samefile(self): # TODO: RUSTPYTHON; remove when done - super().test_samefile() - - # TODO: RUSTPYTHON - @unittest.expectedFailure - def test_samefile_on_link(self): # TODO: RUSTPYTHON; remove when done - super().test_samefile_on_link() - - # TODO: RUSTPYTHON - @unittest.expectedFailure - def test_samestat(self): # TODO: RUSTPYTHON; remove when done - super().test_samestat() - - # TODO: RUSTPYTHON - @unittest.expectedFailure - def test_samestat_on_link(self): # TODO: RUSTPYTHON; remove when done - super().test_samestat_on_link() - class PathLikeTests(NtpathTestCase): diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index cd1897090a6..16416547c1a 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -715,7 +715,6 @@ def test_copytree_simple(self): actual = read_file((dst_dir, 'test_dir', 'test.txt')) self.assertEqual(actual, '456') - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") def test_copytree_dirs_exist_ok(self): src_dir = self.mkdtemp() dst_dir = self.mkdtemp() @@ -1557,7 +1556,6 @@ def test_copyfile_nonexistent_dir(self): write_file(src_file, 'foo') self.assertRaises(FileNotFoundError, shutil.copyfile, src_file, dst) - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") def test_copyfile_copy_dir(self): # Issue 45234 # test copy() and copyfile() raising proper exceptions when src and/or @@ -2498,18 +2496,15 @@ def test_move_file(self): # Move a file to another location on the same filesystem. self._check_move_file(self.src_file, self.dst_file, self.dst_file) - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") def test_move_file_to_dir(self): # Move a file inside an existing dir on the same filesystem. self._check_move_file(self.src_file, self.dst_dir, self.dst_file) - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") def test_move_file_to_dir_pathlike_src(self): # Move a pathlike file to another location on the same filesystem. src = pathlib.Path(self.src_file) self._check_move_file(src, self.dst_dir, self.dst_file) - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") def test_move_file_to_dir_pathlike_dst(self): # Move a file to another pathlike location on the same filesystem. dst = pathlib.Path(self.dst_dir) @@ -2520,7 +2515,6 @@ def test_move_file_other_fs(self): # Move a file to an existing dir on another filesystem. self.test_move_file() - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") @mock_rename def test_move_file_to_dir_other_fs(self): # Move a file to another location on another filesystem. @@ -2539,30 +2533,25 @@ def test_move_dir_other_fs(self): # Move a dir to another location on another filesystem. self.test_move_dir() - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") def test_move_dir_to_dir(self): # Move a dir inside an existing dir on the same filesystem. self._check_move_dir(self.src_dir, self.dst_dir, os.path.join(self.dst_dir, os.path.basename(self.src_dir))) - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") @mock_rename def test_move_dir_to_dir_other_fs(self): # Move a dir inside an existing dir on another filesystem. self.test_move_dir_to_dir() - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") def test_move_dir_sep_to_dir(self): self._check_move_dir(self.src_dir + os.path.sep, self.dst_dir, os.path.join(self.dst_dir, os.path.basename(self.src_dir))) - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") @unittest.skipUnless(os.path.altsep, 'requires os.path.altsep') def test_move_dir_altsep_to_dir(self): self._check_move_dir(self.src_dir + os.path.altsep, self.dst_dir, os.path.join(self.dst_dir, os.path.basename(self.src_dir))) - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") def test_existing_file_inside_dest_dir(self): # A file with the same name inside the destination dir already exists. with open(self.dst_file, "wb"): @@ -2645,7 +2634,6 @@ def test_move_dir_symlink(self): self.assertTrue(os.path.islink(dst_link)) self.assertTrue(os.path.samefile(src, dst_link)) - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") def test_move_return_value(self): rv = shutil.move(self.src_file, self.dst_dir) self.assertEqual(rv, @@ -2655,7 +2643,6 @@ def test_move_as_rename_return_value(self): rv = shutil.move(self.src_file, os.path.join(self.dst_dir, 'bar')) self.assertEqual(rv, os.path.join(self.dst_dir, 'bar')) - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") @mock_rename def test_move_file_special_function(self): moved = [] @@ -2664,7 +2651,6 @@ def _copy(src, dst): shutil.move(self.src_file, self.dst_dir, copy_function=_copy) self.assertEqual(len(moved), 1) - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") @mock_rename def test_move_dir_special_function(self): moved = [] @@ -2693,7 +2679,6 @@ def test_move_dir_caseinsensitive(self): # bpo-26791: Check that a symlink to a directory can # be moved into that directory. - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") @mock_rename def _test_move_symlink_to_dir_into_dir(self, dst): src = os.path.join(self.src_dir, 'linktodir') @@ -2894,7 +2879,6 @@ def test_file_offset(self): self.assertEqual(src.tell(), self.FILESIZE) self.assertEqual(dst.tell(), self.FILESIZE) - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") @unittest.skipIf(os.name != 'nt', "Windows only") def test_win_impl(self): # Make sure alternate Windows implementation is called. diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index 3d6e5a5a97b..4ae81cb99f7 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -1299,6 +1299,7 @@ def test_link_size(self): os_helper.unlink(target) os_helper.unlink(link) + @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") @os_helper.skip_unless_symlink def test_symlink_size(self): path = os.path.join(TEMPDIR, "symlink") @@ -1496,13 +1497,6 @@ def expectedSuccess(test_item): def test_cwd(self): super().test_cwd() - # TODO: RUSTPYTHON - if sys.platform == "win32": - @expectedSuccess - def test_symlink_size(self): - super().test_symlink_size() - pass - class Bz2WriteTest(Bz2Test, WriteTest): pass diff --git a/common/Cargo.toml b/common/Cargo.toml index 28f6c4098e2..654a7e65355 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -38,4 +38,8 @@ widestring = { workspace = true } windows-sys = { workspace = true, features = [ "Win32_Foundation", "Win32_Networking_WinSock", + "Win32_Storage_FileSystem", + "Win32_System_Ioctl", + "Win32_System_LibraryLoader", + "Win32_System_SystemServices", ] } diff --git a/common/src/fileutils.rs b/common/src/fileutils.rs new file mode 100644 index 00000000000..104c6bcd287 --- /dev/null +++ b/common/src/fileutils.rs @@ -0,0 +1,442 @@ +// Python/fileutils.c in CPython +#![allow(non_snake_case)] + +#[cfg(not(windows))] +pub use libc::stat as StatStruct; + +#[cfg(windows)] +pub use windows::{fstat, StatStruct}; + +#[cfg(not(windows))] +pub fn fstat(fd: libc::c_int) -> std::io::Result { + let mut stat = std::mem::MaybeUninit::uninit(); + unsafe { + let ret = libc::fstat(fd, stat.as_mut_ptr()); + if ret == -1 { + Err(crate::os::last_os_error()) + } else { + Ok(stat.assume_init()) + } + } +} + +#[cfg(windows)] +pub mod windows { + use crate::suppress_iph; + use crate::windows::ToWideString; + use libc::{S_IFCHR, S_IFDIR, S_IFMT}; + use std::ffi::{CString, OsStr, OsString}; + use std::os::windows::ffi::OsStrExt; + use std::sync::OnceLock; + use windows_sys::core::PCWSTR; + use windows_sys::Win32::Foundation::{ + FreeLibrary, SetLastError, BOOL, ERROR_INVALID_HANDLE, ERROR_NOT_SUPPORTED, FILETIME, + HANDLE, INVALID_HANDLE_VALUE, + }; + use windows_sys::Win32::Storage::FileSystem::{ + FileBasicInfo, FileIdInfo, GetFileInformationByHandle, GetFileInformationByHandleEx, + GetFileType, BY_HANDLE_FILE_INFORMATION, FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_READONLY, + FILE_ATTRIBUTE_REPARSE_POINT, FILE_BASIC_INFO, FILE_ID_INFO, FILE_TYPE_CHAR, + FILE_TYPE_DISK, FILE_TYPE_PIPE, FILE_TYPE_UNKNOWN, + }; + use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryW}; + use windows_sys::Win32::System::SystemServices::IO_REPARSE_TAG_SYMLINK; + + pub const S_IFIFO: libc::c_int = 0o010000; + pub const S_IFLNK: libc::c_int = 0o120000; + + pub const SECS_BETWEEN_EPOCHS: i64 = 11644473600; // Seconds between 1.1.1601 and 1.1.1970 + + #[derive(Default)] + pub struct StatStruct { + pub st_dev: libc::c_ulong, + pub st_ino: u64, + pub st_mode: libc::c_ushort, + pub st_nlink: i32, + pub st_uid: i32, + pub st_gid: i32, + pub st_rdev: libc::c_ulong, + pub st_size: u64, + pub st_atime: libc::time_t, + pub st_atime_nsec: i32, + pub st_mtime: libc::time_t, + pub st_mtime_nsec: i32, + pub st_ctime: libc::time_t, + pub st_ctime_nsec: i32, + pub st_birthtime: libc::time_t, + pub st_birthtime_nsec: i32, + pub st_file_attributes: libc::c_ulong, + pub st_reparse_tag: u32, + pub st_ino_high: u64, + } + + impl StatStruct { + // update_st_mode_from_path in cpython + pub fn update_st_mode_from_path(&mut self, path: &OsStr, attr: u32) { + if attr & FILE_ATTRIBUTE_DIRECTORY == 0 { + let file_extension = path + .encode_wide() + .collect::>() + .split(|&c| c == '.' as u16) + .last() + .and_then(|s| String::from_utf16(s).ok()); + + if let Some(file_extension) = file_extension { + if file_extension.eq_ignore_ascii_case("exe") + || file_extension.eq_ignore_ascii_case("bat") + || file_extension.eq_ignore_ascii_case("cmd") + || file_extension.eq_ignore_ascii_case("com") + { + self.st_mode |= 0o111; + } + } + } + } + } + + extern "C" { + fn _get_osfhandle(fd: i32) -> libc::intptr_t; + } + + fn get_osfhandle(fd: i32) -> std::io::Result { + let ret = unsafe { suppress_iph!(_get_osfhandle(fd)) }; + if ret as HANDLE == INVALID_HANDLE_VALUE { + Err(crate::os::last_os_error()) + } else { + Ok(ret) + } + } + + // _Py_fstat_noraise in cpython + pub fn fstat(fd: libc::c_int) -> std::io::Result { + let h = get_osfhandle(fd); + if h.is_err() { + unsafe { SetLastError(ERROR_INVALID_HANDLE) }; + } + let h = h?; + // reset stat? + + let file_type = unsafe { GetFileType(h) }; + if file_type == FILE_TYPE_UNKNOWN { + return Err(std::io::Error::last_os_error()); + } + if file_type != FILE_TYPE_DISK { + let st_mode = if file_type == FILE_TYPE_CHAR { + S_IFCHR + } else if file_type == FILE_TYPE_PIPE { + S_IFIFO + } else { + 0 + } as u16; + return Ok(StatStruct { + st_mode, + ..Default::default() + }); + } + + let mut info = unsafe { std::mem::zeroed() }; + let mut basic_info: FILE_BASIC_INFO = unsafe { std::mem::zeroed() }; + let mut id_info: FILE_ID_INFO = unsafe { std::mem::zeroed() }; + + if unsafe { GetFileInformationByHandle(h, &mut info) } == 0 + || unsafe { + GetFileInformationByHandleEx( + h, + FileBasicInfo, + &mut basic_info as *mut _ as *mut _, + std::mem::size_of_val(&basic_info) as u32, + ) + } == 0 + { + return Err(std::io::Error::last_os_error()); + } + + let p_id_info = if unsafe { + GetFileInformationByHandleEx( + h, + FileIdInfo, + &mut id_info as *mut _ as *mut _, + std::mem::size_of_val(&id_info) as u32, + ) + } == 0 + { + None + } else { + Some(&id_info) + }; + + Ok(attribute_data_to_stat( + &info, + 0, + Some(&basic_info), + p_id_info, + )) + } + + fn large_integer_to_time_t_nsec(input: i64) -> (libc::time_t, libc::c_int) { + let nsec_out = (input % 10_000_000) * 100; // FILETIME is in units of 100 nsec. + let time_out = ((input / 10_000_000) - SECS_BETWEEN_EPOCHS) as libc::time_t; + (time_out, nsec_out as _) + } + + fn file_time_to_time_t_nsec(in_ptr: &FILETIME) -> (libc::time_t, libc::c_int) { + let in_val: i64 = unsafe { std::mem::transmute_copy(in_ptr) }; + let nsec_out = (in_val % 10_000_000) * 100; // FILETIME is in units of 100 nsec. + let time_out = (in_val / 10_000_000) - SECS_BETWEEN_EPOCHS; + (time_out, nsec_out as _) + } + + fn attribute_data_to_stat( + info: &BY_HANDLE_FILE_INFORMATION, + reparse_tag: u32, + basic_info: Option<&FILE_BASIC_INFO>, + id_info: Option<&FILE_ID_INFO>, + ) -> StatStruct { + let mut st_mode = attributes_to_mode(info.dwFileAttributes); + let st_size = ((info.nFileSizeHigh as u64) << 32) + info.nFileSizeLow as u64; + let st_dev: libc::c_ulong = if let Some(id_info) = id_info { + id_info.VolumeSerialNumber as _ + } else { + info.dwVolumeSerialNumber + }; + let st_rdev = 0; + + let (st_birthtime, st_ctime, st_mtime, st_atime) = if let Some(basic_info) = basic_info { + ( + large_integer_to_time_t_nsec(basic_info.CreationTime), + large_integer_to_time_t_nsec(basic_info.ChangeTime), + large_integer_to_time_t_nsec(basic_info.LastWriteTime), + large_integer_to_time_t_nsec(basic_info.LastAccessTime), + ) + } else { + ( + file_time_to_time_t_nsec(&info.ftCreationTime), + (0, 0), + file_time_to_time_t_nsec(&info.ftLastWriteTime), + file_time_to_time_t_nsec(&info.ftLastAccessTime), + ) + }; + let st_nlink = info.nNumberOfLinks as i32; + + let st_ino = if let Some(id_info) = id_info { + let file_id: [u64; 2] = unsafe { std::mem::transmute_copy(&id_info.FileId) }; + file_id + } else { + let ino = ((info.nFileIndexHigh as u64) << 32) + info.nFileIndexLow as u64; + [ino, 0] + }; + + if info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 + && reparse_tag == IO_REPARSE_TAG_SYMLINK + { + st_mode = (st_mode & !(S_IFMT as u16)) | (S_IFLNK as u16); + } + let st_file_attributes = info.dwFileAttributes; + + StatStruct { + st_dev, + st_ino: st_ino[0], + st_mode, + st_nlink, + st_uid: 0, + st_gid: 0, + st_rdev, + st_size, + st_atime: st_atime.0, + st_atime_nsec: st_atime.1, + st_mtime: st_mtime.0, + st_mtime_nsec: st_mtime.1, + st_ctime: st_ctime.0, + st_ctime_nsec: st_ctime.1, + st_birthtime: st_birthtime.0, + st_birthtime_nsec: st_birthtime.1, + st_file_attributes, + st_reparse_tag: reparse_tag, + st_ino_high: st_ino[1], + } + } + + fn attributes_to_mode(attr: u32) -> u16 { + let mut m = 0; + if attr & FILE_ATTRIBUTE_DIRECTORY != 0 { + m |= libc::S_IFDIR | 0o111; // IFEXEC for user,group,other + } else { + m |= libc::S_IFREG; + } + if attr & FILE_ATTRIBUTE_READONLY != 0 { + m |= 0o444; + } else { + m |= 0o666; + } + m as _ + } + + #[repr(C)] + pub struct FILE_STAT_BASIC_INFORMATION { + pub FileId: i64, + pub CreationTime: i64, + pub LastAccessTime: i64, + pub LastWriteTime: i64, + pub ChangeTime: i64, + pub AllocationSize: i64, + pub EndOfFile: i64, + pub FileAttributes: u32, + pub ReparseTag: u32, + pub NumberOfLinks: u32, + pub DeviceType: u32, + pub DeviceCharacteristics: u32, + pub Reserved: u32, + pub VolumeSerialNumber: i64, + pub FileId128: [u64; 2], + } + + #[repr(C)] + #[allow(dead_code)] + pub enum FILE_INFO_BY_NAME_CLASS { + FileStatByNameInfo, + FileStatLxByNameInfo, + FileCaseSensitiveByNameInfo, + FileStatBasicByNameInfo, + MaximumFileInfoByNameClass, + } + + // _Py_GetFileInformationByName in cpython + pub fn get_file_information_by_name( + file_name: &OsStr, + file_information_class: FILE_INFO_BY_NAME_CLASS, + ) -> std::io::Result { + static GET_FILE_INFORMATION_BY_NAME: OnceLock< + Option< + unsafe extern "system" fn( + PCWSTR, + FILE_INFO_BY_NAME_CLASS, + *mut libc::c_void, + u32, + ) -> BOOL, + >, + > = OnceLock::new(); + + let GetFileInformationByName = GET_FILE_INFORMATION_BY_NAME + .get_or_init(|| { + let library_name = OsString::from("api-ms-win-core-file-l2-1-4").to_wide_with_nul(); + let module = unsafe { LoadLibraryW(library_name.as_ptr()) }; + if module == 0 { + return None; + } + let name = CString::new("GetFileInformationByName").unwrap(); + if let Some(proc) = + unsafe { GetProcAddress(module, name.as_bytes_with_nul().as_ptr()) } + { + Some(unsafe { std::mem::transmute(proc) }) + } else { + unsafe { FreeLibrary(module) }; + None + } + }) + .ok_or_else(|| std::io::Error::from_raw_os_error(ERROR_NOT_SUPPORTED as _))?; + + let file_name = file_name.to_wide_with_nul(); + let file_info_buffer_size = std::mem::size_of::() as u32; + let mut file_info_buffer = std::mem::MaybeUninit::::uninit(); + unsafe { + if GetFileInformationByName( + file_name.as_ptr(), + file_information_class as _, + file_info_buffer.as_mut_ptr() as _, + file_info_buffer_size, + ) == 0 + { + Err(std::io::Error::last_os_error()) + } else { + Ok(file_info_buffer.assume_init()) + } + } + } + pub fn stat_basic_info_to_stat(info: &FILE_STAT_BASIC_INFORMATION) -> StatStruct { + use windows_sys::Win32::Storage::FileSystem; + use windows_sys::Win32::System::Ioctl; + + const S_IFMT: u16 = self::S_IFMT as _; + const S_IFDIR: u16 = self::S_IFDIR as _; + const S_IFCHR: u16 = self::S_IFCHR as _; + const S_IFIFO: u16 = self::S_IFIFO as _; + const S_IFLNK: u16 = self::S_IFLNK as _; + + let mut st_mode = attributes_to_mode(info.FileAttributes); + let st_size = info.EndOfFile as u64; + let st_birthtime = large_integer_to_time_t_nsec(info.CreationTime); + let st_ctime = large_integer_to_time_t_nsec(info.ChangeTime); + let st_mtime = large_integer_to_time_t_nsec(info.LastWriteTime); + let st_atime = large_integer_to_time_t_nsec(info.LastAccessTime); + let st_nlink = info.NumberOfLinks as _; + let st_dev = info.VolumeSerialNumber as u32; + // File systems with less than 128-bits zero pad into this field + let st_ino = info.FileId128; + // bpo-37834: Only actual symlinks set the S_IFLNK flag. But lstat() will + // open other name surrogate reparse points without traversing them. To + // detect/handle these, check st_file_attributes and st_reparse_tag. + let st_reparse_tag = info.ReparseTag; + if info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 + && info.ReparseTag == IO_REPARSE_TAG_SYMLINK + { + // set the bits that make this a symlink + st_mode = (st_mode & !S_IFMT) | S_IFLNK; + } + let st_file_attributes = info.FileAttributes; + match info.DeviceType { + FileSystem::FILE_DEVICE_DISK + | Ioctl::FILE_DEVICE_VIRTUAL_DISK + | Ioctl::FILE_DEVICE_DFS + | FileSystem::FILE_DEVICE_CD_ROM + | Ioctl::FILE_DEVICE_CONTROLLER + | Ioctl::FILE_DEVICE_DATALINK => {} + Ioctl::FILE_DEVICE_DISK_FILE_SYSTEM + | Ioctl::FILE_DEVICE_CD_ROM_FILE_SYSTEM + | Ioctl::FILE_DEVICE_NETWORK_FILE_SYSTEM => { + st_mode = (st_mode & !S_IFMT) | 0x6000; // _S_IFBLK + } + Ioctl::FILE_DEVICE_CONSOLE + | Ioctl::FILE_DEVICE_NULL + | Ioctl::FILE_DEVICE_KEYBOARD + | Ioctl::FILE_DEVICE_MODEM + | Ioctl::FILE_DEVICE_MOUSE + | Ioctl::FILE_DEVICE_PARALLEL_PORT + | Ioctl::FILE_DEVICE_PRINTER + | Ioctl::FILE_DEVICE_SCREEN + | Ioctl::FILE_DEVICE_SERIAL_PORT + | Ioctl::FILE_DEVICE_SOUND => { + st_mode = (st_mode & !S_IFMT) | S_IFCHR; + } + Ioctl::FILE_DEVICE_NAMED_PIPE => { + st_mode = (st_mode & !S_IFMT) | S_IFIFO; + } + _ => { + if info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0 { + st_mode = (st_mode & !S_IFMT) | S_IFDIR; + } + } + } + + StatStruct { + st_dev, + st_ino: st_ino[0], + st_mode, + st_nlink, + st_uid: 0, + st_gid: 0, + st_rdev: 0, + st_size, + st_atime: st_atime.0, + st_atime_nsec: st_atime.1, + st_mtime: st_mtime.0, + st_mtime_nsec: st_mtime.1, + st_ctime: st_ctime.0, + st_ctime_nsec: st_ctime.1, + st_birthtime: st_birthtime.0, + st_birthtime_nsec: st_birthtime.1, + st_file_attributes, + st_reparse_tag, + st_ino_high: st_ino[1], + } + } +} diff --git a/common/src/lib.rs b/common/src/lib.rs index ffd027a0e67..8a94b939b57 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -11,6 +11,8 @@ pub mod cmp; #[cfg(any(unix, windows, target_os = "wasi"))] pub mod crt_fd; pub mod encodings; +#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))] +pub mod fileutils; pub mod float_ops; pub mod hash; pub mod int; diff --git a/common/src/windows.rs b/common/src/windows.rs index e1f296c941d..4a922ce4354 100644 --- a/common/src/windows.rs +++ b/common/src/windows.rs @@ -5,7 +5,7 @@ use std::{ pub trait ToWideString { fn to_wide(&self) -> Vec; - fn to_wides_with_nul(&self) -> Vec; + fn to_wide_with_nul(&self) -> Vec; } impl ToWideString for T where @@ -14,7 +14,7 @@ where fn to_wide(&self) -> Vec { self.as_ref().encode_wide().collect() } - fn to_wides_with_nul(&self) -> Vec { + fn to_wide_with_nul(&self) -> Vec { self.as_ref().encode_wide().chain(Some(0)).collect() } } diff --git a/stdlib/src/select.rs b/stdlib/src/select.rs index 48705f4c9b7..1e624605f63 100644 --- a/stdlib/src/select.rs +++ b/stdlib/src/select.rs @@ -6,7 +6,7 @@ use std::{io, mem}; pub(crate) fn make_module(vm: &VirtualMachine) -> PyRef { #[cfg(windows)] - crate::vm::stdlib::nt::init_winsock(); + crate::vm::windows::init_winsock(); #[cfg(unix)] { diff --git a/stdlib/src/socket.rs b/stdlib/src/socket.rs index ad823201e18..5aea7be1170 100644 --- a/stdlib/src/socket.rs +++ b/stdlib/src/socket.rs @@ -4,7 +4,7 @@ pub(super) use _socket::{sock_select, timeout_error_msg, PySocket, SelectKind}; pub fn make_module(vm: &VirtualMachine) -> PyRef { #[cfg(windows)] - crate::vm::stdlib::nt::init_winsock(); + crate::vm::windows::init_winsock(); _socket::make_module(vm) } diff --git a/vm/Cargo.toml b/vm/Cargo.toml index f4a000b8c4c..02586f89c5a 100644 --- a/vm/Cargo.toml +++ b/vm/Cargo.toml @@ -102,13 +102,11 @@ which = "4.2.5" num_cpus = "1.13.1" [target.'cfg(windows)'.dependencies] +junction = { workspace = true } schannel = { workspace = true } widestring = { workspace = true } winreg = "0.10.1" -[target.'cfg(windows)'.dependencies.junction] -workspace = true - [target.'cfg(windows)'.dependencies.windows] version = "0.52.0" features = [ @@ -128,6 +126,7 @@ features = [ "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_Environment", + "Win32_System_Ioctl", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", diff --git a/vm/src/fileutils.rs b/vm/src/fileutils.rs deleted file mode 100644 index b91fb674de6..00000000000 --- a/vm/src/fileutils.rs +++ /dev/null @@ -1,243 +0,0 @@ -// Python/fileutils.c in CPython -#[cfg(not(windows))] -pub use libc::stat as StatStruct; - -#[cfg(windows)] -pub use windows::{fstat, StatStruct}; - -#[cfg(not(windows))] -pub fn fstat(fd: libc::c_int) -> std::io::Result { - let mut stat = std::mem::MaybeUninit::uninit(); - unsafe { - let ret = libc::fstat(fd, stat.as_mut_ptr()); - if ret == -1 { - Err(crate::common::os::last_os_error()) - } else { - Ok(stat.assume_init()) - } - } -} - -#[cfg(windows)] -mod windows { - use crate::common::suppress_iph; - use libc::{S_IFCHR, S_IFMT}; - use windows_sys::Win32::Foundation::SetLastError; - use windows_sys::Win32::Foundation::FILETIME; - use windows_sys::Win32::Foundation::{ERROR_INVALID_HANDLE, HANDLE, INVALID_HANDLE_VALUE}; - use windows_sys::Win32::Storage::FileSystem::{ - FileBasicInfo, FileIdInfo, GetFileInformationByHandle, GetFileInformationByHandleEx, - GetFileType, FILE_TYPE_CHAR, FILE_TYPE_DISK, FILE_TYPE_PIPE, FILE_TYPE_UNKNOWN, - }; - use windows_sys::Win32::Storage::FileSystem::{ - BY_HANDLE_FILE_INFORMATION, FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_READONLY, - FILE_ATTRIBUTE_REPARSE_POINT, FILE_BASIC_INFO, FILE_ID_INFO, - }; - - pub const S_IFIFO: libc::c_int = 0o010000; - pub const S_IFLNK: libc::c_int = 0o120000; - - pub const SECS_BETWEEN_EPOCHS: i64 = 11644473600; // Seconds between 1.1.1601 and 1.1.1970 - - #[derive(Default)] - pub struct StatStruct { - pub st_dev: libc::c_ulong, - pub st_ino: u64, - pub st_mode: libc::c_ushort, - pub st_nlink: i32, - pub st_uid: i32, - pub st_gid: i32, - pub st_rdev: libc::c_ulong, - pub st_size: u64, - pub st_atime: libc::time_t, - pub st_atime_nsec: i32, - pub st_mtime: libc::time_t, - pub st_mtime_nsec: i32, - pub st_ctime: libc::time_t, - pub st_ctime_nsec: i32, - pub st_birthtime: libc::time_t, - pub st_birthtime_nsec: i32, - pub st_file_attributes: libc::c_ulong, - pub st_reparse_tag: u32, - pub st_ino_high: u64, - } - - extern "C" { - fn _get_osfhandle(fd: i32) -> libc::intptr_t; - } - - fn get_osfhandle(fd: i32) -> std::io::Result { - let ret = unsafe { suppress_iph!(_get_osfhandle(fd)) }; - if ret as HANDLE == INVALID_HANDLE_VALUE { - Err(crate::common::os::last_os_error()) - } else { - Ok(ret) - } - } - - // _Py_fstat_noraise in cpython - pub fn fstat(fd: libc::c_int) -> std::io::Result { - let h = get_osfhandle(fd); - if h.is_err() { - unsafe { SetLastError(ERROR_INVALID_HANDLE) }; - } - let h = h?; - // reset stat? - - let file_type = unsafe { GetFileType(h) }; - if file_type == FILE_TYPE_UNKNOWN { - return Err(std::io::Error::last_os_error()); - } - if file_type != FILE_TYPE_DISK { - let st_mode = if file_type == FILE_TYPE_CHAR { - S_IFCHR - } else if file_type == FILE_TYPE_PIPE { - S_IFIFO - } else { - 0 - } as u16; - return Ok(StatStruct { - st_mode, - ..Default::default() - }); - } - - let mut info = unsafe { std::mem::zeroed() }; - let mut basic_info: FILE_BASIC_INFO = unsafe { std::mem::zeroed() }; - let mut id_info: FILE_ID_INFO = unsafe { std::mem::zeroed() }; - - if unsafe { GetFileInformationByHandle(h, &mut info) } == 0 - || unsafe { - GetFileInformationByHandleEx( - h, - FileBasicInfo, - &mut basic_info as *mut _ as *mut _, - std::mem::size_of_val(&basic_info) as u32, - ) - } == 0 - { - return Err(std::io::Error::last_os_error()); - } - - let p_id_info = if unsafe { - GetFileInformationByHandleEx( - h, - FileIdInfo, - &mut id_info as *mut _ as *mut _, - std::mem::size_of_val(&id_info) as u32, - ) - } == 0 - { - None - } else { - Some(&id_info) - }; - - Ok(attribute_data_to_stat( - &info, - 0, - Some(&basic_info), - p_id_info, - )) - } - - fn i64_to_time_t_nsec(input: i64) -> (libc::time_t, libc::c_int) { - let nsec_out = (input % 10_000_000) * 100; // FILETIME is in units of 100 nsec. - let time_out = ((input / 10_000_000) - SECS_BETWEEN_EPOCHS) as libc::time_t; - (time_out, nsec_out as _) - } - - fn file_time_to_time_t_nsec(in_ptr: &FILETIME) -> (libc::time_t, libc::c_int) { - let in_val: i64 = unsafe { std::mem::transmute_copy(in_ptr) }; - let nsec_out = (in_val % 10_000_000) * 100; // FILETIME is in units of 100 nsec. - let time_out = (in_val / 10_000_000) - SECS_BETWEEN_EPOCHS; - (time_out, nsec_out as _) - } - - fn attribute_data_to_stat( - info: &BY_HANDLE_FILE_INFORMATION, - reparse_tag: u32, - basic_info: Option<&FILE_BASIC_INFO>, - id_info: Option<&FILE_ID_INFO>, - ) -> StatStruct { - use windows_sys::Win32::System::SystemServices::IO_REPARSE_TAG_SYMLINK; - - let mut st_mode: u16 = attributes_to_mode(info.dwFileAttributes) as _; - let st_size = ((info.nFileSizeHigh as u64) << 32) + info.nFileSizeLow as u64; - let st_dev: libc::c_ulong = if let Some(id_info) = id_info { - id_info.VolumeSerialNumber as _ - } else { - info.dwVolumeSerialNumber - }; - let st_rdev = 0; - - let (st_birth_time, st_ctime, st_mtime, st_atime) = if let Some(basic_info) = basic_info { - ( - i64_to_time_t_nsec(basic_info.CreationTime), - i64_to_time_t_nsec(basic_info.ChangeTime), - i64_to_time_t_nsec(basic_info.LastWriteTime), - i64_to_time_t_nsec(basic_info.LastAccessTime), - ) - } else { - ( - file_time_to_time_t_nsec(&info.ftCreationTime), - (0, 0), - file_time_to_time_t_nsec(&info.ftLastWriteTime), - file_time_to_time_t_nsec(&info.ftLastAccessTime), - ) - }; - let st_nlink = info.nNumberOfLinks as i32; - - let st_ino = if let Some(id_info) = id_info { - let file_id: [u64; 2] = unsafe { std::mem::transmute_copy(&id_info.FileId) }; - file_id - } else { - let ino = ((info.nFileIndexHigh as u64) << 32) + info.nFileIndexLow as u64; - [ino, 0] - }; - - if info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 - && reparse_tag == IO_REPARSE_TAG_SYMLINK - { - st_mode = (st_mode & !(S_IFMT as u16)) | (S_IFLNK as u16); - } - let st_file_attributes = info.dwFileAttributes; - - StatStruct { - st_dev, - st_ino: st_ino[0], - st_mode, - st_nlink, - st_uid: 0, - st_gid: 0, - st_rdev, - st_size, - st_atime: st_atime.0, - st_atime_nsec: st_atime.1, - st_mtime: st_mtime.0, - st_mtime_nsec: st_mtime.1, - st_ctime: st_ctime.0, - st_ctime_nsec: st_ctime.1, - st_birthtime: st_birth_time.0, - st_birthtime_nsec: st_birth_time.1, - st_file_attributes, - st_reparse_tag: reparse_tag, - st_ino_high: st_ino[1], - } - } - - fn attributes_to_mode(attr: u32) -> libc::c_int { - let mut m = 0; - if attr & FILE_ATTRIBUTE_DIRECTORY != 0 { - m |= libc::S_IFDIR | 0o111; // IFEXEC for user,group,other - } else { - m |= libc::S_IFREG; - } - if attr & FILE_ATTRIBUTE_READONLY != 0 { - m |= 0o444; - } else { - m |= 0o666; - } - m - } -} diff --git a/vm/src/lib.rs b/vm/src/lib.rs index b7f79ee4851..3c99e382f6d 100644 --- a/vm/src/lib.rs +++ b/vm/src/lib.rs @@ -53,8 +53,6 @@ mod dictdatatype; #[cfg(feature = "rustpython-compiler")] pub mod eval; pub mod exceptions; -#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))] -mod fileutils; pub mod format; pub mod frame; pub mod function; diff --git a/vm/src/stdlib/io.rs b/vm/src/stdlib/io.rs index 4add9b3bd4c..11d2ab8a5bb 100644 --- a/vm/src/stdlib/io.rs +++ b/vm/src/stdlib/io.rs @@ -3930,7 +3930,7 @@ mod fileio { // TODO: _Py_set_inheritable - let fd_fstat = crate::fileutils::fstat(fd); + let fd_fstat = crate::common::fileutils::fstat(fd); #[cfg(windows)] { diff --git a/vm/src/stdlib/nt.rs b/vm/src/stdlib/nt.rs index 1b2dd55e913..4508162f125 100644 --- a/vm/src/stdlib/nt.rs +++ b/vm/src/stdlib/nt.rs @@ -373,7 +373,7 @@ pub(crate) mod module { } } - pub fn raw_set_handle_inheritable(handle: intptr_t, inheritable: bool) -> io::Result<()> { + pub fn raw_set_handle_inheritable(handle: intptr_t, inheritable: bool) -> std::io::Result<()> { let flags = if inheritable { Foundation::HANDLE_FLAG_INHERIT } else { @@ -420,11 +420,3 @@ pub(crate) mod module { Vec::new() } } - -pub fn init_winsock() { - static WSA_INIT: parking_lot::Once = parking_lot::Once::new(); - WSA_INIT.call_once(|| unsafe { - let mut wsa_data = std::mem::MaybeUninit::uninit(); - let _ = windows_sys::Win32::Networking::WinSock::WSAStartup(0x0101, wsa_data.as_mut_ptr()); - }) -} diff --git a/vm/src/stdlib/os.rs b/vm/src/stdlib/os.rs index fd7ef0b8c41..7b054c7d384 100644 --- a/vm/src/stdlib/os.rs +++ b/vm/src/stdlib/os.rs @@ -7,7 +7,7 @@ use crate::{ }; use std::{ffi, fs, io, path::Path}; -pub(super) fn fs_metadata>( +pub(crate) fn fs_metadata>( path: P, follow_symlink: bool, ) -> io::Result { @@ -123,11 +123,13 @@ pub(super) mod _os { builtins::{ PyBytesRef, PyGenericAlias, PyIntRef, PyStrRef, PyTuple, PyTupleRef, PyTypeRef, }, - common::crt_fd::{Fd, Offset}, - common::lock::{OnceCell, PyRwLock}, - common::suppress_iph, + common::{ + crt_fd::{Fd, Offset}, + fileutils::StatStruct, + lock::{OnceCell, PyRwLock}, + suppress_iph, + }, convert::{IntoPyException, ToPyObject}, - fileutils::StatStruct, function::{ArgBytesLike, Either, FsPath, FuncArgs, OptionalArg}, ospath::{IOErrorBuilder, OsPath, OsPathOrFd, OutputMode}, protocol::PyIterReturn, @@ -810,50 +812,6 @@ pub(super) mod _os { } } - #[cfg(windows)] - fn meta_to_stat(meta: &fs::Metadata) -> io::Result { - let st_mode = { - // Based on CPython fileutils.c' attributes_to_mode - let mut m = 0; - if meta.is_dir() { - m |= libc::S_IFDIR | 0o111; /* IFEXEC for user,group,other */ - } else { - m |= libc::S_IFREG; - } - if meta.permissions().readonly() { - m |= 0o444; - } else { - m |= 0o666; - } - m as _ - }; - let (atime, mtime, ctime) = (meta.accessed()?, meta.modified()?, meta.created()?); - let sec = |systime: SystemTime| match systime.duration_since(SystemTime::UNIX_EPOCH) { - Ok(d) => d.as_secs() as libc::time_t, - Err(e) => -(e.duration().as_secs() as libc::time_t), - }; - let nsec = |systime: SystemTime| match systime.duration_since(SystemTime::UNIX_EPOCH) { - Ok(d) => d.subsec_nanos() as i32, - Err(e) => -(e.duration().subsec_nanos() as i32), - }; - Ok(StatStruct { - st_dev: 0, - st_ino: 0, - st_mode, - st_nlink: 0, - st_uid: 0, - st_gid: 0, - st_size: meta.len(), - st_atime: sec(atime), - st_mtime: sec(mtime), - st_ctime: sec(ctime), - st_atime_nsec: nsec(atime), - st_mtime_nsec: nsec(mtime), - st_ctime_nsec: nsec(ctime), - ..Default::default() - }) - } - #[cfg(windows)] fn stat_inner( file: OsPathOrFd, @@ -863,8 +821,8 @@ pub(super) mod _os { // TODO: replicate CPython's win32_xstat let [] = dir_fd.0; match file { - OsPathOrFd::Path(path) => meta_to_stat(&super::fs_metadata(path, follow_symlinks.0)?), - OsPathOrFd::Fd(fd) => crate::fileutils::fstat(fd), + OsPathOrFd::Path(path) => crate::windows::win32_xstat(&path.path, follow_symlinks.0), + OsPathOrFd::Fd(fd) => crate::common::fileutils::fstat(fd), } .map(Some) } diff --git a/vm/src/stdlib/signal.rs b/vm/src/stdlib/signal.rs index 4a698a76336..cc332b08be7 100644 --- a/vm/src/stdlib/signal.rs +++ b/vm/src/stdlib/signal.rs @@ -201,7 +201,7 @@ pub(crate) mod _signal { let is_socket = if fd != INVALID_WAKEUP { use windows_sys::Win32::Networking::WinSock; - crate::stdlib::nt::init_winsock(); + crate::windows::init_winsock(); let mut res = 0i32; let mut res_size = std::mem::size_of::() as i32; let res = unsafe { diff --git a/vm/src/stdlib/winapi.rs b/vm/src/stdlib/winapi.rs index 429ce479dd9..8570b3d8a55 100644 --- a/vm/src/stdlib/winapi.rs +++ b/vm/src/stdlib/winapi.rs @@ -248,7 +248,7 @@ mod _winapi { #[pyfunction] fn NeedCurrentDirectoryForExePath(exe_name: PyStrRef) -> bool { - let exe_name = exe_name.as_str().to_wides_with_nul(); + let exe_name = exe_name.as_str().to_wide_with_nul(); let return_value = unsafe { windows_sys::Win32::System::Environment::NeedCurrentDirectoryForExePathW( exe_name.as_ptr(), @@ -419,7 +419,7 @@ mod _winapi { // TODO: ctypes.LibraryLoader.LoadLibrary #[allow(dead_code)] fn LoadLibrary(path: PyStrRef, vm: &VirtualMachine) -> PyResult { - let path = path.as_str().to_wides_with_nul(); + let path = path.as_str().to_wide_with_nul(); let handle = unsafe { windows::Win32::System::LibraryLoader::LoadLibraryW(PCWSTR::from_raw(path.as_ptr())) .unwrap() diff --git a/vm/src/windows.rs b/vm/src/windows.rs index e749241c07f..0308ce32ce9 100644 --- a/vm/src/windows.rs +++ b/vm/src/windows.rs @@ -1,8 +1,13 @@ +use crate::common::fileutils::{ + windows::{get_file_information_by_name, FILE_INFO_BY_NAME_CLASS}, + StatStruct, +}; use crate::{ convert::{ToPyObject, ToPyResult}, stdlib::os::errno_err, PyObjectRef, PyResult, TryFromObject, VirtualMachine, }; +use std::{ffi::OsStr, time::SystemTime}; use windows::Win32::Foundation::HANDLE; use windows_sys::Win32::Foundation::{BOOL, HANDLE as RAW_HANDLE, INVALID_HANDLE_VALUE}; @@ -66,3 +71,133 @@ impl ToPyObject for HANDLE { (self.0 as HandleInt).to_pyobject(vm) } } + +pub fn init_winsock() { + static WSA_INIT: parking_lot::Once = parking_lot::Once::new(); + WSA_INIT.call_once(|| unsafe { + let mut wsa_data = std::mem::MaybeUninit::uninit(); + let _ = windows_sys::Win32::Networking::WinSock::WSAStartup(0x0101, wsa_data.as_mut_ptr()); + }) +} + +// win32_xstat in cpython +pub fn win32_xstat(path: &OsStr, traverse: bool) -> std::io::Result { + let mut result = win32_xstat_impl(path, traverse)?; + // ctime is only deprecated from 3.12, so we copy birthtime across + result.st_ctime = result.st_birthtime; + result.st_ctime_nsec = result.st_birthtime_nsec; + Ok(result) +} + +fn is_reparse_tag_name_surrogate(tag: u32) -> bool { + (tag & 0x20000000) > 0 +} + +fn win32_xstat_impl(path: &OsStr, traverse: bool) -> std::io::Result { + use windows_sys::Win32::{Foundation, Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT}; + + let stat_info = + get_file_information_by_name(path, FILE_INFO_BY_NAME_CLASS::FileStatBasicByNameInfo); + match stat_info { + Ok(stat_info) => { + if (stat_info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT == 0) + || (!traverse && is_reparse_tag_name_surrogate(stat_info.ReparseTag)) + { + let mut result = + crate::common::fileutils::windows::stat_basic_info_to_stat(&stat_info); + result.update_st_mode_from_path(path, stat_info.FileAttributes); + return Ok(result); + } + } + Err(e) => { + if let Some(errno) = e.raw_os_error() { + if matches!( + errno as u32, + Foundation::ERROR_FILE_NOT_FOUND + | Foundation::ERROR_PATH_NOT_FOUND + | Foundation::ERROR_NOT_READY + | Foundation::ERROR_BAD_NET_NAME + ) { + return Err(e); + } + } + } + } + + // TODO: check if win32_xstat_slow_impl(&path, result, traverse) is required + meta_to_stat( + &crate::stdlib::os::fs_metadata(path, traverse)?, + file_id(path)?, + ) +} + +// Ported from zed: https://github.com/zed-industries/zed/blob/v0.131.6/crates/fs/src/fs.rs#L1532-L1562 +// can we get file id not open the file twice? +// https://github.com/rust-lang/rust/issues/63010 +fn file_id(path: &OsStr) -> std::io::Result { + use std::os::windows::{fs::OpenOptionsExt, io::AsRawHandle}; + use windows_sys::Win32::{ + Foundation::HANDLE, + Storage::FileSystem::{ + GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION, FILE_FLAG_BACKUP_SEMANTICS, + }, + }; + + let file = std::fs::OpenOptions::new() + .read(true) + .custom_flags(FILE_FLAG_BACKUP_SEMANTICS) + .open(path)?; + + let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() }; + // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle + // This function supports Windows XP+ + let ret = unsafe { GetFileInformationByHandle(file.as_raw_handle() as HANDLE, &mut info) }; + if ret == 0 { + return Err(std::io::Error::last_os_error()); + }; + + Ok(((info.nFileIndexHigh as u64) << 32) | (info.nFileIndexLow as u64)) +} + +fn meta_to_stat(meta: &std::fs::Metadata, file_id: u64) -> std::io::Result { + let st_mode = { + // Based on CPython fileutils.c' attributes_to_mode + let mut m = 0; + if meta.is_dir() { + m |= libc::S_IFDIR | 0o111; /* IFEXEC for user,group,other */ + } else { + m |= libc::S_IFREG; + } + if meta.permissions().readonly() { + m |= 0o444; + } else { + m |= 0o666; + } + m as _ + }; + let (atime, mtime, ctime) = (meta.accessed()?, meta.modified()?, meta.created()?); + let sec = |systime: SystemTime| match systime.duration_since(SystemTime::UNIX_EPOCH) { + Ok(d) => d.as_secs() as libc::time_t, + Err(e) => -(e.duration().as_secs() as libc::time_t), + }; + let nsec = |systime: SystemTime| match systime.duration_since(SystemTime::UNIX_EPOCH) { + Ok(d) => d.subsec_nanos() as i32, + Err(e) => -(e.duration().subsec_nanos() as i32), + }; + Ok(StatStruct { + st_dev: 0, + st_ino: file_id, + st_mode, + st_nlink: 0, + st_uid: 0, + st_gid: 0, + st_size: meta.len(), + st_atime: sec(atime), + st_mtime: sec(mtime), + st_birthtime: sec(ctime), + st_atime_nsec: nsec(atime), + st_mtime_nsec: nsec(mtime), + st_birthtime_nsec: nsec(ctime), + ..Default::default() + }) +}