Skip to main content

rustpython_vm/stdlib/
os.rs

1// spell-checker:disable
2#![allow(unreachable_pub)]
3
4use crate::{
5    AsObject, Py, PyObject, PyObjectRef, PyPayload, PyResult, TryFromObject, VirtualMachine,
6    builtins::{PyModule, PySet},
7    convert::{IntoPyException, ToPyException, ToPyObject},
8    function::{ArgumentError, FromArgs, FuncArgs, Param},
9    host_env::{crt_fd, posix::RawMode},
10    ospath::OsPath,
11};
12use core::marker::PhantomData;
13use std::{io, path::Path};
14
15pub(crate) fn fs_metadata<P: AsRef<Path>>(
16    path: P,
17    follow_symlink: bool,
18) -> io::Result<std::fs::Metadata> {
19    if follow_symlink {
20        crate::host_env::fs::metadata(path.as_ref())
21    } else {
22        crate::host_env::fs::symlink_metadata(path.as_ref())
23    }
24}
25
26#[allow(dead_code)]
27#[derive(FromArgs, Default)]
28pub struct TargetIsDirectory {
29    #[pyarg(any, default)]
30    pub(crate) target_is_directory: bool,
31}
32
33use crate::host_env::os::AT_FDCWD;
34
35const DEFAULT_DIR_FD: crt_fd::Borrowed<'static> = unsafe { crt_fd::Borrowed::borrow_raw(AT_FDCWD) };
36
37pub trait DirFdKeyword: Clone + Copy + Eq + PartialEq {
38    const NAME: &'static str;
39    const PARAMS: &'static [Param] = &[Param::keyword_only(
40        Self::NAME,
41        Some(crate::function::DefaultRepr::None),
42    )];
43}
44
45#[derive(Clone, Copy, Eq, PartialEq)]
46pub struct DefaultDirFd;
47impl DirFdKeyword for DefaultDirFd {
48    const NAME: &'static str = "dir_fd";
49}
50
51#[derive(Clone, Copy, Eq, PartialEq)]
52pub struct SrcDirFd;
53impl DirFdKeyword for SrcDirFd {
54    const NAME: &'static str = "src_dir_fd";
55}
56
57#[derive(Clone, Copy, Eq, PartialEq)]
58pub struct DstDirFd;
59impl DirFdKeyword for DstDirFd {
60    const NAME: &'static str = "dst_dir_fd";
61}
62
63// XXX: AVAILABLE should be a bool, but we can't yet have it as a bool and just cast it to usize
64#[derive(Clone, Copy, Eq, PartialEq)]
65pub struct DirFd<'fd, const AVAILABLE: usize, KW: DirFdKeyword = DefaultDirFd>(
66    pub(crate) [crt_fd::Borrowed<'fd>; AVAILABLE],
67    PhantomData<KW>,
68);
69
70impl<const AVAILABLE: usize, KW: DirFdKeyword> Default for DirFd<'_, AVAILABLE, KW> {
71    fn default() -> Self {
72        Self([DEFAULT_DIR_FD; AVAILABLE], PhantomData)
73    }
74}
75
76// not used on all platforms
77#[allow(unused)]
78impl<'fd, KW: DirFdKeyword> DirFd<'fd, 1, KW> {
79    #[inline(always)]
80    pub(crate) fn get_opt(self) -> Option<crt_fd::Borrowed<'fd>> {
81        let [fd] = self.0;
82        (fd != DEFAULT_DIR_FD).then_some(fd)
83    }
84
85    #[inline]
86    pub(crate) fn raw_opt(self) -> Option<i32> {
87        self.get_opt().map(|fd| fd.as_raw())
88    }
89
90    #[inline(always)]
91    pub(crate) const fn get(self) -> crt_fd::Borrowed<'fd> {
92        let [fd] = self.0;
93        fd
94    }
95}
96
97impl<const AVAILABLE: usize, KW: DirFdKeyword> FromArgs for DirFd<'_, AVAILABLE, KW> {
98    const PARAMS: Option<&'static [Param]> = Some(KW::PARAMS);
99
100    fn from_args(vm: &VirtualMachine, args: &mut FuncArgs) -> Result<Self, ArgumentError> {
101        let fd = match args.take_keyword(KW::NAME) {
102            Some(o) if vm.is_none(&o) => Ok(DEFAULT_DIR_FD),
103            None => Ok(DEFAULT_DIR_FD),
104            Some(o) => {
105                warn_if_bool_fd(&o, vm).map_err(Into::<ArgumentError>::into)?;
106                let fd = o.try_index_opt(vm).unwrap_or_else(|| {
107                    Err(vm.new_type_error(format!(
108                        "argument should be integer or None, not {}",
109                        o.class().name()
110                    )))
111                })?;
112                let fd = fd.try_to_primitive(vm)?;
113                unsafe { crt_fd::Borrowed::try_borrow_raw(fd) }
114            }
115        };
116        if AVAILABLE == 0 && fd.as_ref().is_ok_and(|&fd| fd != DEFAULT_DIR_FD) {
117            return Err(vm
118                .new_not_implemented_error("dir_fd unavailable on this platform")
119                .into());
120        }
121        let fd = fd.map_err(|e| e.to_pyexception(vm))?;
122        Ok(Self([fd; AVAILABLE], PhantomData))
123    }
124}
125
126#[derive(FromArgs)]
127pub(crate) struct SymlinkArgs<'fd> {
128    pub src: OsPath,
129    pub dst: OsPath,
130    #[cfg_attr(any(unix, target_os = "wasi"), expect(unused))]
131    #[pyarg(flatten)]
132    pub target_is_directory: TargetIsDirectory,
133    #[cfg_attr(not(unix), expect(unused))]
134    #[pyarg(flatten)]
135    pub dir_fd: DirFd<'fd, { _os::SYMLINK_DIR_FD as usize }>,
136}
137
138#[derive(FromArgs)]
139pub(super) struct FollowSymlinks(
140    #[pyarg(named, name = "follow_symlinks", default = true)] pub bool,
141);
142
143#[cfg(not(windows))]
144fn bytes_as_os_str<'a>(b: &'a [u8], vm: &VirtualMachine) -> PyResult<&'a std::ffi::OsStr> {
145    rustpython_host_env::os::bytes_as_os_str(b).map_err(|e| {
146        vm.new_unicode_decode_error(
147            vm.ctx.new_str("utf-8"),
148            vm.ctx.new_bytes(b.to_vec()),
149            e.valid_up_to(),
150            e.error_len().map_or(b.len(), |n| e.valid_up_to() + n),
151            vm.ctx.new_str("can't decode path for utf-8"),
152        )
153    })
154}
155
156pub(crate) fn warn_if_bool_fd(obj: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
157    use crate::class::StaticType;
158    if obj
159        .class()
160        .is(crate::builtins::bool_::PyBool::static_type())
161    {
162        crate::stdlib::_warnings::warn(
163            vm.ctx.exceptions.runtime_warning,
164            "bool is used as a file descriptor".to_owned(),
165            1,
166            vm,
167        )?;
168    }
169    Ok(())
170}
171
172impl TryFromObject for crt_fd::Owned {
173    fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
174        warn_if_bool_fd(&obj, vm)?;
175        let fd = crt_fd::Raw::try_from_object(vm, obj)?;
176        unsafe { Self::try_from_raw(fd) }.map_err(|e| e.into_pyexception(vm))
177    }
178}
179
180impl TryFromObject for crt_fd::Borrowed<'_> {
181    fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
182        warn_if_bool_fd(&obj, vm)?;
183        let fd = crt_fd::Raw::try_from_object(vm, obj)?;
184        unsafe { crt_fd::Borrowed::try_borrow_raw(fd) }.map_err(|e| e.into_pyexception(vm))
185    }
186}
187
188impl ToPyObject for crt_fd::Owned {
189    fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef {
190        self.into_raw().to_pyobject(vm)
191    }
192}
193
194impl ToPyObject for crt_fd::Borrowed<'_> {
195    fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef {
196        self.as_raw().to_pyobject(vm)
197    }
198}
199
200#[pymodule(sub, name = "posix")]
201pub(super) mod _os {
202    use super::{DirFd, DstDirFd, FollowSymlinks, RawMode, SrcDirFd, SupportFunc};
203    #[cfg(not(windows))]
204    use crate::exceptions;
205    use crate::host_env::fileutils::StatStruct;
206    #[cfg(any(unix, windows))]
207    use crate::utils::ToCString;
208    use crate::{
209        AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject,
210        builtins::{
211            PyBytesRef, PyGenericAlias, PyIntRef, PyStrRef, PyTuple, PyTupleRef, PyTypeRef,
212        },
213        class::PyClassDef,
214        common::lock::{OnceCell, PyRwLock},
215        convert::{IntoPyException, ToPyObject},
216        exceptions::{OSErrorBuilder, ToOSErrorBuilder},
217        function::{ArgBytesLike, ArgMemoryBuffer, FsPath, FuncArgs},
218        host_env::crt_fd,
219        ospath::{OsPath, OsPathOrFd, OutputMode, PathConverter},
220        protocol::PyIterReturn,
221        recursion::ReprGuard,
222        types::{
223            Destructor, IterNext, Iterable, PyStructSequence, PyStructSequenceData, Representable,
224            SelfIter,
225        },
226        vm::VirtualMachine,
227    };
228    #[cfg(not(windows))]
229    use core::marker::PhantomData;
230    use core::{hint::cold_path, time::Duration};
231    use crossbeam_utils::atomic::AtomicCell;
232    use rustpython_common::wtf8::Wtf8Buf;
233    #[cfg(windows)]
234    use rustpython_host_env::{nt as host_nt, windows::ToWideString};
235
236    #[cfg(all(any(unix, target_os = "wasi"), not(target_os = "redox")))]
237    use rustpython_host_env::posix as host_posix;
238    use std::{fs, io, path::PathBuf, time::SystemTime};
239
240    const OPEN_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox")));
241    pub(crate) const MKDIR_DIR_FD: bool = cfg!(any(unix, target_os = "wasi"));
242    const STAT_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox")));
243    const UTIME_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox")));
244    pub(crate) const SYMLINK_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox")));
245    pub(crate) const UNLINK_DIR_FD: bool = cfg!(not(windows));
246    const LINK_DIR_FD: bool = cfg!(any(unix, target_os = "wasi"));
247    const RENAME_DIR_FD: bool = cfg!(any(unix, target_os = "wasi"));
248    const RMDIR_DIR_FD: bool = cfg!(not(windows));
249    const SCANDIR_FD: bool = cfg!(all(unix, not(target_os = "redox")));
250
251    #[pyattr]
252    use crate::host_env::os::{O_APPEND, O_CREAT, O_EXCL, O_RDONLY, O_RDWR, O_TRUNC, O_WRONLY};
253
254    #[pyattr]
255    pub(crate) use crate::host_env::os::{F_OK, R_OK, W_OK, X_OK};
256
257    // ST_RDONLY and ST_NOSUID flags for statvfs
258    #[cfg(all(unix, not(target_os = "redox")))]
259    #[pyattr]
260    use crate::host_env::os::{ST_NOSUID, ST_RDONLY};
261
262    #[derive(FromArgs)]
263    struct CloseArgs {
264        #[pyarg(any)]
265        fd: crt_fd::Owned,
266    }
267
268    #[pyfunction]
269    fn close(fd: CloseArgs) -> io::Result<()> {
270        crt_fd::close(fd.fd)
271    }
272
273    #[pyfunction]
274    fn closerange(fd_low: i32, fd_high: i32) {
275        for fileno in fd_low..fd_high {
276            if let Ok(fd) = unsafe { crt_fd::Owned::try_from_raw(fileno) } {
277                drop(fd);
278            }
279        }
280    }
281
282    #[cfg(any(unix, windows, target_os = "wasi"))]
283    #[derive(FromArgs)]
284    struct OpenArgs<'fd> {
285        path: OsPath,
286        flags: i32,
287        #[pyarg(any, default = 0o777)]
288        mode: i32,
289        #[pyarg(flatten)]
290        dir_fd: DirFd<'fd, { OPEN_DIR_FD as usize }>,
291    }
292
293    #[pyfunction]
294    fn open(args: OpenArgs<'_>, vm: &VirtualMachine) -> PyResult<crt_fd::Owned> {
295        os_open(args.path, args.flags, args.mode, args.dir_fd, vm)
296    }
297
298    #[cfg(any(unix, windows, target_os = "wasi"))]
299    pub(crate) fn os_open(
300        name: OsPath,
301        flags: i32,
302        mode: i32,
303        dir_fd: DirFd<'_, { OPEN_DIR_FD as usize }>,
304        vm: &VirtualMachine,
305    ) -> PyResult<crt_fd::Owned> {
306        #[cfg(windows)]
307        let fd = {
308            let [] = dir_fd.0;
309            let name = name.to_wide_cstring(vm)?;
310            let flags = flags | crate::host_env::os::O_NOINHERIT;
311            crt_fd::wopen(&name, flags, mode)
312        };
313        #[cfg(not(windows))]
314        let fd = {
315            let name = name.clone().into_cstring(vm)?;
316            #[cfg(not(target_os = "wasi"))]
317            let flags = flags | crate::host_env::os::O_CLOEXEC;
318            #[cfg(not(target_os = "redox"))]
319            if let Some(dir_fd) = dir_fd.get_opt() {
320                crt_fd::openat(dir_fd, &name, flags, mode)
321            } else {
322                crt_fd::open(&name, flags, mode)
323            }
324            #[cfg(target_os = "redox")]
325            {
326                let [] = dir_fd.0;
327                crt_fd::open(&name, flags, mode)
328            }
329        };
330        fd.map_err(|err| OSErrorBuilder::with_filename_from_errno(&err, name, vm))
331    }
332
333    #[derive(FromArgs)]
334    struct FsyncArgs<'a> {
335        #[pyarg(any)]
336        fd: crt_fd::Borrowed<'a>,
337    }
338
339    #[pyfunction]
340    fn fsync(fd: FsyncArgs<'_>) -> io::Result<()> {
341        crt_fd::fsync(fd.fd)
342    }
343
344    #[pyfunction]
345    fn read(fd: crt_fd::Borrowed<'_>, length: usize, vm: &VirtualMachine) -> PyResult<PyBytesRef> {
346        let mut buffer = vm.new_zeroed_bytes(length)?;
347        loop {
348            match vm.allow_threads(|| crt_fd::read(fd, &mut buffer)) {
349                Ok(n) => {
350                    buffer.truncate(n);
351                    return Ok(vm.ctx.new_bytes(buffer));
352                }
353                Err(e) if e.raw_os_error() == Some(libc::EINTR) => {
354                    vm.check_signals()?;
355                    continue;
356                }
357                Err(e) => return Err(e.into_pyexception(vm)),
358            }
359        }
360    }
361
362    /// `read(2)` into `buf`, retrying on EINTR (PEP 475).
363    fn read_into_slice(
364        fd: crt_fd::Borrowed<'_>,
365        buf: &mut [u8],
366        vm: &VirtualMachine,
367    ) -> PyResult<usize> {
368        loop {
369            match vm.allow_threads(|| crt_fd::read(fd, buf)) {
370                Ok(n) => return Ok(n),
371                Err(e) if e.raw_os_error() == Some(libc::EINTR) => {
372                    vm.check_signals()?;
373                    continue;
374                }
375                Err(e) => return Err(e.into_pyexception(vm)),
376            }
377        }
378    }
379
380    #[pyfunction]
381    fn readinto(
382        fd: crt_fd::Borrowed<'_>,
383        buffer: ArgMemoryBuffer,
384        vm: &VirtualMachine,
385    ) -> PyResult<usize> {
386        if rustpython_host_env::io::reads_without_waiting(fd) {
387            // The read answers from the file itself, so it returns without
388            // waiting on anyone; write where the caller asked directly.
389            return buffer.with_ref(|buf| read_into_slice(fd, buf, vm));
390        }
391
392        // A pipe, socket or terminal answers only when the other end writes,
393        // which may be never. Holding the export for the whole call is what
394        // keeps the target from being resized meanwhile; but reaching its
395        // bytes takes a lock that every other thread touching the same object
396        // waits on, and a thread waiting on a lock never reaches a safepoint,
397        // so holding that one across the wait stops the world from being
398        // stopped at all. Read aside and take the lock for the copy.
399        let mut scratch = vm.new_zeroed_bytes(buffer.len())?;
400        let n = read_into_slice(fd, &mut scratch, vm)?;
401        buffer.borrow_buf_mut()[..n].copy_from_slice(&scratch[..n]);
402        Ok(n)
403    }
404
405    #[pyfunction]
406    fn write(fd: crt_fd::Borrowed<'_>, data: ArgBytesLike, vm: &VirtualMachine) -> PyResult<usize> {
407        let owned = data.with_ref(|b| b.to_vec());
408        loop {
409            match vm.allow_threads(|| crt_fd::write(fd, &owned)) {
410                Ok(n) => return Ok(n),
411                Err(e) if e.raw_os_error() == Some(libc::EINTR) => {
412                    vm.check_signals()?;
413                    continue;
414                }
415                Err(e) => return Err(e.into_pyexception(vm)),
416            }
417        }
418    }
419
420    #[derive(FromArgs)]
421    struct MkdirArgs<'a> {
422        #[pyarg(any)]
423        path: OsPath,
424        #[pyarg(any, default = 0o777)]
425        mode: RawMode,
426        #[pyarg(flatten)]
427        #[cfg_attr(not(any(unix, target_os = "wasi")), expect(unused))]
428        dir_fd: DirFd<'a, { MKDIR_DIR_FD as usize }>,
429    }
430
431    #[pyfunction]
432    fn mkdir(args: MkdirArgs<'_>, vm: &VirtualMachine) -> PyResult<()> {
433        #[cfg(any(unix, target_os = "wasi"))]
434        let dir_fd = args.dir_fd.get_opt();
435        #[cfg(not(any(unix, target_os = "wasi")))]
436        let dir_fd = None;
437
438        crate::host_env::posix::make_dir(dir_fd, &args.path.path, args.mode)
439            .map_err(|err| OSErrorBuilder::with_filename(&err, args.path, vm))
440    }
441
442    #[pyfunction]
443    fn mkdirs(path: PyStrRef, vm: &VirtualMachine) -> PyResult<()> {
444        let os_path = vm.fsencode(&path)?;
445        crate::host_env::fs::create_dir_all(&*os_path).map_err(|err| err.into_pyexception(vm))
446    }
447
448    #[derive(FromArgs)]
449    struct PathDirFd<'a, const N: usize> {
450        #[pyarg(any)]
451        path: OsPath,
452        #[pyarg(flatten)]
453        dir_fd: DirFd<'a, N>,
454    }
455
456    #[cfg(not(windows))]
457    #[pyfunction]
458    fn rmdir(args: PathDirFd<'_, { RMDIR_DIR_FD as usize }>, vm: &VirtualMachine) -> PyResult<()> {
459        crate::host_env::posix::remove_dir_at(args.dir_fd.get_opt(), &args.path.path)
460            .map_err(|err| OSErrorBuilder::with_filename(&err, args.path, vm))
461    }
462
463    #[cfg(windows)]
464    #[pyfunction]
465    fn rmdir(args: PathDirFd<'_, 0>, vm: &VirtualMachine) -> PyResult<()> {
466        let [] = args.dir_fd.0;
467        crate::host_env::fs::remove_dir(&args.path)
468            .map_err(|err| OSErrorBuilder::with_filename(&err, args.path, vm))
469    }
470
471    const LISTDIR_FD: bool = cfg!(all(unix, not(target_os = "redox")));
472
473    #[derive(FromArgs)]
474    struct ListDirArgs<'a> {
475        #[pyarg(any, optional)]
476        path: Option<OsPathOrFd<'a>>,
477    }
478
479    #[pyfunction]
480    fn listdir(args: ListDirArgs<'_>, vm: &VirtualMachine) -> PyResult<Vec<PyObjectRef>> {
481        let path = args
482            .path
483            .unwrap_or_else(|| OsPathOrFd::Path(OsPath::new_str(".")));
484        let list = match path {
485            OsPathOrFd::Path(path) => {
486                let dir_iter = match crate::host_env::fs::read_dir(&path) {
487                    Ok(iter) => iter,
488                    Err(err) => {
489                        return Err(OSErrorBuilder::with_filename(&err, path, vm));
490                    }
491                };
492                let mode = path.mode();
493                dir_iter
494                    .map(|entry| match entry {
495                        Ok(entry_path) => Ok(mode.process_path(entry_path.file_name(), vm)),
496                        Err(err) => Err(OSErrorBuilder::with_filename(&err, path.clone(), vm)),
497                    })
498                    .collect::<PyResult<_>>()?
499            }
500            OsPathOrFd::Fd(fno) => {
501                #[cfg(not(all(unix, not(target_os = "redox"))))]
502                {
503                    let _ = fno;
504                    return Err(
505                        vm.new_not_implemented_error("can't pass fd to listdir on this platform")
506                    );
507                }
508                #[cfg(all(unix, not(target_os = "redox")))]
509                {
510                    let mut dir = host_posix::FdDirStream::from_fd(fno.into())
511                        .map_err(|e| e.into_pyexception(vm))?;
512                    let mut list = Vec::new();
513                    while let Some(entry) = dir.next_entry().map_err(|e| e.into_pyexception(vm))? {
514                        list.push(
515                            OutputMode::String.process_path(
516                                rustpython_host_env::os::bytes_as_os_str(&entry.name)
517                                    .expect("unix dir entry names are arbitrary bytes"),
518                                vm,
519                            ),
520                        );
521                    }
522                    list
523                }
524            }
525        };
526        Ok(list)
527    }
528
529    #[cfg(not(windows))]
530    fn env_bytes_as_bytes_checked(
531        obj: &crate::function::Either<PyStrRef, PyBytesRef>,
532    ) -> Option<&[u8]> {
533        match obj {
534            crate::function::Either::A(s) if !s.contains_nuls() => Some(s.as_bytes()),
535            crate::function::Either::B(b) if !b.payload.contains_nuls() => Some(b.as_bytes()),
536            _ => {
537                cold_path();
538                None
539            }
540        }
541    }
542
543    /// Check if wide string length exceeds Windows environment variable limit.
544    #[cfg(windows)]
545    fn check_env_var_len(wide_len: usize, vm: &VirtualMachine) -> PyResult<()> {
546        use crate::host_env::windows::_MAX_ENV;
547        if wide_len > _MAX_ENV + 1 {
548            return Err(vm.new_value_error(format!(
549                "the environment variable is longer than {_MAX_ENV} characters",
550            )));
551        }
552        Ok(())
553    }
554
555    #[cfg(windows)]
556    #[pyfunction]
557    fn putenv(name: PyStrRef, value: PyStrRef, vm: &VirtualMachine) -> PyResult<()> {
558        let name_str = name.expect_str();
559        let value_str = value.expect_str();
560        // Search from index 1 because on Windows starting '=' is allowed for
561        // defining hidden environment variables.
562        if name_str.is_empty()
563            || name_str.get(1..).is_some_and(|s| s.contains('='))
564            || name.contains_nuls()
565            || value.contains_nuls()
566        {
567            cold_path();
568            return Err(vm.new_value_error("illegal environment variable name"));
569        }
570        let env_str = format!("{name_str}={value_str}");
571        // env_str is guaranteed nul-free by the checks above.
572        let wide = widestring::WideCString::from_str(&env_str)
573            .expect("env_str validated to contain no NUL");
574        check_env_var_len(wide.len() + 1, vm)?;
575
576        // Use _wputenv (not SetEnvironmentVariableW) to update CRT environ.
577        rustpython_host_env::nt::wputenv(&wide).map_err(|e| e.into_pyexception(vm))
578    }
579
580    #[cfg(not(windows))]
581    #[pyfunction]
582    fn putenv(
583        name: crate::function::Either<PyStrRef, PyBytesRef>,
584        value: crate::function::Either<PyStrRef, PyBytesRef>,
585        vm: &VirtualMachine,
586    ) -> PyResult<()> {
587        let (Some(name), Some(value)) = (
588            env_bytes_as_bytes_checked(&name),
589            env_bytes_as_bytes_checked(&value),
590        ) else {
591            cold_path();
592            return Err(exceptions::nul_byte_error(vm));
593        };
594        if name.is_empty() || name.contains(&b'=') {
595            return Err(vm.new_value_error("illegal environment variable name"));
596        }
597        let name = super::bytes_as_os_str(name, vm)?;
598        let value = super::bytes_as_os_str(value, vm)?;
599        // SAFETY: requirements forwarded from the caller
600        unsafe { crate::host_env::os::set_var(name, value) };
601        Ok(())
602    }
603
604    #[cfg(windows)]
605    #[pyfunction]
606    fn unsetenv(name: PyStrRef, vm: &VirtualMachine) -> PyResult<()> {
607        let name_str = name.expect_str();
608        // Search from index 1 because on Windows starting '=' is allowed for
609        // defining hidden environment variables.
610        if name_str.is_empty()
611            || name_str.get(1..).is_some_and(|s| s.contains('='))
612            || name.contains_nuls()
613        {
614            cold_path();
615            return Err(vm.new_value_error("illegal environment variable name"));
616        }
617        // "name=" to unset (empty value removes the variable)
618        let env_str = format!("{name_str}=");
619        // env_str is guaranteed nul-free by the checks above.
620        let wide = widestring::WideCString::from_str(&env_str)
621            .expect("env_str validated to contain no NUL");
622        check_env_var_len(wide.len() + 1, vm)?;
623
624        // Use _wputenv (not SetEnvironmentVariableW) to update CRT environ.
625        rustpython_host_env::nt::wputenv(&wide).map_err(|e| e.into_pyexception(vm))
626    }
627
628    #[cfg(not(windows))]
629    #[pyfunction]
630    fn unsetenv(
631        name: crate::function::Either<PyStrRef, PyBytesRef>,
632        vm: &VirtualMachine,
633    ) -> PyResult<()> {
634        let Some(name) = env_bytes_as_bytes_checked(&name) else {
635            cold_path();
636            return Err(exceptions::nul_byte_error(vm));
637        };
638        if name.is_empty() || name.contains(&b'=') {
639            let x = vm.new_errno_error(
640                22,
641                format!(
642                    "Invalid argument: {}",
643                    core::str::from_utf8(name).unwrap_or("<bytes encoding failure>")
644                ),
645            );
646
647            return Err(x.upcast());
648        }
649        let name = super::bytes_as_os_str(name, vm)?;
650        // SAFETY: requirements forwarded from the caller
651        unsafe { crate::host_env::os::remove_var(name) };
652        Ok(())
653    }
654
655    #[pyfunction]
656    fn readlink(args: PathDirFd<'_, 0>, vm: &VirtualMachine) -> PyResult {
657        let path = args.path;
658        let mode = path.mode();
659        let [] = args.dir_fd.0;
660        let path =
661            fs::read_link(&path).map_err(|err| OSErrorBuilder::with_filename(&err, path, vm))?;
662        Ok(mode.process_path(path, vm))
663    }
664
665    #[pyattr]
666    #[pyclass(name)]
667    #[derive(Debug, PyPayload)]
668    struct DirEntry {
669        file_name: std::ffi::OsString,
670        pathval: PathBuf,
671        file_type: io::Result<fs::FileType>,
672        /// dirent d_type value, used when file_type is unavailable (fd-based scandir)
673        #[cfg(unix)]
674        d_type: Option<u8>,
675        /// Parent directory fd for fd-based scandir, used for fstatat
676        #[cfg(not(any(windows, target_os = "redox")))]
677        dir_fd: Option<crt_fd::Raw>,
678        mode: OutputMode,
679        stat: OnceCell<PyObjectRef>,
680        lstat: OnceCell<PyObjectRef>,
681        #[cfg(unix)]
682        ino: AtomicCell<u64>,
683        #[cfg(windows)]
684        ino: AtomicCell<Option<u128>>,
685        #[cfg(not(any(unix, windows)))]
686        ino: AtomicCell<Option<u64>>,
687        /// `WIN32_FIND_DATAW` from `rposix_scandir.nextentry`.
688        #[cfg(windows)]
689        find: Option<host_nt::ScandirEntry>,
690    }
691
692    impl DirEntry {
693        /// Build the DirFd to use for stat calls.
694        /// If this entry was produced by fd-based scandir, use the stored dir_fd
695        /// so that fstatat(dir_fd, name, ...) is used instead of stat(full_path).
696        fn stat_dir_fd(&self) -> DirFd<'_, { STAT_DIR_FD as usize }> {
697            #[cfg(not(any(windows, target_os = "redox")))]
698            if let Some(raw_fd) = self.dir_fd {
699                // Safety: the fd came from os.open() and is borrowed for
700                // the lifetime of this DirEntry reference.
701                let borrowed = unsafe { crt_fd::Borrowed::borrow_raw(raw_fd) };
702                return DirFd([borrowed; STAT_DIR_FD as usize], PhantomData);
703            }
704            DirFd::default()
705        }
706
707        /// Stat-based mode test fallback. Uses fstatat when dir_fd is available.
708        #[cfg(unix)]
709        fn test_mode_via_stat(
710            &self,
711            follow_symlinks: bool,
712            mode_bits: u32,
713            vm: &VirtualMachine,
714        ) -> PyResult<bool> {
715            match self.stat(FollowSymlinks(follow_symlinks), vm) {
716                Ok(stat_obj) => {
717                    let st_mode: i32 = stat_obj.get_attr("st_mode", vm)?.try_into_value(vm)?;
718                    #[allow(
719                        clippy::unnecessary_cast,
720                        reason = "'st_mode' and 'S_IFMT' are not u32 on all platforms"
721                    )]
722                    Ok((st_mode as u32 & libc::S_IFMT as u32) == mode_bits)
723                }
724                Err(e) => {
725                    if e.fast_isinstance(vm.ctx.exceptions.file_not_found_error) {
726                        Ok(false)
727                    } else {
728                        Err(e)
729                    }
730                }
731            }
732        }
733
734        fn is_symlink(&self, vm: &VirtualMachine) -> PyResult<bool> {
735            #[cfg(windows)]
736            if let Some(find) = &self.find {
737                return Ok(find.is_symlink());
738            }
739            if let Ok(file_type) = &self.file_type {
740                return Ok(file_type.is_symlink());
741            }
742            #[cfg(unix)]
743            if let Some(dt) = self.d_type
744                && dt != libc::DT_UNKNOWN
745            {
746                return Ok(dt == libc::DT_LNK);
747            }
748            #[cfg(unix)]
749            return self.test_mode_via_stat(false, libc::S_IFLNK as _, vm);
750            #[cfg(not(unix))]
751            match &self.file_type {
752                Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
753                Err(e) => {
754                    use crate::convert::ToPyException;
755                    Err(e.to_pyexception(vm))
756                }
757                Ok(_) => Ok(false),
758            }
759        }
760
761        fn stat(&self, follow_symlinks: FollowSymlinks, vm: &VirtualMachine) -> PyResult {
762            let effective_dir_fd = self.stat_dir_fd();
763            let do_stat = |follow_symlinks| {
764                stat_at(
765                    OsPath {
766                        path: self.pathval.as_os_str().to_owned(),
767                        origin: None,
768                    }
769                    .into(),
770                    effective_dir_fd,
771                    FollowSymlinks(follow_symlinks),
772                    vm,
773                )
774            };
775            let lstat = || match self.lstat.get() {
776                Some(val) => Ok(val),
777                None => {
778                    let val = do_stat(false)?;
779                    let _ = self.lstat.set(val);
780                    Ok(self.lstat.get().unwrap())
781                }
782            };
783            let stat = if follow_symlinks.0 {
784                match self.stat.get() {
785                    Some(val) => val,
786                    None => {
787                        let val = if self.is_symlink(vm)? {
788                            do_stat(true)?
789                        } else {
790                            lstat()?.clone()
791                        };
792                        let _ = self.stat.set(val);
793                        self.stat.get().unwrap()
794                    }
795                }
796            } else {
797                lstat()?
798            };
799            Ok(stat.clone())
800        }
801    }
802
803    #[pyclass(flags(DISALLOW_INSTANTIATION), with(Representable))]
804    impl Py<DirEntry> {
805        #[pygetset]
806        fn name(&self, vm: &VirtualMachine) -> PyObjectRef {
807            self.mode.process_path(&self.file_name, vm)
808        }
809
810        #[pygetset]
811        fn path(&self, vm: &VirtualMachine) -> PyObjectRef {
812            self.mode.process_path(&self.pathval, vm)
813        }
814
815        #[pymethod]
816        fn is_dir(&self, follow_symlinks: FollowSymlinks, vm: &VirtualMachine) -> PyResult<bool> {
817            #[cfg(windows)]
818            if let Some(find) = &self.find {
819                // Follow only real symlinks. Junctions are not symlinks, so
820                // FILE_ATTRIBUTE_DIRECTORY is used as-is. A directory symlink
821                // is not a directory when follow_symlinks is false.
822                let is_symlink = find.is_symlink();
823                if !(follow_symlinks.0 && is_symlink) {
824                    return Ok(!is_symlink && find.is_directory());
825                }
826            }
827            if let Ok(file_type) = &self.file_type
828                && (!follow_symlinks.0 || !file_type.is_symlink())
829            {
830                return Ok(file_type.is_dir());
831            }
832            #[cfg(unix)]
833            if let Some(dt) = self.d_type {
834                let is_symlink = dt == libc::DT_LNK;
835                let need_stat = dt == libc::DT_UNKNOWN || (follow_symlinks.0 && is_symlink);
836                if !need_stat {
837                    return Ok(dt == libc::DT_DIR);
838                }
839            }
840            #[cfg(unix)]
841            return self.test_mode_via_stat(follow_symlinks.0, libc::S_IFDIR as _, vm);
842            #[cfg(not(unix))]
843            match super::fs_metadata(&self.pathval, follow_symlinks.0) {
844                Ok(meta) => Ok(meta.is_dir()),
845                Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
846                Err(e) => Err(e.into_pyexception(vm)),
847            }
848        }
849
850        #[pymethod]
851        fn is_file(&self, follow_symlinks: FollowSymlinks, vm: &VirtualMachine) -> PyResult<bool> {
852            #[cfg(windows)]
853            if let Some(find) = &self.find
854                && (!follow_symlinks.0 || !find.is_symlink())
855            {
856                return Ok(find.is_file());
857            }
858            if let Ok(file_type) = &self.file_type
859                && (!follow_symlinks.0 || !file_type.is_symlink())
860            {
861                return Ok(file_type.is_file());
862            }
863            #[cfg(unix)]
864            if let Some(dt) = self.d_type {
865                let is_symlink = dt == libc::DT_LNK;
866                let need_stat = dt == libc::DT_UNKNOWN || (follow_symlinks.0 && is_symlink);
867                if !need_stat {
868                    return Ok(dt == libc::DT_REG);
869                }
870            }
871            #[cfg(unix)]
872            return self.test_mode_via_stat(follow_symlinks.0, libc::S_IFREG as _, vm);
873            #[cfg(not(unix))]
874            match super::fs_metadata(&self.pathval, follow_symlinks.0) {
875                Ok(meta) => Ok(meta.is_file()),
876                Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
877                Err(e) => Err(e.into_pyexception(vm)),
878            }
879        }
880
881        #[pymethod]
882        fn is_symlink(&self, vm: &VirtualMachine) -> PyResult<bool> {
883            self.payload.is_symlink(vm)
884        }
885
886        #[pymethod]
887        fn stat(&self, follow_symlinks: FollowSymlinks, vm: &VirtualMachine) -> PyResult {
888            self.payload.stat(follow_symlinks, vm)
889        }
890
891        #[cfg(windows)]
892        #[pymethod]
893        fn inode(&self, vm: &VirtualMachine) -> PyResult<u128> {
894            match self.ino.load() {
895                Some(ino) => Ok(ino),
896                None => {
897                    let stat = stat_inner(
898                        OsPath::new_str(self.pathval.as_os_str()).into(),
899                        DirFd::default(),
900                        FollowSymlinks(false),
901                    )
902                    .map_err(|e| e.into_pyexception(vm))?
903                    .ok_or_else(|| crate::exceptions::nul_char_error(vm))?;
904                    // On Windows, combine st_ino and st_ino_high into 128-bit value
905                    let ino: u128 = cfg_select! {
906                        windows => stat.st_ino as u128 | ((stat.st_ino_high as u128) << 64),
907                        _ => stat.st_ino as u128,
908                    };
909                    // Err(T) means other thread set `ino` at the mean time which is safe to ignore
910                    let _ = self.ino.compare_exchange(None, Some(ino));
911                    Ok(ino)
912                }
913            }
914        }
915
916        #[cfg(unix)]
917        #[pymethod]
918        fn inode(&self, _vm: &VirtualMachine) -> u64 {
919            self.ino.load()
920        }
921
922        #[cfg(not(any(unix, windows)))]
923        #[pymethod]
924        fn inode(&self, _vm: &VirtualMachine) -> Option<u64> {
925            self.ino.load()
926        }
927
928        #[cfg(not(windows))]
929        #[pymethod]
930        const fn is_junction(&self, _vm: &VirtualMachine) -> bool {
931            false
932        }
933
934        #[cfg(windows)]
935        #[pymethod]
936        fn is_junction(&self, _vm: &VirtualMachine) -> bool {
937            self.pathval.to_wide_cstring().is_ok_and(|path| {
938                host_nt::test_file_type_by_name(&path, host_nt::TestType::Junction)
939            })
940        }
941
942        #[pymethod]
943        fn __fspath__(&self, vm: &VirtualMachine) -> PyObjectRef {
944            self.path(vm)
945        }
946
947        #[pyclassmethod]
948        fn __class_getitem__(
949            cls: PyTypeRef,
950            object: PyObjectRef,
951            vm: &VirtualMachine,
952        ) -> PyResult<PyGenericAlias> {
953            PyGenericAlias::from_args(cls, object, vm)
954        }
955
956        #[pymethod]
957        fn __reduce__(&self, vm: &VirtualMachine) -> PyResult {
958            Err(vm.new_type_error("cannot pickle 'DirEntry' object"))
959        }
960    }
961
962    impl Representable for DirEntry {
963        #[inline]
964        fn repr_wtf8(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<Wtf8Buf> {
965            let name = match zelf.as_object().get_attr("name", vm) {
966                Ok(name) => Some(name),
967                Err(e)
968                    if e.fast_isinstance(vm.ctx.exceptions.attribute_error)
969                        || e.fast_isinstance(vm.ctx.exceptions.value_error) =>
970                {
971                    None
972                }
973                Err(e) => return Err(e),
974            };
975            if let Some(name) = name {
976                if let Some(_guard) = ReprGuard::enter(vm, zelf.as_object()) {
977                    let repr = name.repr(vm)?;
978                    let mut result = Wtf8Buf::from(format!("<{} ", zelf.class()));
979                    result.push_wtf8(repr.as_wtf8());
980                    result.push_char('>');
981                    Ok(result)
982                } else {
983                    Err(vm.new_runtime_error(format!(
984                        "reentrant call inside {}.__repr__",
985                        zelf.class()
986                    )))
987                }
988            } else {
989                Ok(Wtf8Buf::from(format!("<{}>", zelf.class())))
990            }
991        }
992    }
993    #[pyattr]
994    #[pyclass(name = "ScandirIter")]
995    #[derive(Debug, PyPayload)]
996    struct ScandirIterator {
997        #[cfg(windows)]
998        entries: PyRwLock<Option<host_nt::Scandir>>,
999        #[cfg(not(windows))]
1000        entries: PyRwLock<Option<fs::ReadDir>>,
1001        mode: OutputMode,
1002    }
1003
1004    #[pyclass(flags(DISALLOW_INSTANTIATION), with(Destructor, IterNext, Iterable))]
1005    impl Py<ScandirIterator> {
1006        #[pymethod]
1007        fn close(&self) {
1008            let _dropped = self.entries.write().take();
1009        }
1010
1011        #[pymethod]
1012        const fn __enter__(zelf: PyRef<ScandirIterator>) -> PyRef<ScandirIterator> {
1013            zelf
1014        }
1015
1016        #[pymethod]
1017        fn __exit__(zelf: PyRef<ScandirIterator>, _args: FuncArgs) {
1018            zelf.close()
1019        }
1020
1021        #[pymethod]
1022        fn __reduce__(&self, vm: &VirtualMachine) -> PyResult {
1023            Err(vm.new_type_error("cannot pickle 'ScandirIterator' object"))
1024        }
1025    }
1026
1027    impl Destructor for ScandirIterator {
1028        fn del(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<()> {
1029            // Emit ResourceWarning if the iterator is not yet exhausted/closed
1030            if zelf.entries.read().is_some() {
1031                let _ = crate::stdlib::_warnings::warn(
1032                    vm.ctx.exceptions.resource_warning,
1033                    format!("unclosed scandir iterator {:?}", zelf.as_object()),
1034                    1,
1035                    vm,
1036                );
1037                zelf.close();
1038            }
1039            Ok(())
1040        }
1041    }
1042
1043    impl SelfIter for ScandirIterator {}
1044
1045    impl IterNext for ScandirIterator {
1046        fn next(zelf: &crate::Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
1047            #[cfg(windows)]
1048            {
1049                let mut entryref = zelf.entries.write();
1050                let inner = match entryref.as_mut() {
1051                    None => return Ok(PyIterReturn::StopIteration(None)),
1052                    Some(inner) => inner,
1053                };
1054                loop {
1055                    match inner.next_entry() {
1056                        Ok(None) => {
1057                            let _dropped = entryref.take();
1058                            return Ok(PyIterReturn::StopIteration(None));
1059                        }
1060                        Err(err) => {
1061                            let _dropped = entryref.take();
1062                            return Err(err.into_pyexception(vm));
1063                        }
1064                        Ok(Some(entry)) if entry.is_dot_or_dotdot() => continue,
1065                        Ok(Some(entry)) => {
1066                            let pathval = PathBuf::from(inner.entry_path(&entry));
1067                            let lstat = {
1068                                let cell = OnceCell::new();
1069                                let stat_obj =
1070                                    StatResultData::from_stat(&entry.to_stat_struct(), vm)
1071                                        .to_pyobject(vm);
1072                                let _ = cell.set(stat_obj);
1073                                cell
1074                            };
1075                            return Ok(PyIterReturn::Return(
1076                                DirEntry {
1077                                    file_name: entry.name.clone(),
1078                                    pathval,
1079                                    file_type: Err(io::Error::other(
1080                                        "file_type taken from find data",
1081                                    )),
1082                                    mode: zelf.mode,
1083                                    lstat,
1084                                    stat: OnceCell::new(),
1085                                    ino: AtomicCell::new(None),
1086                                    find: Some(entry),
1087                                }
1088                                .into_ref(&vm.ctx)
1089                                .into(),
1090                            ));
1091                        }
1092                    }
1093                }
1094            }
1095            #[cfg(not(windows))]
1096            {
1097                let entryref: &mut Option<fs::ReadDir> = &mut zelf.entries.write();
1098
1099                match entryref {
1100                    None => Ok(PyIterReturn::StopIteration(None)),
1101                    Some(inner) => match inner.next() {
1102                        Some(entry) => match entry {
1103                            Ok(entry) => {
1104                                #[cfg(unix)]
1105                                let ino = {
1106                                    use std::os::unix::fs::DirEntryExt;
1107                                    entry.ino()
1108                                };
1109                                #[cfg(not(unix))]
1110                                let ino = None;
1111
1112                                let pathval = entry.path();
1113                                let lstat = OnceCell::new();
1114
1115                                Ok(PyIterReturn::Return(
1116                                    DirEntry {
1117                                        file_name: entry.file_name(),
1118                                        pathval,
1119                                        file_type: entry.file_type(),
1120                                        #[cfg(unix)]
1121                                        d_type: None,
1122                                        #[cfg(not(any(windows, target_os = "redox")))]
1123                                        dir_fd: None,
1124                                        mode: zelf.mode,
1125                                        lstat,
1126                                        stat: OnceCell::new(),
1127                                        ino: AtomicCell::new(ino),
1128                                    }
1129                                    .into_ref(&vm.ctx)
1130                                    .into(),
1131                                ))
1132                            }
1133                            Err(err) => Err(err.into_pyexception(vm)),
1134                        },
1135                        None => {
1136                            let _dropped = entryref.take();
1137                            Ok(PyIterReturn::StopIteration(None))
1138                        }
1139                    },
1140                }
1141            }
1142        }
1143    }
1144
1145    #[cfg(all(unix, not(target_os = "redox")))]
1146    #[pyattr]
1147    #[pyclass(name = "ScandirIter")]
1148    #[derive(Debug, PyPayload)]
1149    struct ScandirIteratorFd {
1150        dir: crate::common::lock::PyMutex<Option<host_posix::FdDirStream>>,
1151        /// The original fd passed to scandir(), stored in DirEntry for fstatat
1152        orig_fd: crt_fd::Raw,
1153    }
1154
1155    #[cfg(all(unix, not(target_os = "redox")))]
1156    #[pyclass(flags(DISALLOW_INSTANTIATION), with(Destructor, IterNext, Iterable))]
1157    impl Py<ScandirIteratorFd> {
1158        #[pymethod]
1159        fn close(&self) {
1160            let _dropped = self.dir.lock().take();
1161        }
1162
1163        #[pymethod]
1164        const fn __enter__(zelf: PyRef<ScandirIteratorFd>) -> PyRef<ScandirIteratorFd> {
1165            zelf
1166        }
1167
1168        #[pymethod]
1169        fn __exit__(zelf: PyRef<ScandirIteratorFd>, _args: FuncArgs) {
1170            zelf.close()
1171        }
1172
1173        #[pymethod]
1174        fn __reduce__(&self, vm: &VirtualMachine) -> PyResult {
1175            Err(vm.new_type_error("cannot pickle 'ScandirIterator' object"))
1176        }
1177    }
1178
1179    #[cfg(all(unix, not(target_os = "redox")))]
1180    impl Destructor for ScandirIteratorFd {
1181        fn del(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<()> {
1182            if zelf.dir.lock().is_some() {
1183                let _ = crate::stdlib::_warnings::warn(
1184                    vm.ctx.exceptions.resource_warning,
1185                    format!("unclosed scandir iterator {:?}", zelf.as_object()),
1186                    1,
1187                    vm,
1188                );
1189                zelf.close();
1190            }
1191            Ok(())
1192        }
1193    }
1194
1195    #[cfg(all(unix, not(target_os = "redox")))]
1196    impl SelfIter for ScandirIteratorFd {}
1197
1198    #[cfg(all(unix, not(target_os = "redox")))]
1199    impl IterNext for ScandirIteratorFd {
1200        fn next(zelf: &crate::Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
1201            let mut guard = zelf.dir.lock();
1202            let dir = match guard.as_mut() {
1203                None => return Ok(PyIterReturn::StopIteration(None)),
1204                Some(dir) => dir,
1205            };
1206            let Some(entry) = dir.next_entry().map_err(|e| e.into_pyexception(vm))? else {
1207                drop(guard.take());
1208                return Ok(PyIterReturn::StopIteration(None));
1209            };
1210            let file_name = std::ffi::OsString::from(
1211                rustpython_host_env::os::bytes_as_os_str(&entry.name)
1212                    .expect("unix dir entry names are arbitrary bytes"),
1213            );
1214            let pathval = PathBuf::from(&file_name);
1215            Ok(PyIterReturn::Return(
1216                DirEntry {
1217                    file_name,
1218                    pathval,
1219                    file_type: Err(io::Error::other(
1220                        "file_type unavailable for fd-based scandir",
1221                    )),
1222                    d_type: entry.d_type,
1223                    dir_fd: Some(zelf.orig_fd),
1224                    mode: OutputMode::String,
1225                    lstat: OnceCell::new(),
1226                    stat: OnceCell::new(),
1227                    ino: AtomicCell::new(entry.ino as _),
1228                }
1229                .into_ref(&vm.ctx)
1230                .into(),
1231            ))
1232        }
1233    }
1234
1235    #[derive(FromArgs)]
1236    struct ScandirArgs<'a> {
1237        #[pyarg(any, optional)]
1238        path: Option<OsPathOrFd<'a>>,
1239    }
1240
1241    #[pyfunction]
1242    fn scandir(args: ScandirArgs<'_>, vm: &VirtualMachine) -> PyResult {
1243        let path = args
1244            .path
1245            .unwrap_or_else(|| OsPathOrFd::Path(OsPath::new_str(".")));
1246        match path {
1247            OsPathOrFd::Path(path) => {
1248                #[cfg(windows)]
1249                {
1250                    let wide = path
1251                        .path
1252                        .to_wide_cstring()
1253                        .map_err(|err| OSErrorBuilder::with_filename(&err, path.clone(), vm))?;
1254                    let entries = host_nt::scandir(&wide)
1255                        .map_err(|err| OSErrorBuilder::with_filename(&err, path.clone(), vm))?;
1256                    Ok(ScandirIterator {
1257                        entries: PyRwLock::new(Some(entries)),
1258                        mode: path.mode(),
1259                    }
1260                    .into_ref(&vm.ctx)
1261                    .into())
1262                }
1263                #[cfg(not(windows))]
1264                {
1265                    let entries = crate::host_env::fs::read_dir(&path.path)
1266                        .map_err(|err| OSErrorBuilder::with_filename(&err, path.clone(), vm))?;
1267                    Ok(ScandirIterator {
1268                        entries: PyRwLock::new(Some(entries)),
1269                        mode: path.mode(),
1270                    }
1271                    .into_ref(&vm.ctx)
1272                    .into())
1273                }
1274            }
1275            OsPathOrFd::Fd(fno) => {
1276                #[cfg(not(all(unix, not(target_os = "redox"))))]
1277                {
1278                    let _ = fno;
1279                    Err(vm.new_not_implemented_error("can't pass fd to scandir on this platform"))
1280                }
1281                #[cfg(all(unix, not(target_os = "redox")))]
1282                {
1283                    let dir = host_posix::FdDirStream::from_fd(fno.into())
1284                        .map_err(|e| e.into_pyexception(vm))?;
1285                    Ok(ScandirIteratorFd {
1286                        dir: crate::common::lock::PyMutex::new(Some(dir)),
1287                        orig_fd: fno.as_raw(),
1288                    }
1289                    .into_ref(&vm.ctx)
1290                    .into())
1291                }
1292            }
1293        }
1294    }
1295
1296    #[derive(Debug, FromArgs)]
1297    #[pystruct_sequence_data]
1298    struct StatResultData {
1299        pub st_mode: PyIntRef,
1300        pub st_ino: PyIntRef,
1301        pub st_dev: PyIntRef,
1302        pub st_nlink: PyIntRef,
1303        pub st_uid: PyIntRef,
1304        pub st_gid: PyIntRef,
1305        pub st_size: PyIntRef,
1306        // Indices 7-9: integer seconds
1307        #[pyarg(positional, default)]
1308        #[pystruct_sequence(unnamed)]
1309        pub st_atime_int: i64,
1310        #[pyarg(positional, default)]
1311        #[pystruct_sequence(unnamed)]
1312        pub st_mtime_int: i64,
1313        #[pyarg(positional, default)]
1314        #[pystruct_sequence(unnamed)]
1315        pub st_ctime_int: i64,
1316        // Float time attributes
1317        #[pyarg(any, default)]
1318        #[pystruct_sequence(skip)]
1319        pub st_atime: f64,
1320        #[pyarg(any, default)]
1321        #[pystruct_sequence(skip)]
1322        pub st_mtime: f64,
1323        #[pyarg(any, default)]
1324        #[pystruct_sequence(skip)]
1325        pub st_ctime: f64,
1326        // Nanosecond attributes
1327        #[pyarg(any, default)]
1328        #[pystruct_sequence(skip)]
1329        pub st_atime_ns: i128,
1330        #[pyarg(any, default)]
1331        #[pystruct_sequence(skip)]
1332        pub st_mtime_ns: i128,
1333        #[pyarg(any, default)]
1334        #[pystruct_sequence(skip)]
1335        pub st_ctime_ns: i128,
1336        // Unix-specific attributes
1337        #[cfg(not(windows))]
1338        #[pyarg(any, default)]
1339        #[pystruct_sequence(skip)]
1340        pub st_blksize: u64,
1341        #[cfg(not(windows))]
1342        #[pyarg(any, default)]
1343        #[pystruct_sequence(skip)]
1344        pub st_blocks: u64,
1345        #[cfg(windows)]
1346        #[pyarg(any, default)]
1347        #[pystruct_sequence(skip)]
1348        pub st_reparse_tag: u32,
1349        #[cfg(windows)]
1350        #[pyarg(any, default)]
1351        #[pystruct_sequence(skip)]
1352        pub st_file_attributes: u32,
1353    }
1354
1355    impl StatResultData {
1356        fn from_stat(stat: &StatStruct, vm: &VirtualMachine) -> Self {
1357            let (atime, mtime, ctime);
1358            #[cfg(all(any(unix, windows), not(target_os = "wasi")))]
1359            {
1360                atime = (stat.st_atime, stat.st_atime_nsec);
1361                mtime = (stat.st_mtime, stat.st_mtime_nsec);
1362                ctime = (stat.st_ctime, stat.st_ctime_nsec);
1363            }
1364            #[cfg(target_os = "wasi")]
1365            {
1366                atime = (stat.st_atim.tv_sec, stat.st_atim.tv_nsec);
1367                mtime = (stat.st_mtim.tv_sec, stat.st_mtim.tv_nsec);
1368                ctime = (stat.st_ctim.tv_sec, stat.st_ctim.tv_nsec);
1369            }
1370
1371            const NANOS_PER_SEC: u32 = 1_000_000_000;
1372            let to_f64 = |(s, ns)| (s as f64) + (ns as f64) / (NANOS_PER_SEC as f64);
1373            let to_ns = |(s, ns)| s as i128 * NANOS_PER_SEC as i128 + ns as i128;
1374
1375            #[cfg(windows)]
1376            let st_reparse_tag = stat.st_reparse_tag;
1377            #[cfg(windows)]
1378            let st_file_attributes = stat.st_file_attributes;
1379
1380            // On Windows, combine st_ino and st_ino_high into a 128-bit value
1381            // like _pystat_l128_from_l64_l64
1382            #[cfg(windows)]
1383            let st_ino: u128 = stat.st_ino as u128 | ((stat.st_ino_high as u128) << 64);
1384            #[cfg(not(windows))]
1385            let st_ino = stat.st_ino;
1386
1387            #[cfg(not(windows))]
1388            #[allow(
1389                clippy::useless_conversion,
1390                reason = "signedness differs between platforms"
1391            )]
1392            let st_blksize = stat.st_blksize.try_into().unwrap_or(4096);
1393
1394            #[cfg(not(windows))]
1395            #[allow(
1396                clippy::useless_conversion,
1397                reason = "signedness differs between platforms"
1398            )]
1399            let st_blocks = stat.st_blocks.try_into().unwrap_or_default();
1400
1401            Self {
1402                st_mode: vm.ctx.new_pyref(stat.st_mode),
1403                st_ino: vm.ctx.new_pyref(st_ino),
1404                st_dev: vm.ctx.new_pyref(stat.st_dev),
1405                st_nlink: vm.ctx.new_pyref(stat.st_nlink),
1406                st_uid: vm.ctx.new_pyref(stat.st_uid),
1407                st_gid: vm.ctx.new_pyref(stat.st_gid),
1408                st_size: vm.ctx.new_pyref(stat.st_size),
1409                st_atime_int: atime.0,
1410                st_mtime_int: mtime.0,
1411                st_ctime_int: ctime.0,
1412                st_atime: to_f64(atime),
1413                st_mtime: to_f64(mtime),
1414                st_ctime: to_f64(ctime),
1415                st_atime_ns: to_ns(atime),
1416                st_mtime_ns: to_ns(mtime),
1417                st_ctime_ns: to_ns(ctime),
1418                #[cfg(not(windows))]
1419                st_blksize,
1420                #[cfg(not(windows))]
1421                st_blocks,
1422                #[cfg(windows)]
1423                st_reparse_tag,
1424                #[cfg(windows)]
1425                st_file_attributes,
1426            }
1427        }
1428    }
1429
1430    #[pyattr]
1431    #[pystruct_sequence(name = "stat_result", module = "os", data = "StatResultData")]
1432    struct PyStatResult;
1433
1434    #[pyclass(with(PyStructSequence))]
1435    impl PyStatResult {
1436        #[pyslot]
1437        fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
1438            let result = crate::types::struct_sequence_new(
1439                cls.clone(),
1440                args.bind_for(vm, Self::NAME)?,
1441                StatResultData::OPTIONAL_FIELD_NAMES,
1442                vm,
1443            )?;
1444            let tuple = result.downcast_ref::<PyTuple>().unwrap();
1445            let mut items: Vec<PyObjectRef> = tuple.as_slice().to_vec();
1446
1447            // Copy integer time fields to hidden float timestamp slots when not provided.
1448            // indices 7-9: st_atime_int, st_mtime_int, st_ctime_int
1449            // i+3: st_atime/st_mtime/st_ctime (float timestamps, copied from int if missing)
1450            // i+6: st_atime_ns/st_mtime_ns/st_ctime_ns (left as None if not provided)
1451            for i in 7..=9 {
1452                if vm.is_none(&items[i + 3]) {
1453                    items[i + 3] = items[i].clone();
1454                }
1455            }
1456
1457            PyTuple::new_unchecked(items.into_boxed_slice())
1458                .into_ref_with_type(vm, cls)
1459                .map(Into::into)
1460        }
1461    }
1462
1463    #[cfg(windows)]
1464    fn stat_inner(
1465        file: OsPathOrFd<'_>,
1466        dir_fd: DirFd<'_, { STAT_DIR_FD as usize }>,
1467        follow_symlinks: FollowSymlinks,
1468    ) -> io::Result<Option<StatStruct>> {
1469        let [] = dir_fd.0;
1470        match file {
1471            OsPathOrFd::Path(path) => {
1472                let path = path.path.to_wide_cstring()?;
1473                host_nt::win32_xstat(&path, follow_symlinks.0)
1474            }
1475            OsPathOrFd::Fd(fd) => crate::host_env::fileutils::fstat(fd),
1476        }
1477        .map(Some)
1478    }
1479
1480    #[cfg(not(windows))]
1481    fn stat_inner(
1482        file: OsPathOrFd<'_>,
1483        dir_fd: DirFd<'_, { STAT_DIR_FD as usize }>,
1484        follow_symlinks: FollowSymlinks,
1485    ) -> io::Result<Option<StatStruct>> {
1486        match file {
1487            OsPathOrFd::Path(path) => {
1488                host_posix::stat_path(path, dir_fd.get_opt(), follow_symlinks.0)
1489            }
1490            OsPathOrFd::Fd(fd) => host_posix::stat_fd(fd).map(Some),
1491        }
1492    }
1493
1494    #[derive(FromArgs)]
1495    struct StatArgs<'a> {
1496        #[pyarg(any)]
1497        path: OsPathOrFd<'a>,
1498        #[pyarg(flatten)]
1499        dir_fd: DirFd<'a, { STAT_DIR_FD as usize }>,
1500        #[pyarg(flatten)]
1501        follow_symlinks: FollowSymlinks,
1502    }
1503
1504    fn stat_at(
1505        path: OsPathOrFd<'_>,
1506        dir_fd: DirFd<'_, { STAT_DIR_FD as usize }>,
1507        follow_symlinks: FollowSymlinks,
1508        vm: &VirtualMachine,
1509    ) -> PyResult {
1510        if matches!(path, OsPathOrFd::Fd(_)) && !follow_symlinks.0 {
1511            return Err(vm.new_value_error("stat: cannot use fd and follow_symlinks together"));
1512        }
1513        let stat = stat_inner(path.clone(), dir_fd, follow_symlinks)
1514            .map_err(|err| OSErrorBuilder::with_filename(&err, path, vm))?
1515            .ok_or_else(|| crate::exceptions::nul_char_error(vm))?;
1516        Ok(StatResultData::from_stat(&stat, vm).to_pyobject(vm))
1517    }
1518
1519    #[pyfunction]
1520    fn stat(args: StatArgs<'_>, vm: &VirtualMachine) -> PyResult {
1521        let StatArgs {
1522            path,
1523            dir_fd,
1524            follow_symlinks,
1525        } = args;
1526        stat_at(path, dir_fd, follow_symlinks, vm)
1527    }
1528
1529    #[derive(FromArgs)]
1530    struct FstatArgs<'a> {
1531        #[pyarg(any)]
1532        fd: crt_fd::Borrowed<'a>,
1533    }
1534
1535    #[pyfunction]
1536    fn fstat(args: FstatArgs<'_>, vm: &VirtualMachine) -> PyResult {
1537        stat_at(
1538            OsPathOrFd::Fd(args.fd),
1539            DirFd::default(),
1540            FollowSymlinks(true),
1541            vm,
1542        )
1543    }
1544
1545    #[pyfunction]
1546    fn lstat(args: PathDirFd<'_, { STAT_DIR_FD as usize }>, vm: &VirtualMachine) -> PyResult {
1547        stat_at(args.path.into(), args.dir_fd, FollowSymlinks(false), vm)
1548    }
1549
1550    fn curdir_inner(vm: &VirtualMachine) -> PyResult<PathBuf> {
1551        crate::host_env::os::current_dir().map_err(|err| err.into_pyexception(vm))
1552    }
1553
1554    #[pyfunction]
1555    fn getcwd(vm: &VirtualMachine) -> PyResult {
1556        Ok(OutputMode::String.process_path(curdir_inner(vm)?, vm))
1557    }
1558
1559    #[pyfunction]
1560    fn getcwdb(vm: &VirtualMachine) -> PyResult {
1561        Ok(OutputMode::Bytes.process_path(curdir_inner(vm)?, vm))
1562    }
1563
1564    #[derive(FromArgs)]
1565    struct ChdirArgs {
1566        #[pyarg(any)]
1567        path: OsPath,
1568    }
1569
1570    #[pyfunction]
1571    fn chdir(path: ChdirArgs, vm: &VirtualMachine) -> PyResult<()> {
1572        let path = path.path;
1573        crate::host_env::os::set_current_dir(&path.path)
1574            .map_err(|err| OSErrorBuilder::with_filename(&err, path, vm))
1575    }
1576
1577    #[derive(FromArgs)]
1578    struct FspathArgs {
1579        #[pyarg(any)]
1580        path: PyObjectRef,
1581    }
1582
1583    #[pyfunction]
1584    fn fspath(path: FspathArgs, vm: &VirtualMachine) -> PyResult<FsPath> {
1585        FsPath::try_from_path_like(path.path, false, vm)
1586    }
1587
1588    #[derive(FromArgs)]
1589    struct RenameArgs<'fd> {
1590        #[pyarg(any)]
1591        src: PyObjectRef,
1592        #[pyarg(any)]
1593        dst: PyObjectRef,
1594        #[pyarg(flatten)]
1595        #[cfg_attr(not(any(unix, target_os = "wasi")), expect(dead_code))]
1596        src_dir_fd: DirFd<'fd, { RENAME_DIR_FD as usize }, SrcDirFd>,
1597        #[pyarg(flatten)]
1598        #[cfg_attr(not(any(unix, target_os = "wasi")), expect(dead_code))]
1599        dst_dir_fd: DirFd<'fd, { RENAME_DIR_FD as usize }, DstDirFd>,
1600    }
1601
1602    #[pyfunction]
1603    fn rename(args: RenameArgs<'_>, vm: &VirtualMachine) -> PyResult<()> {
1604        let src = PathConverter::new()
1605            .function("rename")
1606            .argument("src")
1607            .try_path(args.src, vm)?;
1608        let dst = PathConverter::new()
1609            .function("rename")
1610            .argument("dst")
1611            .try_path(args.dst, vm)?;
1612
1613        #[cfg(any(unix, target_os = "wasi"))]
1614        let src_dir_fd = args.src_dir_fd.get_opt();
1615        #[cfg(not(any(unix, target_os = "wasi")))]
1616        let src_dir_fd = None;
1617
1618        #[cfg(any(unix, target_os = "wasi"))]
1619        let dst_dir_fd = args.dst_dir_fd.get_opt();
1620        #[cfg(not(any(unix, target_os = "wasi")))]
1621        let dst_dir_fd = None;
1622
1623        crate::host_env::posix::rename(&src, src_dir_fd, &dst, dst_dir_fd).map_err(|err| {
1624            let builder = err.to_os_error_builder(vm);
1625            let builder = builder.filename(src.filename(vm));
1626            let builder = builder.filename2(dst.filename(vm));
1627            builder.build(vm).upcast()
1628        })
1629    }
1630
1631    #[pyfunction]
1632    fn replace(args: RenameArgs<'_>, vm: &VirtualMachine) -> PyResult<()> {
1633        let src = PathConverter::new()
1634            .function("replace")
1635            .argument("src")
1636            .try_path(args.src, vm)?;
1637        let dst = PathConverter::new()
1638            .function("replace")
1639            .argument("dst")
1640            .try_path(args.dst, vm)?;
1641
1642        #[cfg(any(unix, target_os = "wasi"))]
1643        let src_dir_fd = args.src_dir_fd.get_opt();
1644        #[cfg(not(any(unix, target_os = "wasi")))]
1645        let src_dir_fd = None;
1646
1647        #[cfg(any(unix, target_os = "wasi"))]
1648        let dst_dir_fd = args.dst_dir_fd.get_opt();
1649        #[cfg(not(any(unix, target_os = "wasi")))]
1650        let dst_dir_fd = None;
1651
1652        crate::host_env::posix::replace(&src, src_dir_fd, &dst, dst_dir_fd).map_err(|err| {
1653            let builder = err.to_os_error_builder(vm);
1654            let builder = builder.filename(src.filename(vm));
1655            let builder = builder.filename2(dst.filename(vm));
1656            builder.build(vm).upcast()
1657        })
1658    }
1659
1660    #[pyfunction]
1661    fn getpid(vm: &VirtualMachine) -> PyObjectRef {
1662        let pid = if cfg!(target_arch = "wasm32") {
1663            // Return an arbitrary value, greater than 1 which is special.
1664            // The value 42 is picked from wasi-libc
1665            // https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-21/libc-bottom-half/getpid/getpid.c
1666            42
1667        } else {
1668            crate::host_env::os::process_id()
1669        };
1670        vm.ctx.new_int(pid).into()
1671    }
1672
1673    #[pyfunction]
1674    fn cpu_count(vm: &VirtualMachine) -> PyObjectRef {
1675        // A count the configuration names is answered with, over the one the
1676        // host reports. = os_cpu_count_impl
1677        if let Some(configured) = vm.state.config.settings.cpu_count {
1678            return vm.ctx.new_int(configured.get()).into();
1679        }
1680        let cpu_count = crate::host_env::os::cpu_count();
1681        vm.ctx.new_int(cpu_count).into()
1682    }
1683
1684    #[derive(FromArgs)]
1685    struct ExitStatus {
1686        #[pyarg(any)]
1687        status: i32,
1688    }
1689
1690    #[pyfunction]
1691    fn _exit(status: ExitStatus) {
1692        crate::host_env::os::exit(status.status)
1693    }
1694
1695    #[pyfunction]
1696    fn abort() {
1697        crate::host_env::os::abort()
1698    }
1699
1700    #[pyfunction]
1701    fn urandom(size: isize, vm: &VirtualMachine) -> PyResult<Vec<u8>> {
1702        if size < 0 {
1703            return Err(vm.new_value_error("negative argument not allowed"));
1704        }
1705        crate::host_env::os::urandom(size as usize).map_err(|e| e.into_pyexception(vm))
1706    }
1707
1708    #[pyfunction]
1709    pub fn isatty(fd: i32) -> bool {
1710        crate::host_env::os::isatty(fd)
1711    }
1712
1713    #[pyfunction]
1714    pub(crate) fn lseek(
1715        fd: crt_fd::Borrowed<'_>,
1716        position: crt_fd::Offset,
1717        whence: i32,
1718        vm: &VirtualMachine,
1719    ) -> PyResult<crt_fd::Offset> {
1720        crate::host_env::os::seek_fd(fd, position, whence).map_err(|e| e.into_pyexception(vm))
1721    }
1722
1723    #[derive(FromArgs)]
1724    struct LinkArgs<'fd> {
1725        #[pyarg(any)]
1726        src: OsPath,
1727        #[pyarg(any)]
1728        dst: OsPath,
1729        #[pyarg(flatten)]
1730        src_dir_fd: DirFd<'fd, { LINK_DIR_FD as usize }, SrcDirFd>,
1731        #[pyarg(flatten)]
1732        dst_dir_fd: DirFd<'fd, { LINK_DIR_FD as usize }, DstDirFd>,
1733        #[pyarg(named, default = cfg!(not(windows)), py_default = "(os.name != 'nt')")]
1734        follow_symlinks: bool,
1735    }
1736
1737    #[pyfunction]
1738    fn link(args: LinkArgs<'_>, vm: &VirtualMachine) -> PyResult<()> {
1739        let LinkArgs {
1740            src,
1741            dst,
1742            src_dir_fd,
1743            dst_dir_fd,
1744            follow_symlinks,
1745        } = args;
1746
1747        #[cfg(unix)]
1748        {
1749            use std::os::unix::ffi::OsStrExt;
1750
1751            use crate::convert::ToPyException;
1752            let src_cstr = alloc::ffi::CString::new(src.path.as_os_str().as_bytes())
1753                .map_err(|e| e.to_pyexception(vm))?;
1754            let dst_cstr = alloc::ffi::CString::new(dst.path.as_os_str().as_bytes())
1755                .map_err(|e| e.to_pyexception(vm))?;
1756
1757            let src_fd = src_dir_fd.get().as_raw();
1758            let dst_fd = dst_dir_fd.get().as_raw();
1759            if let Err(err) = crate::host_env::posix::link_paths(
1760                src_fd,
1761                src_cstr.as_c_str(),
1762                dst_fd,
1763                dst_cstr.as_c_str(),
1764                follow_symlinks,
1765            ) {
1766                let builder = err.to_os_error_builder(vm);
1767                let builder = builder.filename(src.filename(vm));
1768                let builder = builder.filename2(dst.filename(vm));
1769                return Err(builder.build(vm).upcast());
1770            }
1771
1772            Ok(())
1773        }
1774
1775        #[cfg(windows)]
1776        {
1777            let _ = (src_dir_fd, dst_dir_fd);
1778            let src_path = if follow_symlinks {
1779                crate::host_env::fs::canonicalize(&src.path)
1780                    .unwrap_or_else(|_| PathBuf::from(src.path.clone()))
1781            } else {
1782                PathBuf::from(src.path.clone())
1783            };
1784            // CreateHardLinkW(new, existing)
1785            let src_wide = src_path
1786                .to_wide_cstring()
1787                .map_err(|err| err.into_pyexception(vm))?;
1788            let dst_wide = dst.to_wide_cstring(vm)?;
1789            rustpython_host_env::winapi::create_hard_link(&dst_wide, &src_wide).map_err(|err| {
1790                let builder = err.to_os_error_builder(vm);
1791                let builder = builder.filename(src.filename(vm));
1792                let builder = builder.filename2(dst.filename(vm));
1793                builder.build(vm).upcast()
1794            })
1795        }
1796
1797        #[cfg(not(any(unix, windows)))]
1798        {
1799            let _ = (src_dir_fd, dst_dir_fd);
1800            let src_path = if follow_symlinks {
1801                crate::host_env::fs::canonicalize(&src.path)
1802                    .unwrap_or_else(|_| PathBuf::from(src.path.clone()))
1803            } else {
1804                PathBuf::from(src.path.clone())
1805            };
1806
1807            fs::hard_link(&src_path, &dst.path).map_err(|err| {
1808                let builder = err.to_os_error_builder(vm);
1809                let builder = builder.filename(src.filename(vm));
1810                let builder = builder.filename2(dst.filename(vm));
1811                builder.build(vm).upcast()
1812            })
1813        }
1814    }
1815
1816    #[cfg(any(unix, windows))]
1817    #[derive(FromArgs)]
1818    struct SystemArgs {
1819        #[pyarg(any)]
1820        command: PyStrRef,
1821    }
1822
1823    #[cfg(any(unix, windows))]
1824    #[pyfunction]
1825    fn system(command: SystemArgs, vm: &VirtualMachine) -> PyResult<i32> {
1826        let command = command.command;
1827        let cstr = command.to_cstring(vm)?;
1828        let x = crate::host_env::os::system(cstr.as_c_str());
1829        Ok(x)
1830    }
1831
1832    #[derive(FromArgs)]
1833    struct UtimeArgs<'fd> {
1834        path: OsPath,
1835        #[pyarg(any, default)]
1836        times: Option<PyTupleRef>,
1837        #[pyarg(named, default)]
1838        ns: Option<PyTupleRef>,
1839        #[pyarg(flatten)]
1840        dir_fd: DirFd<'fd, { UTIME_DIR_FD as usize }>,
1841        #[pyarg(flatten)]
1842        follow_symlinks: FollowSymlinks,
1843    }
1844
1845    #[pyfunction]
1846    fn utime(args: UtimeArgs<'_>, vm: &VirtualMachine) -> PyResult<()> {
1847        let parse_tup = |tup: &Py<PyTuple>| -> Option<(PyObjectRef, PyObjectRef)> {
1848            if tup.as_slice().len() != 2 {
1849                None
1850            } else {
1851                Some((tup.as_slice()[0].clone(), tup.as_slice()[1].clone()))
1852            }
1853        };
1854        let (acc, modif) = match (args.times, args.ns) {
1855            (Some(t), None) => {
1856                let (a, m) = parse_tup(&t).ok_or_else(|| {
1857                    vm.new_type_error("utime: 'times' must be either a tuple of two ints or None")
1858                })?;
1859                (a.try_into_value(vm)?, m.try_into_value(vm)?)
1860            }
1861            (None, Some(ns)) => {
1862                let (a, m) = parse_tup(&ns)
1863                    .ok_or_else(|| vm.new_type_error("utime: 'ns' must be a tuple of two ints"))?;
1864                let ns_in_sec: PyObjectRef = vm.ctx.new_int(1_000_000_000).into();
1865                let ns_to_dur = |obj: PyObjectRef| {
1866                    let divmod = vm._divmod(&obj, &ns_in_sec)?;
1867                    let (div, rem) = divmod
1868                        .downcast_ref::<PyTuple>()
1869                        .and_then(parse_tup)
1870                        .ok_or_else(|| {
1871                            vm.new_type_error(format!(
1872                                "{}.__divmod__() must return a 2-tuple, not {}",
1873                                obj.class().name(),
1874                                divmod.class().name()
1875                            ))
1876                        })?;
1877                    let secs = div.try_index(vm)?.try_to_primitive(vm)?;
1878                    let ns = rem.try_index(vm)?.try_to_primitive(vm)?;
1879                    Ok(Duration::new(secs, ns))
1880                };
1881                // TODO: do validation to make sure this doesn't.. underflow?
1882                (ns_to_dur(a)?, ns_to_dur(m)?)
1883            }
1884            (None, None) => {
1885                let now = SystemTime::now();
1886                let now = now.duration_since(SystemTime::UNIX_EPOCH).unwrap();
1887                (now, now)
1888            }
1889            (Some(_), Some(_)) => {
1890                return Err(vm.new_value_error(
1891                    "utime: you may specify either 'times' or 'ns' but not both",
1892                ));
1893            }
1894        };
1895        utime_impl(args.path, acc, modif, args.dir_fd, args.follow_symlinks, vm)
1896    }
1897
1898    fn utime_impl(
1899        path: OsPath,
1900        acc: Duration,
1901        modif: Duration,
1902        dir_fd: DirFd<'_, { UTIME_DIR_FD as usize }>,
1903        _follow_symlinks: FollowSymlinks,
1904        vm: &VirtualMachine,
1905    ) -> PyResult<()> {
1906        #[cfg(any(target_os = "wasi", unix))]
1907        {
1908            #[cfg(not(target_os = "redox"))]
1909            {
1910                let path_for_err = path.clone();
1911                let path = path.into_cstring(vm)?;
1912                if let Err(err) = crate::host_env::posix::set_file_times_at(
1913                    dir_fd.get().as_raw(),
1914                    path.as_c_str(),
1915                    acc,
1916                    modif,
1917                    _follow_symlinks.0,
1918                ) {
1919                    Err(OSErrorBuilder::with_filename(&err, path_for_err, vm))
1920                } else {
1921                    Ok(())
1922                }
1923            }
1924            #[cfg(target_os = "redox")]
1925            {
1926                let [] = dir_fd.0;
1927                rustpython_host_env::posix::utimes(path.as_ref(), acc, modif)
1928                    .map_err(|err| err.into_pyexception(vm))
1929            }
1930        }
1931        #[cfg(windows)]
1932        {
1933            let [] = dir_fd.0;
1934
1935            if !_follow_symlinks.0 {
1936                return Err(vm.new_not_implemented_error(
1937                    "utime: follow_symlinks unavailable on this platform",
1938                ));
1939            }
1940
1941            crate::host_env::os::set_file_times(&path, acc, modif)
1942                .map_err(|err| OSErrorBuilder::with_filename(&err, path, vm))
1943        }
1944    }
1945
1946    #[cfg(all(any(unix, windows), not(target_os = "redox")))]
1947    #[derive(Debug)]
1948    #[pystruct_sequence_data]
1949    struct TimesResultData {
1950        pub user: f64,
1951        pub system: f64,
1952        pub children_user: f64,
1953        pub children_system: f64,
1954        pub elapsed: f64,
1955    }
1956
1957    #[cfg(all(windows, not(target_os = "redox")))]
1958    #[pyattr]
1959    #[pystruct_sequence(name = "times_result", module = "nt", data = "TimesResultData")]
1960    struct PyTimesResult;
1961
1962    #[cfg(all(unix, not(windows), not(target_os = "redox")))]
1963    #[pyattr]
1964    #[pystruct_sequence(name = "times_result", module = "posix", data = "TimesResultData")]
1965    struct PyTimesResult;
1966
1967    #[cfg(all(any(unix, windows), not(target_os = "redox")))]
1968    #[pyclass(with(PyStructSequence))]
1969    impl PyTimesResult {}
1970
1971    #[allow(
1972        clippy::unnecessary_wraps,
1973        reason = "Can return an error on some platforms"
1974    )]
1975    #[cfg(all(any(unix, windows), not(target_os = "redox")))]
1976    #[pyfunction]
1977    fn times(vm: &VirtualMachine) -> PyResult {
1978        #[cfg(windows)]
1979        {
1980            let times = crate::host_env::time::get_process_times_100ns()
1981                .ok_or_else(|| vm.new_last_os_error())?;
1982
1983            let times_result = TimesResultData {
1984                user: times.user as f64 * 1e-7,
1985                system: times.system as f64 * 1e-7,
1986                children_user: 0.0,
1987                children_system: 0.0,
1988                elapsed: 0.0,
1989            };
1990
1991            Ok(times_result.to_pyobject(vm))
1992        }
1993        #[cfg(unix)]
1994        {
1995            let times = crate::host_env::time::process_times()
1996                .map_err(|_| vm.new_os_error("Fail to get times"))?;
1997
1998            let times_result = TimesResultData {
1999                user: times.user,
2000                system: times.system,
2001                children_user: times.children_user,
2002                children_system: times.children_system,
2003                elapsed: times.elapsed,
2004            };
2005
2006            Ok(times_result.to_pyobject(vm))
2007        }
2008    }
2009
2010    #[cfg(target_os = "linux")]
2011    #[derive(FromArgs)]
2012    struct CopyFileRangeArgs<'fd> {
2013        #[pyarg(positional)]
2014        src: crt_fd::Borrowed<'fd>,
2015        #[pyarg(positional)]
2016        dst: crt_fd::Borrowed<'fd>,
2017        #[pyarg(positional)]
2018        count: i64,
2019        #[pyarg(any, default)]
2020        offset_src: Option<crt_fd::Offset>,
2021        #[pyarg(any, default)]
2022        offset_dst: Option<crt_fd::Offset>,
2023    }
2024
2025    #[cfg(target_os = "linux")]
2026    #[pyfunction]
2027    fn copy_file_range(args: CopyFileRangeArgs<'_>, vm: &VirtualMachine) -> PyResult<usize> {
2028        let count: usize = args
2029            .count
2030            .try_into()
2031            .map_err(|_| vm.new_value_error("count should >= 0"))?;
2032        let mut offset_src = args
2033            .offset_src
2034            .map(TryInto::try_into)
2035            .transpose()
2036            .map_err(|_| vm.new_value_error("offset_src should be >= 0"))?;
2037        let mut offset_dst = args
2038            .offset_dst
2039            .map(TryInto::try_into)
2040            .transpose()
2041            .map_err(|_| vm.new_value_error("offset_dst should be >= 0"))?;
2042
2043        crate::host_env::os::copy_file_range(
2044            args.src,
2045            offset_src.as_mut(),
2046            args.dst,
2047            offset_dst.as_mut(),
2048            count,
2049        )
2050        .map_err(|e| vm.new_errno_error(e.raw_os_error(), e.to_string()).upcast())
2051    }
2052
2053    #[pyfunction]
2054    fn strerror(code: i32) -> String {
2055        crate::host_env::time::strerror(code)
2056    }
2057
2058    #[pyfunction]
2059    pub(crate) fn ftruncate(fd: crt_fd::Borrowed<'_>, length: crt_fd::Offset) -> io::Result<()> {
2060        crt_fd::ftruncate(fd, length)
2061    }
2062
2063    #[derive(FromArgs)]
2064    struct TruncateArgs {
2065        #[pyarg(any)]
2066        path: PyObjectRef,
2067        #[pyarg(any)]
2068        length: crt_fd::Offset,
2069    }
2070
2071    #[pyfunction]
2072    fn truncate(args: TruncateArgs, vm: &VirtualMachine) -> PyResult<()> {
2073        let TruncateArgs { path, length } = args;
2074        match path.clone().try_into_value::<crt_fd::Borrowed<'_>>(vm) {
2075            Ok(fd) => return ftruncate(fd, length).map_err(|e| e.into_pyexception(vm)),
2076            Err(e) if e.fast_isinstance(vm.ctx.exceptions.warning) => return Err(e),
2077            Err(_) => {}
2078        }
2079
2080        #[cold]
2081        fn error(
2082            vm: &VirtualMachine,
2083            error: std::io::Error,
2084            path: OsPath,
2085        ) -> crate::builtins::PyBaseExceptionRef {
2086            OSErrorBuilder::with_filename(&error, path, vm)
2087        }
2088
2089        let path = OsPath::try_from_object(vm, path)?;
2090        // TODO: just call libc::truncate() on POSIX
2091        let f = match crate::host_env::fs::open_write(&path) {
2092            Ok(f) => f,
2093            Err(e) => return Err(error(vm, e, path)),
2094        };
2095        f.set_len(length as u64).map_err(|e| error(vm, e, path))?;
2096        drop(f);
2097        Ok(())
2098    }
2099
2100    #[cfg(all(unix, not(any(target_os = "redox", target_os = "android"))))]
2101    #[pyfunction]
2102    fn getloadavg(vm: &VirtualMachine) -> PyResult<(f64, f64, f64)> {
2103        crate::host_env::time::getloadavg()
2104            .map(Into::into)
2105            .map_err(|_| vm.new_os_error("Load averages are unobtainable"))
2106    }
2107
2108    #[cfg(unix)]
2109    #[derive(FromArgs)]
2110    struct WaitStatusArgs {
2111        #[pyarg(any)]
2112        status: i32,
2113    }
2114
2115    #[cfg(unix)]
2116    #[pyfunction]
2117    fn waitstatus_to_exitcode(status: WaitStatusArgs, vm: &VirtualMachine) -> PyResult<i32> {
2118        let status = status.status;
2119        let status = u32::try_from(status)
2120            .map_err(|_| vm.new_value_error(format!("invalid WEXITSTATUS: {status}")))?;
2121
2122        if let Some(exitcode) = crate::host_env::time::waitstatus_to_exitcode(status as libc::c_int)
2123        {
2124            return Ok(exitcode);
2125        }
2126
2127        Err(vm.new_value_error(format!("Invalid wait status: {}", status as libc::c_int)))
2128    }
2129
2130    #[cfg(windows)]
2131    #[derive(FromArgs)]
2132    struct WaitStatusArgs {
2133        #[pyarg(any)]
2134        status: PyObjectRef,
2135    }
2136
2137    #[cfg(windows)]
2138    #[pyfunction]
2139    fn waitstatus_to_exitcode(status: WaitStatusArgs, vm: &VirtualMachine) -> PyResult<u32> {
2140        let status = status.status;
2141        let status = status.try_index(vm)?.try_to_primitive_in_range::<u64>(vm)?;
2142        let exitcode = status >> 8;
2143        // ExitProcess() accepts an UINT type:
2144        // reject exit code which doesn't fit in an UINT
2145        u32::try_from(exitcode)
2146            .map_err(|_| vm.new_value_error(format!("Invalid exit code: {exitcode}")))
2147    }
2148
2149    #[derive(FromArgs)]
2150    struct DeviceEncodingArgs {
2151        #[pyarg(any)]
2152        fd: i32,
2153    }
2154
2155    #[pyfunction]
2156    fn device_encoding(fd: DeviceEncodingArgs, _vm: &VirtualMachine) -> Option<String> {
2157        let fd = fd.fd;
2158        if !isatty(fd) {
2159            return None;
2160        }
2161
2162        rustpython_host_env::os::device_encoding(fd)
2163    }
2164
2165    #[pystruct_sequence_data]
2166    #[allow(dead_code)]
2167    pub(crate) struct TerminalSizeData {
2168        pub columns: usize,
2169        pub lines: usize,
2170    }
2171
2172    #[pyattr]
2173    #[pystruct_sequence(name = "terminal_size", module = "os", data = "TerminalSizeData")]
2174    pub(crate) struct PyTerminalSize;
2175
2176    #[pyclass(with(PyStructSequence))]
2177    impl PyTerminalSize {}
2178
2179    #[derive(Debug)]
2180    #[pystruct_sequence_data]
2181    pub(crate) struct UnameResultData {
2182        pub sysname: String,
2183        pub nodename: String,
2184        pub release: String,
2185        pub version: String,
2186        pub machine: String,
2187    }
2188
2189    #[cfg(windows)]
2190    #[pyattr]
2191    #[pystruct_sequence(name = "uname_result", module = "nt", data = "UnameResultData")]
2192    pub(crate) struct PyUnameResult;
2193
2194    #[cfg(not(windows))]
2195    #[pyattr]
2196    #[pystruct_sequence(name = "uname_result", module = "posix", data = "UnameResultData")]
2197    pub(crate) struct PyUnameResult;
2198
2199    #[pyclass(with(PyStructSequence))]
2200    impl PyUnameResult {}
2201
2202    // statvfs_result: Result from statvfs or fstatvfs.
2203    // = statvfs_result_fields
2204    #[cfg(all(unix, not(target_os = "redox")))]
2205    #[derive(Debug)]
2206    #[pystruct_sequence_data]
2207    pub(crate) struct StatvfsResultData {
2208        pub f_bsize: libc::c_ulong,     // filesystem block size
2209        pub f_frsize: libc::c_ulong,    // fragment size
2210        pub f_blocks: libc::fsblkcnt_t, // size of fs in f_frsize units
2211        pub f_bfree: libc::fsblkcnt_t,  // free blocks
2212        pub f_bavail: libc::fsblkcnt_t, // free blocks for unprivileged users
2213        pub f_files: libc::fsfilcnt_t,  // inodes
2214        pub f_ffree: libc::fsfilcnt_t,  // free inodes
2215        pub f_favail: libc::fsfilcnt_t, // free inodes for unprivileged users
2216        pub f_flag: libc::c_ulong,      // mount flags
2217        pub f_namemax: libc::c_ulong,   // maximum filename length
2218        #[pystruct_sequence(skip)]
2219        pub f_fsid: libc::c_ulong, // filesystem ID (not in tuple but accessible as attribute)
2220    }
2221
2222    #[cfg(all(unix, not(target_os = "redox")))]
2223    #[pyattr]
2224    #[pystruct_sequence(name = "statvfs_result", module = "os", data = "StatvfsResultData")]
2225    pub(crate) struct PyStatvfsResult;
2226
2227    #[cfg(all(unix, not(target_os = "redox")))]
2228    #[pyclass(with(PyStructSequence))]
2229    impl PyStatvfsResult {
2230        #[pyslot]
2231        fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
2232            crate::types::struct_sequence_new(
2233                cls,
2234                args.bind_for(vm, Self::NAME)?,
2235                StatvfsResultData::OPTIONAL_FIELD_NAMES,
2236                vm,
2237            )
2238        }
2239    }
2240
2241    #[cfg(all(unix, not(target_os = "redox")))]
2242    impl StatvfsResultData {
2243        fn from_statvfs(st: crate::host_env::posix::StatVfsInfo) -> Self {
2244            Self {
2245                f_bsize: st.f_bsize,
2246                f_frsize: st.f_frsize,
2247                f_blocks: st.f_blocks,
2248                f_bfree: st.f_bfree,
2249                f_bavail: st.f_bavail,
2250                f_files: st.f_files,
2251                f_ffree: st.f_ffree,
2252                f_favail: st.f_favail,
2253                f_flag: st.f_flag,
2254                f_namemax: st.f_namemax,
2255                f_fsid: st.f_fsid,
2256            }
2257        }
2258    }
2259
2260    #[cfg(all(unix, not(target_os = "redox")))]
2261    #[derive(FromArgs)]
2262    struct StatvfsArgs<'a> {
2263        #[pyarg(any)]
2264        path: OsPathOrFd<'a>,
2265    }
2266
2267    #[cfg(all(unix, not(target_os = "redox")))]
2268    fn statvfs_inner(path: OsPathOrFd<'_>, vm: &VirtualMachine) -> PyResult {
2269        let st = match &path {
2270            OsPathOrFd::Path(p) => {
2271                let cpath = p.clone().into_cstring(vm)?;
2272                crate::host_env::posix::statvfs_path(cpath.as_c_str())
2273            }
2274            OsPathOrFd::Fd(fd) => crate::host_env::posix::statvfs_fd(fd.as_raw()),
2275        };
2276        if let Err(err) = st {
2277            return Err(OSErrorBuilder::with_filename(&err, path, vm));
2278        }
2279        Ok(StatvfsResultData::from_statvfs(st.unwrap()).to_pyobject(vm))
2280    }
2281
2282    #[cfg(all(unix, not(target_os = "redox")))]
2283    #[pyfunction]
2284    fn statvfs(args: StatvfsArgs<'_>, vm: &VirtualMachine) -> PyResult {
2285        statvfs_inner(args.path, vm)
2286    }
2287
2288    #[cfg(all(unix, not(target_os = "redox")))]
2289    #[pyfunction]
2290    fn fstatvfs(fd: crt_fd::Borrowed<'_>, vm: &VirtualMachine) -> PyResult {
2291        statvfs_inner(OsPathOrFd::Fd(fd), vm)
2292    }
2293
2294    pub(super) fn support_funcs() -> Vec<SupportFunc> {
2295        let mut supports = super::platform::module::support_funcs();
2296        supports.extend(vec![
2297            SupportFunc::new("open", Some(false), Some(OPEN_DIR_FD), Some(false)),
2298            SupportFunc::new("access", Some(false), Some(false), None),
2299            SupportFunc::new("chdir", None, Some(false), Some(false)),
2300            // chflags Some, None Some
2301            SupportFunc::new("link", Some(false), Some(false), Some(cfg!(unix))),
2302            SupportFunc::new("listdir", Some(LISTDIR_FD), Some(false), Some(false)),
2303            SupportFunc::new("mkdir", Some(false), Some(MKDIR_DIR_FD), Some(false)),
2304            // mkfifo Some Some None
2305            // mknod Some Some None
2306            SupportFunc::new("readlink", Some(false), None, Some(false)),
2307            SupportFunc::new("remove", Some(false), Some(UNLINK_DIR_FD), Some(false)),
2308            SupportFunc::new("unlink", Some(false), Some(UNLINK_DIR_FD), Some(false)),
2309            SupportFunc::new("rename", Some(false), Some(RENAME_DIR_FD), Some(false)),
2310            SupportFunc::new("replace", Some(false), Some(RENAME_DIR_FD), Some(false)), // TODO: Fix replace
2311            SupportFunc::new("rmdir", Some(false), Some(RMDIR_DIR_FD), Some(false)),
2312            SupportFunc::new("scandir", Some(SCANDIR_FD), Some(false), Some(false)),
2313            SupportFunc::new("stat", Some(true), Some(STAT_DIR_FD), Some(true)),
2314            SupportFunc::new("fstat", Some(false), Some(false), Some(false)),
2315            SupportFunc::new("symlink", Some(false), Some(SYMLINK_DIR_FD), Some(false)),
2316            SupportFunc::new("truncate", Some(true), Some(false), Some(false)),
2317            SupportFunc::new("ftruncate", Some(true), Some(false), Some(false)),
2318            SupportFunc::new("fsync", Some(true), Some(false), Some(false)),
2319            SupportFunc::new(
2320                "utime",
2321                Some(false),
2322                Some(UTIME_DIR_FD),
2323                Some(cfg!(all(unix, not(target_os = "redox")))),
2324            ),
2325        ]);
2326        supports
2327    }
2328}
2329pub(crate) use _os::ftruncate;
2330
2331pub(crate) struct SupportFunc {
2332    name: &'static str,
2333    // realistically, each of these is just a bool of "is this function in the supports_* set".
2334    // However, None marks that the function maybe _should_ support fd/dir_fd/follow_symlinks, but
2335    // we haven't implemented it yet.
2336    fd: Option<bool>,
2337    dir_fd: Option<bool>,
2338    follow_symlinks: Option<bool>,
2339}
2340
2341impl SupportFunc {
2342    pub(crate) const fn new(
2343        name: &'static str,
2344        fd: Option<bool>,
2345        dir_fd: Option<bool>,
2346        follow_symlinks: Option<bool>,
2347    ) -> Self {
2348        Self {
2349            name,
2350            fd,
2351            dir_fd,
2352            follow_symlinks,
2353        }
2354    }
2355}
2356
2357pub fn module_exec(vm: &VirtualMachine, module: &Py<PyModule>) -> PyResult<()> {
2358    let support_funcs = _os::support_funcs();
2359    let supports_fd = PySet::default().into_ref(&vm.ctx);
2360    let supports_dir_fd = PySet::default().into_ref(&vm.ctx);
2361    let supports_follow_symlinks = PySet::default().into_ref(&vm.ctx);
2362    for support in support_funcs {
2363        let func_obj = module.get_attr(support.name, vm)?;
2364        if support.fd.unwrap_or(false) {
2365            supports_fd.clone().add(func_obj.clone(), vm)?;
2366        }
2367        if support.dir_fd.unwrap_or(false) {
2368            supports_dir_fd.clone().add(func_obj.clone(), vm)?;
2369        }
2370        if support.follow_symlinks.unwrap_or(false) {
2371            supports_follow_symlinks.clone().add(func_obj, vm)?;
2372        }
2373    }
2374
2375    extend_module!(vm, module, {
2376        "supports_fd" => supports_fd,
2377        "supports_dir_fd" => supports_dir_fd,
2378        "supports_follow_symlinks" => supports_follow_symlinks,
2379        "error" => vm.ctx.exceptions.os_error.to_owned(),
2380    });
2381
2382    Ok(())
2383}
2384
2385/// Convert a mapping (e.g. os._Environ) to a plain dict for use by execve/posix_spawn.
2386///
2387/// For `os._Environ`, accesses the internal `_data` dict directly at the Rust level.
2388/// This avoids Python-level method calls that can deadlock after fork() when
2389/// parking_lot locks are held by threads that no longer exist.
2390#[cfg(any(unix, windows))]
2391pub(crate) fn envobj_to_dict(
2392    env: crate::function::ArgMapping,
2393    vm: &VirtualMachine,
2394) -> PyResult<crate::builtins::PyDictRef> {
2395    let obj = env.obj();
2396    if let Some(dict) = obj.downcast_ref_if_exact::<crate::builtins::PyDict>(vm) {
2397        return Ok(dict.to_owned());
2398    }
2399    if let Some(inst_dict) = obj.dict()
2400        && let Ok(Some(data)) = inst_dict.get_item_opt("_data", vm)
2401        && let Some(dict) = data.downcast_ref_if_exact::<crate::builtins::PyDict>(vm)
2402    {
2403        return Ok(dict.to_owned());
2404    }
2405    let keys = vm.call_method(obj, "keys", ())?;
2406    let dict = vm.ctx.new_dict();
2407    for key in keys.get_iter(vm)?.into_iter::<PyObjectRef>(vm) {
2408        let key = key?;
2409        let val = obj.get_item(&*key, vm)?;
2410        dict.set_item(&*key, val, vm)?;
2411    }
2412    Ok(dict)
2413}
2414
2415#[cfg(not(windows))]
2416use super::posix as platform;
2417
2418#[cfg(windows)]
2419use super::nt as platform;
2420
2421pub(crate) use platform::module::MODULE_NAME;