rustpython_vm/stdlib/
os.rs

1use crate::{
2    builtins::{PyBaseExceptionRef, PyModule, PySet},
3    common::crt_fd::Fd,
4    convert::ToPyException,
5    function::{ArgumentError, FromArgs, FuncArgs},
6    AsObject, Py, PyPayload, PyResult, VirtualMachine,
7};
8use std::{ffi, fs, io, path::Path};
9
10pub(crate) fn fs_metadata<P: AsRef<Path>>(
11    path: P,
12    follow_symlink: bool,
13) -> io::Result<fs::Metadata> {
14    if follow_symlink {
15        fs::metadata(path.as_ref())
16    } else {
17        fs::symlink_metadata(path.as_ref())
18    }
19}
20
21#[cfg(unix)]
22impl crate::convert::IntoPyException for nix::Error {
23    fn into_pyexception(self, vm: &VirtualMachine) -> PyBaseExceptionRef {
24        io::Error::from(self).into_pyexception(vm)
25    }
26}
27
28/// Convert the error stored in the `errno` variable into an Exception
29#[inline]
30pub fn errno_err(vm: &VirtualMachine) -> PyBaseExceptionRef {
31    crate::common::os::last_os_error().to_pyexception(vm)
32}
33
34#[allow(dead_code)]
35#[derive(FromArgs, Default)]
36pub struct TargetIsDirectory {
37    #[pyarg(any, default = "false")]
38    pub(crate) target_is_directory: bool,
39}
40
41cfg_if::cfg_if! {
42    if #[cfg(all(any(unix, target_os = "wasi"), not(target_os = "redox")))] {
43        use libc::AT_FDCWD;
44    } else {
45        const AT_FDCWD: i32 = -100;
46    }
47}
48const DEFAULT_DIR_FD: Fd = Fd(AT_FDCWD);
49
50// XXX: AVAILABLE should be a bool, but we can't yet have it as a bool and just cast it to usize
51#[derive(Copy, Clone, PartialEq, Eq)]
52pub struct DirFd<const AVAILABLE: usize>(pub(crate) [Fd; AVAILABLE]);
53
54impl<const AVAILABLE: usize> Default for DirFd<AVAILABLE> {
55    fn default() -> Self {
56        Self([DEFAULT_DIR_FD; AVAILABLE])
57    }
58}
59
60// not used on all platforms
61#[allow(unused)]
62impl DirFd<1> {
63    #[inline(always)]
64    pub(crate) fn fd_opt(&self) -> Option<Fd> {
65        self.get_opt().map(Fd)
66    }
67
68    #[inline]
69    pub(crate) fn get_opt(&self) -> Option<i32> {
70        let fd = self.fd();
71        if fd == DEFAULT_DIR_FD {
72            None
73        } else {
74            Some(fd.0)
75        }
76    }
77
78    #[inline(always)]
79    pub(crate) fn fd(&self) -> Fd {
80        self.0[0]
81    }
82}
83
84impl<const AVAILABLE: usize> FromArgs for DirFd<AVAILABLE> {
85    fn from_args(vm: &VirtualMachine, args: &mut FuncArgs) -> Result<Self, ArgumentError> {
86        let fd = match args.take_keyword("dir_fd") {
87            Some(o) if vm.is_none(&o) => DEFAULT_DIR_FD,
88            None => DEFAULT_DIR_FD,
89            Some(o) => {
90                let fd = o.try_index_opt(vm).unwrap_or_else(|| {
91                    Err(vm.new_type_error(format!(
92                        "argument should be integer or None, not {}",
93                        o.class().name()
94                    )))
95                })?;
96                let fd = fd.try_to_primitive(vm)?;
97                Fd(fd)
98            }
99        };
100        if AVAILABLE == 0 && fd != DEFAULT_DIR_FD {
101            return Err(vm
102                .new_not_implemented_error("dir_fd unavailable on this platform".to_owned())
103                .into());
104        }
105        Ok(Self([fd; AVAILABLE]))
106    }
107}
108
109#[derive(FromArgs)]
110pub(super) struct FollowSymlinks(
111    #[pyarg(named, name = "follow_symlinks", default = "true")] pub bool,
112);
113
114fn bytes_as_osstr<'a>(b: &'a [u8], vm: &VirtualMachine) -> PyResult<&'a ffi::OsStr> {
115    rustpython_common::os::bytes_as_osstr(b)
116        .map_err(|_| vm.new_unicode_decode_error("can't decode path for utf-8".to_owned()))
117}
118
119#[pymodule(sub)]
120pub(super) mod _os {
121    use super::{errno_err, DirFd, FollowSymlinks, SupportFunc};
122    use crate::{
123        builtins::{
124            PyBytesRef, PyGenericAlias, PyIntRef, PyStrRef, PyTuple, PyTupleRef, PyTypeRef,
125        },
126        common::{
127            crt_fd::{Fd, Offset},
128            fileutils::StatStruct,
129            lock::{OnceCell, PyRwLock},
130            suppress_iph,
131        },
132        convert::{IntoPyException, ToPyObject},
133        function::{ArgBytesLike, Either, FsPath, FuncArgs, OptionalArg},
134        ospath::{IOErrorBuilder, OsPath, OsPathOrFd, OutputMode},
135        protocol::PyIterReturn,
136        recursion::ReprGuard,
137        types::{IterNext, Iterable, PyStructSequence, Representable, SelfIter},
138        vm::VirtualMachine,
139        AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject,
140    };
141    use crossbeam_utils::atomic::AtomicCell;
142    use itertools::Itertools;
143    use std::{
144        env, ffi, fs,
145        fs::OpenOptions,
146        io::{self, Read, Write},
147        path::PathBuf,
148        time::{Duration, SystemTime},
149    };
150
151    const OPEN_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox")));
152    pub(crate) const MKDIR_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox")));
153    const STAT_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox")));
154    const UTIME_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox")));
155    pub(crate) const SYMLINK_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox")));
156
157    #[pyattr]
158    use libc::{
159        O_APPEND, O_CREAT, O_EXCL, O_RDONLY, O_RDWR, O_TRUNC, O_WRONLY, SEEK_CUR, SEEK_END,
160        SEEK_SET,
161    };
162
163    #[pyattr]
164    pub(crate) const F_OK: u8 = 0;
165    #[pyattr]
166    pub(crate) const R_OK: u8 = 1 << 2;
167    #[pyattr]
168    pub(crate) const W_OK: u8 = 1 << 1;
169    #[pyattr]
170    pub(crate) const X_OK: u8 = 1 << 0;
171
172    #[pyfunction]
173    fn close(fileno: i32, vm: &VirtualMachine) -> PyResult<()> {
174        Fd(fileno).close().map_err(|e| e.into_pyexception(vm))
175    }
176
177    #[pyfunction]
178    fn closerange(fd_low: i32, fd_high: i32) {
179        for fileno in fd_low..fd_high {
180            let _ = Fd(fileno).close();
181        }
182    }
183
184    #[cfg(any(unix, windows, target_os = "wasi"))]
185    #[derive(FromArgs)]
186    struct OpenArgs {
187        path: OsPath,
188        flags: i32,
189        #[pyarg(any, default)]
190        mode: Option<i32>,
191        #[pyarg(flatten)]
192        dir_fd: DirFd<{ OPEN_DIR_FD as usize }>,
193    }
194
195    #[pyfunction]
196    fn open(args: OpenArgs, vm: &VirtualMachine) -> PyResult<i32> {
197        os_open(args.path, args.flags, args.mode, args.dir_fd, vm)
198    }
199
200    #[cfg(any(unix, windows, target_os = "wasi"))]
201    pub(crate) fn os_open(
202        name: OsPath,
203        flags: i32,
204        mode: Option<i32>,
205        dir_fd: DirFd<{ OPEN_DIR_FD as usize }>,
206        vm: &VirtualMachine,
207    ) -> PyResult<i32> {
208        let mode = mode.unwrap_or(0o777);
209        #[cfg(windows)]
210        let fd = {
211            let [] = dir_fd.0;
212            let name = name.to_widecstring(vm)?;
213            let flags = flags | libc::O_NOINHERIT;
214            Fd::wopen(&name, flags, mode)
215        };
216        #[cfg(not(windows))]
217        let fd = {
218            let name = name.clone().into_cstring(vm)?;
219            #[cfg(not(target_os = "wasi"))]
220            let flags = flags | libc::O_CLOEXEC;
221            #[cfg(not(target_os = "redox"))]
222            if let Some(dir_fd) = dir_fd.fd_opt() {
223                dir_fd.openat(&name, flags, mode)
224            } else {
225                Fd::open(&name, flags, mode)
226            }
227            #[cfg(target_os = "redox")]
228            {
229                let [] = dir_fd.0;
230                Fd::open(&name, flags, mode)
231            }
232        };
233        fd.map(|fd| fd.0)
234            .map_err(|err| IOErrorBuilder::with_filename(&err, name, vm))
235    }
236
237    #[pyfunction]
238    fn fsync(fd: i32, vm: &VirtualMachine) -> PyResult<()> {
239        Fd(fd).fsync().map_err(|err| err.into_pyexception(vm))
240    }
241
242    #[pyfunction]
243    fn read(fd: i32, n: usize, vm: &VirtualMachine) -> PyResult<PyBytesRef> {
244        let mut buffer = vec![0u8; n];
245        let mut file = Fd(fd);
246        let n = file
247            .read(&mut buffer)
248            .map_err(|err| err.into_pyexception(vm))?;
249        buffer.truncate(n);
250
251        Ok(vm.ctx.new_bytes(buffer))
252    }
253
254    #[pyfunction]
255    fn write(fd: i32, data: ArgBytesLike, vm: &VirtualMachine) -> PyResult {
256        let mut file = Fd(fd);
257        let written = data
258            .with_ref(|b| file.write(b))
259            .map_err(|err| err.into_pyexception(vm))?;
260
261        Ok(vm.ctx.new_int(written).into())
262    }
263
264    #[pyfunction]
265    #[pyfunction(name = "unlink")]
266    fn remove(path: OsPath, dir_fd: DirFd<0>, vm: &VirtualMachine) -> PyResult<()> {
267        let [] = dir_fd.0;
268        let is_junction = cfg!(windows)
269            && fs::metadata(&path).map_or(false, |meta| meta.file_type().is_dir())
270            && fs::symlink_metadata(&path).map_or(false, |meta| meta.file_type().is_symlink());
271        let res = if is_junction {
272            fs::remove_dir(&path)
273        } else {
274            fs::remove_file(&path)
275        };
276        res.map_err(|err| IOErrorBuilder::with_filename(&err, path, vm))
277    }
278
279    #[cfg(not(windows))]
280    #[pyfunction]
281    fn mkdir(
282        path: OsPath,
283        mode: OptionalArg<i32>,
284        dir_fd: DirFd<{ MKDIR_DIR_FD as usize }>,
285        vm: &VirtualMachine,
286    ) -> PyResult<()> {
287        let mode = mode.unwrap_or(0o777);
288        let path = path.into_cstring(vm)?;
289        #[cfg(not(target_os = "redox"))]
290        if let Some(fd) = dir_fd.get_opt() {
291            let res = unsafe { libc::mkdirat(fd, path.as_ptr(), mode as _) };
292            let res = if res < 0 { Err(errno_err(vm)) } else { Ok(()) };
293            return res;
294        }
295        #[cfg(target_os = "redox")]
296        let [] = dir_fd.0;
297        let res = unsafe { libc::mkdir(path.as_ptr(), mode as _) };
298        if res < 0 {
299            Err(errno_err(vm))
300        } else {
301            Ok(())
302        }
303    }
304
305    #[pyfunction]
306    fn mkdirs(path: PyStrRef, vm: &VirtualMachine) -> PyResult<()> {
307        fs::create_dir_all(path.as_str()).map_err(|err| err.into_pyexception(vm))
308    }
309
310    #[pyfunction]
311    fn rmdir(path: OsPath, dir_fd: DirFd<0>, vm: &VirtualMachine) -> PyResult<()> {
312        let [] = dir_fd.0;
313        fs::remove_dir(&path).map_err(|err| IOErrorBuilder::with_filename(&err, path, vm))
314    }
315
316    const LISTDIR_FD: bool = cfg!(all(unix, not(target_os = "redox")));
317
318    #[pyfunction]
319    fn listdir(path: OptionalArg<OsPathOrFd>, vm: &VirtualMachine) -> PyResult<Vec<PyObjectRef>> {
320        let path = path.unwrap_or_else(|| OsPathOrFd::Path(OsPath::new_str(".")));
321        let list = match path {
322            OsPathOrFd::Path(path) => {
323                let dir_iter = match fs::read_dir(&path) {
324                    Ok(iter) => iter,
325                    Err(err) => {
326                        return Err(IOErrorBuilder::with_filename(&err, path, vm));
327                    }
328                };
329                dir_iter
330                    .map(|entry| match entry {
331                        Ok(entry_path) => path.mode.process_path(entry_path.file_name(), vm),
332                        Err(err) => Err(IOErrorBuilder::with_filename(&err, path.clone(), vm)),
333                    })
334                    .collect::<PyResult<_>>()?
335            }
336            OsPathOrFd::Fd(fno) => {
337                #[cfg(not(all(unix, not(target_os = "redox"))))]
338                {
339                    let _ = fno;
340                    return Err(vm.new_not_implemented_error(
341                        "can't pass fd to listdir on this platform".to_owned(),
342                    ));
343                }
344                #[cfg(all(unix, not(target_os = "redox")))]
345                {
346                    use rustpython_common::os::ffi::OsStrExt;
347                    let new_fd = nix::unistd::dup(fno).map_err(|e| e.into_pyexception(vm))?;
348                    let mut dir =
349                        nix::dir::Dir::from_fd(new_fd).map_err(|e| e.into_pyexception(vm))?;
350                    dir.iter()
351                        .filter_map(|entry| {
352                            entry
353                                .map_err(|e| e.into_pyexception(vm))
354                                .and_then(|entry| {
355                                    let fname = entry.file_name().to_bytes();
356                                    Ok(match fname {
357                                        b"." | b".." => None,
358                                        _ => Some(
359                                            OutputMode::String
360                                                .process_path(ffi::OsStr::from_bytes(fname), vm)?,
361                                        ),
362                                    })
363                                })
364                                .transpose()
365                        })
366                        .collect::<PyResult<_>>()?
367                }
368            }
369        };
370        Ok(list)
371    }
372
373    fn env_bytes_as_bytes(obj: &Either<PyStrRef, PyBytesRef>) -> &[u8] {
374        match obj {
375            Either::A(ref s) => s.as_str().as_bytes(),
376            Either::B(ref b) => b.as_bytes(),
377        }
378    }
379
380    #[pyfunction]
381    fn putenv(
382        key: Either<PyStrRef, PyBytesRef>,
383        value: Either<PyStrRef, PyBytesRef>,
384        vm: &VirtualMachine,
385    ) -> PyResult<()> {
386        let key = env_bytes_as_bytes(&key);
387        let value = env_bytes_as_bytes(&value);
388        if key.contains(&b'\0') || value.contains(&b'\0') {
389            return Err(vm.new_value_error("embedded null byte".to_string()));
390        }
391        if key.is_empty() || key.contains(&b'=') {
392            return Err(vm.new_value_error("illegal environment variable name".to_string()));
393        }
394        let key = super::bytes_as_osstr(key, vm)?;
395        let value = super::bytes_as_osstr(value, vm)?;
396        env::set_var(key, value);
397        Ok(())
398    }
399
400    #[pyfunction]
401    fn unsetenv(key: Either<PyStrRef, PyBytesRef>, vm: &VirtualMachine) -> PyResult<()> {
402        let key = env_bytes_as_bytes(&key);
403        if key.contains(&b'\0') {
404            return Err(vm.new_value_error("embedded null byte".to_string()));
405        }
406        if key.is_empty() || key.contains(&b'=') {
407            return Err(vm.new_errno_error(
408                22,
409                format!(
410                    "Invalid argument: {}",
411                    std::str::from_utf8(key).unwrap_or("<bytes encoding failure>")
412                ),
413            ));
414        }
415        let key = super::bytes_as_osstr(key, vm)?;
416        env::remove_var(key);
417        Ok(())
418    }
419
420    #[pyfunction]
421    fn readlink(path: OsPath, dir_fd: DirFd<0>, vm: &VirtualMachine) -> PyResult {
422        let mode = path.mode;
423        let [] = dir_fd.0;
424        let path =
425            fs::read_link(&path).map_err(|err| IOErrorBuilder::with_filename(&err, path, vm))?;
426        mode.process_path(path, vm)
427    }
428
429    #[pyattr]
430    #[pyclass(name)]
431    #[derive(Debug, PyPayload)]
432    struct DirEntry {
433        file_name: std::ffi::OsString,
434        pathval: PathBuf,
435        file_type: io::Result<fs::FileType>,
436        mode: OutputMode,
437        stat: OnceCell<PyObjectRef>,
438        lstat: OnceCell<PyObjectRef>,
439        #[cfg(unix)]
440        ino: AtomicCell<u64>,
441        #[cfg(not(unix))]
442        ino: AtomicCell<Option<u64>>,
443    }
444
445    #[pyclass(with(Representable))]
446    impl DirEntry {
447        #[pygetset]
448        fn name(&self, vm: &VirtualMachine) -> PyResult {
449            self.mode.process_path(&self.file_name, vm)
450        }
451
452        #[pygetset]
453        fn path(&self, vm: &VirtualMachine) -> PyResult {
454            self.mode.process_path(&self.pathval, vm)
455        }
456
457        fn perform_on_metadata(
458            &self,
459            follow_symlinks: FollowSymlinks,
460            action: fn(fs::Metadata) -> bool,
461            vm: &VirtualMachine,
462        ) -> PyResult<bool> {
463            match super::fs_metadata(&self.pathval, follow_symlinks.0) {
464                Ok(meta) => Ok(action(meta)),
465                Err(e) => {
466                    // FileNotFoundError is caught and not raised
467                    if e.kind() == io::ErrorKind::NotFound {
468                        Ok(false)
469                    } else {
470                        Err(e.into_pyexception(vm))
471                    }
472                }
473            }
474        }
475
476        #[pymethod]
477        fn is_dir(&self, follow_symlinks: FollowSymlinks, vm: &VirtualMachine) -> PyResult<bool> {
478            self.perform_on_metadata(
479                follow_symlinks,
480                |meta: fs::Metadata| -> bool { meta.is_dir() },
481                vm,
482            )
483        }
484
485        #[pymethod]
486        fn is_file(&self, follow_symlinks: FollowSymlinks, vm: &VirtualMachine) -> PyResult<bool> {
487            self.perform_on_metadata(
488                follow_symlinks,
489                |meta: fs::Metadata| -> bool { meta.is_file() },
490                vm,
491            )
492        }
493
494        #[pymethod]
495        fn is_symlink(&self, vm: &VirtualMachine) -> PyResult<bool> {
496            Ok(self
497                .file_type
498                .as_ref()
499                .map_err(|err| err.into_pyexception(vm))?
500                .is_symlink())
501        }
502
503        #[pymethod]
504        fn stat(
505            &self,
506            dir_fd: DirFd<{ STAT_DIR_FD as usize }>,
507            follow_symlinks: FollowSymlinks,
508            vm: &VirtualMachine,
509        ) -> PyResult {
510            let do_stat = |follow_symlinks| {
511                stat(
512                    OsPath {
513                        path: self.pathval.as_os_str().to_owned(),
514                        mode: OutputMode::String,
515                    }
516                    .into(),
517                    dir_fd,
518                    FollowSymlinks(follow_symlinks),
519                    vm,
520                )
521            };
522            let lstat = || self.lstat.get_or_try_init(|| do_stat(false));
523            let stat = if follow_symlinks.0 {
524                // if follow_symlinks == true and we aren't a symlink, cache both stat and lstat
525                self.stat.get_or_try_init(|| {
526                    if self.is_symlink(vm)? {
527                        do_stat(true)
528                    } else {
529                        lstat().cloned()
530                    }
531                })?
532            } else {
533                lstat()?
534            };
535            Ok(stat.clone())
536        }
537
538        #[cfg(not(unix))]
539        #[pymethod]
540        fn inode(&self, vm: &VirtualMachine) -> PyResult<u64> {
541            match self.ino.load() {
542                Some(ino) => Ok(ino),
543                None => {
544                    let stat = stat_inner(
545                        OsPath {
546                            path: self.pathval.as_os_str().to_owned(),
547                            mode: OutputMode::String,
548                        }
549                        .into(),
550                        DirFd::default(),
551                        FollowSymlinks(false),
552                    )
553                    .map_err(|e| e.into_pyexception(vm))?
554                    .ok_or_else(|| crate::exceptions::cstring_error(vm))?;
555                    // Err(T) means other thread set `ino` at the mean time which is safe to ignore
556                    let _ = self.ino.compare_exchange(None, Some(stat.st_ino));
557                    Ok(stat.st_ino)
558                }
559            }
560        }
561
562        #[cfg(unix)]
563        #[pymethod]
564        fn inode(&self, _vm: &VirtualMachine) -> PyResult<u64> {
565            Ok(self.ino.load())
566        }
567
568        #[cfg(not(windows))]
569        #[pymethod]
570        fn is_junction(&self, _vm: &VirtualMachine) -> PyResult<bool> {
571            Ok(false)
572        }
573
574        #[cfg(windows)]
575        #[pymethod]
576        fn is_junction(&self, _vm: &VirtualMachine) -> PyResult<bool> {
577            Ok(junction::exists(self.pathval.clone()).unwrap_or(false))
578        }
579
580        #[pymethod(magic)]
581        fn fspath(&self, vm: &VirtualMachine) -> PyResult {
582            self.path(vm)
583        }
584
585        #[pyclassmethod(magic)]
586        fn class_getitem(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias {
587            PyGenericAlias::new(cls, args, vm)
588        }
589    }
590
591    impl Representable for DirEntry {
592        #[inline]
593        fn repr_str(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<String> {
594            let name = match zelf.as_object().get_attr("name", vm) {
595                Ok(name) => Some(name),
596                Err(e)
597                    if e.fast_isinstance(vm.ctx.exceptions.attribute_error)
598                        || e.fast_isinstance(vm.ctx.exceptions.value_error) =>
599                {
600                    None
601                }
602                Err(e) => return Err(e),
603            };
604            if let Some(name) = name {
605                if let Some(_guard) = ReprGuard::enter(vm, zelf.as_object()) {
606                    let repr = name.repr(vm)?;
607                    Ok(format!("<{} {}>", zelf.class(), repr))
608                } else {
609                    Err(vm.new_runtime_error(format!(
610                        "reentrant call inside {}.__repr__",
611                        zelf.class()
612                    )))
613                }
614            } else {
615                Ok(format!("<{}>", zelf.class()))
616            }
617        }
618    }
619
620    #[pyattr]
621    #[pyclass(name = "ScandirIter")]
622    #[derive(Debug, PyPayload)]
623    struct ScandirIterator {
624        entries: PyRwLock<Option<fs::ReadDir>>,
625        mode: OutputMode,
626    }
627
628    #[pyclass(with(IterNext, Iterable))]
629    impl ScandirIterator {
630        #[pymethod]
631        fn close(&self) {
632            let entryref: &mut Option<fs::ReadDir> = &mut self.entries.write();
633            let _dropped = entryref.take();
634        }
635
636        #[pymethod(magic)]
637        fn enter(zelf: PyRef<Self>) -> PyRef<Self> {
638            zelf
639        }
640
641        #[pymethod(magic)]
642        fn exit(zelf: PyRef<Self>, _args: FuncArgs) {
643            zelf.close()
644        }
645    }
646    impl SelfIter for ScandirIterator {}
647    impl IterNext for ScandirIterator {
648        fn next(zelf: &crate::Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
649            let entryref: &mut Option<fs::ReadDir> = &mut zelf.entries.write();
650
651            match entryref {
652                None => Ok(PyIterReturn::StopIteration(None)),
653                Some(inner) => match inner.next() {
654                    Some(entry) => match entry {
655                        Ok(entry) => {
656                            #[cfg(unix)]
657                            let ino = {
658                                use std::os::unix::fs::DirEntryExt;
659                                entry.ino()
660                            };
661                            #[cfg(not(unix))]
662                            let ino = None;
663
664                            Ok(PyIterReturn::Return(
665                                DirEntry {
666                                    file_name: entry.file_name(),
667                                    pathval: entry.path(),
668                                    file_type: entry.file_type(),
669                                    mode: zelf.mode,
670                                    lstat: OnceCell::new(),
671                                    stat: OnceCell::new(),
672                                    ino: AtomicCell::new(ino),
673                                }
674                                .into_ref(&vm.ctx)
675                                .into(),
676                            ))
677                        }
678                        Err(err) => Err(err.into_pyexception(vm)),
679                    },
680                    None => {
681                        let _dropped = entryref.take();
682                        Ok(PyIterReturn::StopIteration(None))
683                    }
684                },
685            }
686        }
687    }
688
689    #[pyfunction]
690    fn scandir(path: OptionalArg<OsPath>, vm: &VirtualMachine) -> PyResult {
691        let path = path.unwrap_or_else(|| OsPath::new_str("."));
692        let entries = fs::read_dir(&path.path)
693            .map_err(|err| IOErrorBuilder::with_filename(&err, path.clone(), vm))?;
694        Ok(ScandirIterator {
695            entries: PyRwLock::new(Some(entries)),
696            mode: path.mode,
697        }
698        .into_ref(&vm.ctx)
699        .into())
700    }
701
702    #[pyattr]
703    #[pyclass(module = "os", name = "stat_result")]
704    #[derive(Debug, PyStructSequence, FromArgs)]
705    struct StatResult {
706        pub st_mode: PyIntRef,
707        pub st_ino: PyIntRef,
708        pub st_dev: PyIntRef,
709        pub st_nlink: PyIntRef,
710        pub st_uid: PyIntRef,
711        pub st_gid: PyIntRef,
712        pub st_size: PyIntRef,
713        // TODO: unnamed structsequence fields
714        #[pyarg(positional, default)]
715        pub __st_atime_int: libc::time_t,
716        #[pyarg(positional, default)]
717        pub __st_mtime_int: libc::time_t,
718        #[pyarg(positional, default)]
719        pub __st_ctime_int: libc::time_t,
720        #[pyarg(any, default)]
721        pub st_atime: f64,
722        #[pyarg(any, default)]
723        pub st_mtime: f64,
724        #[pyarg(any, default)]
725        pub st_ctime: f64,
726        #[pyarg(any, default)]
727        pub st_atime_ns: i128,
728        #[pyarg(any, default)]
729        pub st_mtime_ns: i128,
730        #[pyarg(any, default)]
731        pub st_ctime_ns: i128,
732        #[pyarg(any, default)]
733        pub st_reparse_tag: u32,
734    }
735
736    #[pyclass(with(PyStructSequence))]
737    impl StatResult {
738        fn from_stat(stat: &StatStruct, vm: &VirtualMachine) -> Self {
739            let (atime, mtime, ctime);
740            #[cfg(any(unix, windows))]
741            #[cfg(not(target_os = "netbsd"))]
742            {
743                atime = (stat.st_atime, stat.st_atime_nsec);
744                mtime = (stat.st_mtime, stat.st_mtime_nsec);
745                ctime = (stat.st_ctime, stat.st_ctime_nsec);
746            }
747            #[cfg(target_os = "netbsd")]
748            {
749                atime = (stat.st_atime, stat.st_atimensec);
750                mtime = (stat.st_mtime, stat.st_mtimensec);
751                ctime = (stat.st_ctime, stat.st_ctimensec);
752            }
753            #[cfg(target_os = "wasi")]
754            {
755                atime = (stat.st_atim.tv_sec, stat.st_atim.tv_nsec);
756                mtime = (stat.st_mtim.tv_sec, stat.st_mtim.tv_nsec);
757                ctime = (stat.st_ctim.tv_sec, stat.st_ctim.tv_nsec);
758            }
759
760            const NANOS_PER_SEC: u32 = 1_000_000_000;
761            let to_f64 = |(s, ns)| (s as f64) + (ns as f64) / (NANOS_PER_SEC as f64);
762            let to_ns = |(s, ns)| s as i128 * NANOS_PER_SEC as i128 + ns as i128;
763
764            #[cfg(windows)]
765            let st_reparse_tag = stat.st_reparse_tag;
766            #[cfg(not(windows))]
767            let st_reparse_tag = 0;
768
769            StatResult {
770                st_mode: vm.ctx.new_pyref(stat.st_mode),
771                st_ino: vm.ctx.new_pyref(stat.st_ino),
772                st_dev: vm.ctx.new_pyref(stat.st_dev),
773                st_nlink: vm.ctx.new_pyref(stat.st_nlink),
774                st_uid: vm.ctx.new_pyref(stat.st_uid),
775                st_gid: vm.ctx.new_pyref(stat.st_gid),
776                st_size: vm.ctx.new_pyref(stat.st_size),
777                __st_atime_int: atime.0,
778                __st_mtime_int: mtime.0,
779                __st_ctime_int: ctime.0,
780                st_atime: to_f64(atime),
781                st_mtime: to_f64(mtime),
782                st_ctime: to_f64(ctime),
783                st_atime_ns: to_ns(atime),
784                st_mtime_ns: to_ns(mtime),
785                st_ctime_ns: to_ns(ctime),
786                st_reparse_tag,
787            }
788        }
789
790        #[pyslot]
791        fn slot_new(_cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
792            let flatten_args = |r: &[PyObjectRef]| {
793                let mut vec_args = Vec::from(r);
794                loop {
795                    if let Ok(obj) = vec_args.iter().exactly_one() {
796                        match obj.payload::<PyTuple>() {
797                            Some(t) => {
798                                vec_args = Vec::from(t.as_slice());
799                            }
800                            None => {
801                                return vec_args;
802                            }
803                        }
804                    } else {
805                        return vec_args;
806                    }
807                }
808            };
809
810            let args: FuncArgs = flatten_args(&args.args).into();
811
812            let stat: StatResult = args.bind(vm)?;
813            Ok(stat.to_pyobject(vm))
814        }
815    }
816
817    #[cfg(windows)]
818    fn stat_inner(
819        file: OsPathOrFd,
820        dir_fd: DirFd<{ STAT_DIR_FD as usize }>,
821        follow_symlinks: FollowSymlinks,
822    ) -> io::Result<Option<StatStruct>> {
823        // TODO: replicate CPython's win32_xstat
824        let [] = dir_fd.0;
825        match file {
826            OsPathOrFd::Path(path) => crate::windows::win32_xstat(&path.path, follow_symlinks.0),
827            OsPathOrFd::Fd(fd) => crate::common::fileutils::fstat(fd),
828        }
829        .map(Some)
830    }
831
832    #[cfg(not(windows))]
833    fn stat_inner(
834        file: OsPathOrFd,
835        dir_fd: DirFd<{ STAT_DIR_FD as usize }>,
836        follow_symlinks: FollowSymlinks,
837    ) -> io::Result<Option<StatStruct>> {
838        let mut stat = std::mem::MaybeUninit::uninit();
839        let ret = match file {
840            OsPathOrFd::Path(path) => {
841                use rustpython_common::os::ffi::OsStrExt;
842                let path = path.as_ref().as_os_str().as_bytes();
843                let path = match ffi::CString::new(path) {
844                    Ok(x) => x,
845                    Err(_) => return Ok(None),
846                };
847
848                #[cfg(not(target_os = "redox"))]
849                let fstatat_ret = dir_fd.get_opt().map(|dir_fd| {
850                    let flags = if follow_symlinks.0 {
851                        0
852                    } else {
853                        libc::AT_SYMLINK_NOFOLLOW
854                    };
855                    unsafe { libc::fstatat(dir_fd, path.as_ptr(), stat.as_mut_ptr(), flags) }
856                });
857                #[cfg(target_os = "redox")]
858                let ([], fstatat_ret) = (dir_fd.0, None);
859
860                fstatat_ret.unwrap_or_else(|| {
861                    if follow_symlinks.0 {
862                        unsafe { libc::stat(path.as_ptr(), stat.as_mut_ptr()) }
863                    } else {
864                        unsafe { libc::lstat(path.as_ptr(), stat.as_mut_ptr()) }
865                    }
866                })
867            }
868            OsPathOrFd::Fd(fd) => unsafe { libc::fstat(fd, stat.as_mut_ptr()) },
869        };
870        if ret < 0 {
871            return Err(io::Error::last_os_error());
872        }
873        Ok(Some(unsafe { stat.assume_init() }))
874    }
875
876    #[pyfunction]
877    #[pyfunction(name = "fstat")]
878    fn stat(
879        file: OsPathOrFd,
880        dir_fd: DirFd<{ STAT_DIR_FD as usize }>,
881        follow_symlinks: FollowSymlinks,
882        vm: &VirtualMachine,
883    ) -> PyResult {
884        let stat = stat_inner(file.clone(), dir_fd, follow_symlinks)
885            .map_err(|err| IOErrorBuilder::with_filename(&err, file, vm))?
886            .ok_or_else(|| crate::exceptions::cstring_error(vm))?;
887        Ok(StatResult::from_stat(&stat, vm).to_pyobject(vm))
888    }
889
890    #[pyfunction]
891    fn lstat(
892        file: OsPathOrFd,
893        dir_fd: DirFd<{ STAT_DIR_FD as usize }>,
894        vm: &VirtualMachine,
895    ) -> PyResult {
896        stat(file, dir_fd, FollowSymlinks(false), vm)
897    }
898
899    fn curdir_inner(vm: &VirtualMachine) -> PyResult<PathBuf> {
900        env::current_dir().map_err(|err| err.into_pyexception(vm))
901    }
902
903    #[pyfunction]
904    fn getcwd(vm: &VirtualMachine) -> PyResult {
905        OutputMode::String.process_path(curdir_inner(vm)?, vm)
906    }
907
908    #[pyfunction]
909    fn getcwdb(vm: &VirtualMachine) -> PyResult {
910        OutputMode::Bytes.process_path(curdir_inner(vm)?, vm)
911    }
912
913    #[pyfunction]
914    fn chdir(path: OsPath, vm: &VirtualMachine) -> PyResult<()> {
915        env::set_current_dir(&path.path)
916            .map_err(|err| IOErrorBuilder::with_filename(&err, path, vm))
917    }
918
919    #[pyfunction]
920    fn fspath(path: PyObjectRef, vm: &VirtualMachine) -> PyResult<FsPath> {
921        FsPath::try_from(path, false, vm)
922    }
923
924    #[pyfunction]
925    #[pyfunction(name = "replace")]
926    fn rename(src: OsPath, dst: OsPath, vm: &VirtualMachine) -> PyResult<()> {
927        fs::rename(&src.path, &dst.path).map_err(|err| {
928            IOErrorBuilder::new(&err)
929                .filename(src)
930                .filename2(dst)
931                .into_pyexception(vm)
932        })
933    }
934
935    #[pyfunction]
936    fn getpid(vm: &VirtualMachine) -> PyObjectRef {
937        let pid = if cfg!(target_arch = "wasm32") {
938            // Return an arbitrary value, greater than 1 which is special.
939            // The value 42 is picked from wasi-libc
940            // https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-21/libc-bottom-half/getpid/getpid.c
941            42
942        } else {
943            std::process::id()
944        };
945        vm.ctx.new_int(pid).into()
946    }
947
948    #[pyfunction]
949    fn cpu_count(vm: &VirtualMachine) -> PyObjectRef {
950        let cpu_count = num_cpus::get();
951        vm.ctx.new_int(cpu_count).into()
952    }
953
954    #[pyfunction]
955    fn _exit(code: i32) {
956        std::process::exit(code)
957    }
958
959    #[pyfunction]
960    fn abort() {
961        extern "C" {
962            fn abort();
963        }
964        unsafe { abort() }
965    }
966
967    #[pyfunction]
968    fn urandom(size: isize, vm: &VirtualMachine) -> PyResult<Vec<u8>> {
969        if size < 0 {
970            return Err(vm.new_value_error("negative argument not allowed".to_owned()));
971        }
972        let mut buf = vec![0u8; size as usize];
973        getrandom::getrandom(&mut buf).map_err(|e| match e.raw_os_error() {
974            Some(errno) => io::Error::from_raw_os_error(errno).into_pyexception(vm),
975            None => vm.new_os_error("Getting random failed".to_owned()),
976        })?;
977        Ok(buf)
978    }
979
980    #[pyfunction]
981    pub fn isatty(fd: i32) -> bool {
982        unsafe { suppress_iph!(libc::isatty(fd)) != 0 }
983    }
984
985    #[pyfunction]
986    pub fn lseek(fd: i32, position: Offset, how: i32, vm: &VirtualMachine) -> PyResult<Offset> {
987        #[cfg(not(windows))]
988        let res = unsafe { suppress_iph!(libc::lseek(fd, position, how)) };
989        #[cfg(windows)]
990        let res = unsafe {
991            use windows_sys::Win32::Storage::FileSystem;
992            let handle = Fd(fd).to_raw_handle().map_err(|e| e.into_pyexception(vm))?;
993            let mut distance_to_move: [i32; 2] = std::mem::transmute(position);
994            let ret = FileSystem::SetFilePointer(
995                handle as _,
996                distance_to_move[0],
997                &mut distance_to_move[1],
998                how as _,
999            );
1000            if ret == FileSystem::INVALID_SET_FILE_POINTER {
1001                -1
1002            } else {
1003                distance_to_move[0] = ret as _;
1004                std::mem::transmute::<[i32; 2], i64>(distance_to_move)
1005            }
1006        };
1007        if res < 0 {
1008            Err(errno_err(vm))
1009        } else {
1010            Ok(res)
1011        }
1012    }
1013
1014    #[pyfunction]
1015    fn link(src: OsPath, dst: OsPath, vm: &VirtualMachine) -> PyResult<()> {
1016        fs::hard_link(&src.path, &dst.path).map_err(|err| {
1017            IOErrorBuilder::new(&err)
1018                .filename(src)
1019                .filename2(dst)
1020                .into_pyexception(vm)
1021        })
1022    }
1023
1024    #[derive(FromArgs)]
1025    struct UtimeArgs {
1026        path: OsPath,
1027        #[pyarg(any, default)]
1028        times: Option<PyTupleRef>,
1029        #[pyarg(named, default)]
1030        ns: Option<PyTupleRef>,
1031        #[pyarg(flatten)]
1032        dir_fd: DirFd<{ UTIME_DIR_FD as usize }>,
1033        #[pyarg(flatten)]
1034        follow_symlinks: FollowSymlinks,
1035    }
1036
1037    #[pyfunction]
1038    fn utime(args: UtimeArgs, vm: &VirtualMachine) -> PyResult<()> {
1039        let parse_tup = |tup: &PyTuple| -> Option<(PyObjectRef, PyObjectRef)> {
1040            if tup.len() != 2 {
1041                None
1042            } else {
1043                Some((tup[0].clone(), tup[1].clone()))
1044            }
1045        };
1046        let (acc, modif) = match (args.times, args.ns) {
1047            (Some(t), None) => {
1048                let (a, m) = parse_tup(&t).ok_or_else(|| {
1049                    vm.new_type_error(
1050                        "utime: 'times' must be either a tuple of two ints or None".to_owned(),
1051                    )
1052                })?;
1053                (a.try_into_value(vm)?, m.try_into_value(vm)?)
1054            }
1055            (None, Some(ns)) => {
1056                let (a, m) = parse_tup(&ns).ok_or_else(|| {
1057                    vm.new_type_error("utime: 'ns' must be a tuple of two ints".to_owned())
1058                })?;
1059                let ns_in_sec: PyObjectRef = vm.ctx.new_int(1_000_000_000).into();
1060                let ns_to_dur = |obj: PyObjectRef| {
1061                    let divmod = vm._divmod(&obj, &ns_in_sec)?;
1062                    let (div, rem) =
1063                        divmod
1064                            .payload::<PyTuple>()
1065                            .and_then(parse_tup)
1066                            .ok_or_else(|| {
1067                                vm.new_type_error(format!(
1068                                    "{}.__divmod__() must return a 2-tuple, not {}",
1069                                    obj.class().name(),
1070                                    divmod.class().name()
1071                                ))
1072                            })?;
1073                    let secs = div.try_index(vm)?.try_to_primitive(vm)?;
1074                    let ns = rem.try_index(vm)?.try_to_primitive(vm)?;
1075                    Ok(Duration::new(secs, ns))
1076                };
1077                // TODO: do validation to make sure this doesn't.. underflow?
1078                (ns_to_dur(a)?, ns_to_dur(m)?)
1079            }
1080            (None, None) => {
1081                let now = SystemTime::now();
1082                let now = now.duration_since(SystemTime::UNIX_EPOCH).unwrap();
1083                (now, now)
1084            }
1085            (Some(_), Some(_)) => {
1086                return Err(vm.new_value_error(
1087                    "utime: you may specify either 'times' or 'ns' but not both".to_owned(),
1088                ))
1089            }
1090        };
1091        utime_impl(args.path, acc, modif, args.dir_fd, args.follow_symlinks, vm)
1092    }
1093
1094    fn utime_impl(
1095        path: OsPath,
1096        acc: Duration,
1097        modif: Duration,
1098        dir_fd: DirFd<{ UTIME_DIR_FD as usize }>,
1099        _follow_symlinks: FollowSymlinks,
1100        vm: &VirtualMachine,
1101    ) -> PyResult<()> {
1102        #[cfg(any(target_os = "wasi", unix))]
1103        {
1104            #[cfg(not(target_os = "redox"))]
1105            {
1106                let path = path.into_cstring(vm)?;
1107
1108                let ts = |d: Duration| libc::timespec {
1109                    tv_sec: d.as_secs() as _,
1110                    tv_nsec: d.subsec_nanos() as _,
1111                };
1112                let times = [ts(acc), ts(modif)];
1113
1114                let ret = unsafe {
1115                    libc::utimensat(
1116                        dir_fd.fd().0,
1117                        path.as_ptr(),
1118                        times.as_ptr(),
1119                        if _follow_symlinks.0 {
1120                            0
1121                        } else {
1122                            libc::AT_SYMLINK_NOFOLLOW
1123                        },
1124                    )
1125                };
1126                if ret < 0 {
1127                    Err(errno_err(vm))
1128                } else {
1129                    Ok(())
1130                }
1131            }
1132            #[cfg(target_os = "redox")]
1133            {
1134                let [] = dir_fd.0;
1135
1136                let tv = |d: Duration| libc::timeval {
1137                    tv_sec: d.as_secs() as _,
1138                    tv_usec: d.as_micros() as _,
1139                };
1140                nix::sys::stat::utimes(path.as_ref(), &tv(acc).into(), &tv(modif).into())
1141                    .map_err(|err| err.into_pyexception(vm))
1142            }
1143        }
1144        #[cfg(windows)]
1145        {
1146            use std::{fs::OpenOptions, os::windows::prelude::*};
1147            type DWORD = u32;
1148            use windows_sys::Win32::{Foundation::FILETIME, Storage::FileSystem};
1149
1150            let [] = dir_fd.0;
1151
1152            let ft = |d: Duration| {
1153                let intervals =
1154                    ((d.as_secs() as i64 + 11644473600) * 10_000_000) + (d.as_nanos() as i64 / 100);
1155                FILETIME {
1156                    dwLowDateTime: intervals as DWORD,
1157                    dwHighDateTime: (intervals >> 32) as DWORD,
1158                }
1159            };
1160
1161            let acc = ft(acc);
1162            let modif = ft(modif);
1163
1164            let f = OpenOptions::new()
1165                .write(true)
1166                .custom_flags(windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS)
1167                .open(path)
1168                .map_err(|err| err.into_pyexception(vm))?;
1169
1170            let ret = unsafe {
1171                FileSystem::SetFileTime(f.as_raw_handle() as _, std::ptr::null(), &acc, &modif)
1172            };
1173
1174            if ret == 0 {
1175                Err(io::Error::last_os_error().into_pyexception(vm))
1176            } else {
1177                Ok(())
1178            }
1179        }
1180    }
1181
1182    #[cfg(all(any(unix, windows), not(target_os = "redox")))]
1183    #[pyattr]
1184    #[pyclass(module = "os", name = "times_result")]
1185    #[derive(Debug, PyStructSequence)]
1186    struct TimesResult {
1187        pub user: f64,
1188        pub system: f64,
1189        pub children_user: f64,
1190        pub children_system: f64,
1191        pub elapsed: f64,
1192    }
1193
1194    #[cfg(all(any(unix, windows), not(target_os = "redox")))]
1195    #[pyclass(with(PyStructSequence))]
1196    impl TimesResult {}
1197
1198    #[cfg(all(any(unix, windows), not(target_os = "redox")))]
1199    #[pyfunction]
1200    fn times(vm: &VirtualMachine) -> PyResult {
1201        #[cfg(windows)]
1202        {
1203            use std::mem::MaybeUninit;
1204            use windows_sys::Win32::{Foundation::FILETIME, System::Threading};
1205
1206            let mut _create = MaybeUninit::<FILETIME>::uninit();
1207            let mut _exit = MaybeUninit::<FILETIME>::uninit();
1208            let mut kernel = MaybeUninit::<FILETIME>::uninit();
1209            let mut user = MaybeUninit::<FILETIME>::uninit();
1210
1211            unsafe {
1212                let h_proc = Threading::GetCurrentProcess();
1213                Threading::GetProcessTimes(
1214                    h_proc,
1215                    _create.as_mut_ptr(),
1216                    _exit.as_mut_ptr(),
1217                    kernel.as_mut_ptr(),
1218                    user.as_mut_ptr(),
1219                );
1220            }
1221
1222            let kernel = unsafe { kernel.assume_init() };
1223            let user = unsafe { user.assume_init() };
1224
1225            let times_result = TimesResult {
1226                user: user.dwHighDateTime as f64 * 429.4967296 + user.dwLowDateTime as f64 * 1e-7,
1227                system: kernel.dwHighDateTime as f64 * 429.4967296
1228                    + kernel.dwLowDateTime as f64 * 1e-7,
1229                children_user: 0.0,
1230                children_system: 0.0,
1231                elapsed: 0.0,
1232            };
1233
1234            Ok(times_result.to_pyobject(vm))
1235        }
1236        #[cfg(unix)]
1237        {
1238            let mut t = libc::tms {
1239                tms_utime: 0,
1240                tms_stime: 0,
1241                tms_cutime: 0,
1242                tms_cstime: 0,
1243            };
1244
1245            let tick_for_second = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as f64;
1246            let c = unsafe { libc::times(&mut t as *mut _) };
1247
1248            // XXX: The signedness of `clock_t` varies from platform to platform.
1249            if c == (-1i8) as libc::clock_t {
1250                return Err(vm.new_os_error("Fail to get times".to_string()));
1251            }
1252
1253            let times_result = TimesResult {
1254                user: t.tms_utime as f64 / tick_for_second,
1255                system: t.tms_stime as f64 / tick_for_second,
1256                children_user: t.tms_cutime as f64 / tick_for_second,
1257                children_system: t.tms_cstime as f64 / tick_for_second,
1258                elapsed: c as f64 / tick_for_second,
1259            };
1260
1261            Ok(times_result.to_pyobject(vm))
1262        }
1263    }
1264
1265    #[cfg(target_os = "linux")]
1266    #[derive(FromArgs)]
1267    struct CopyFileRangeArgs {
1268        #[pyarg(positional)]
1269        src: i32,
1270        #[pyarg(positional)]
1271        dst: i32,
1272        #[pyarg(positional)]
1273        count: i64,
1274        #[pyarg(any, default)]
1275        offset_src: Option<Offset>,
1276        #[pyarg(any, default)]
1277        offset_dst: Option<Offset>,
1278    }
1279
1280    #[cfg(target_os = "linux")]
1281    #[pyfunction]
1282    fn copy_file_range(args: CopyFileRangeArgs, vm: &VirtualMachine) -> PyResult<usize> {
1283        let p_offset_src = args.offset_src.as_ref().map_or_else(std::ptr::null, |x| x);
1284        let p_offset_dst = args.offset_dst.as_ref().map_or_else(std::ptr::null, |x| x);
1285        let count: usize = args
1286            .count
1287            .try_into()
1288            .map_err(|_| vm.new_value_error("count should >= 0".to_string()))?;
1289
1290        // The flags argument is provided to allow
1291        // for future extensions and currently must be to 0.
1292        let flags = 0u32;
1293
1294        // Safety: p_offset_src and p_offset_dst is a unique pointer for offset_src and offset_dst respectively,
1295        // and will only be freed after this function ends.
1296        //
1297        // Why not use `libc::copy_file_range`: On `musl-libc`, `libc::copy_file_range` is not provided. Therefore
1298        // we use syscalls directly instead.
1299        let ret = unsafe {
1300            libc::syscall(
1301                libc::SYS_copy_file_range,
1302                args.src,
1303                p_offset_src as *mut i64,
1304                args.dst,
1305                p_offset_dst as *mut i64,
1306                count,
1307                flags,
1308            )
1309        };
1310
1311        usize::try_from(ret).map_err(|_| errno_err(vm))
1312    }
1313
1314    #[pyfunction]
1315    fn strerror(e: i32) -> String {
1316        unsafe { ffi::CStr::from_ptr(libc::strerror(e)) }
1317            .to_string_lossy()
1318            .into_owned()
1319    }
1320
1321    #[pyfunction]
1322    pub fn ftruncate(fd: i32, length: Offset, vm: &VirtualMachine) -> PyResult<()> {
1323        Fd(fd).ftruncate(length).map_err(|e| e.into_pyexception(vm))
1324    }
1325
1326    #[pyfunction]
1327    fn truncate(path: PyObjectRef, length: Offset, vm: &VirtualMachine) -> PyResult<()> {
1328        if let Ok(fd) = path.try_to_value(vm) {
1329            return ftruncate(fd, length, vm);
1330        }
1331
1332        #[cold]
1333        fn error(
1334            vm: &VirtualMachine,
1335            error: std::io::Error,
1336            path: OsPath,
1337        ) -> crate::builtins::PyBaseExceptionRef {
1338            IOErrorBuilder::with_filename(&error, path, vm)
1339        }
1340
1341        let path = OsPath::try_from_object(vm, path)?;
1342        // TODO: just call libc::truncate() on POSIX
1343        let f = match OpenOptions::new().write(true).open(&path) {
1344            Ok(f) => f,
1345            Err(e) => return Err(error(vm, e, path)),
1346        };
1347        f.set_len(length as u64).map_err(|e| error(vm, e, path))?;
1348        drop(f);
1349        Ok(())
1350    }
1351
1352    #[cfg(all(unix, not(any(target_os = "redox", target_os = "android"))))]
1353    #[pyfunction]
1354    fn getloadavg(vm: &VirtualMachine) -> PyResult<(f64, f64, f64)> {
1355        let mut loadavg = [0f64; 3];
1356
1357        // Safety: loadavg is on stack and only write by `getloadavg` and are freed
1358        // after this function ends.
1359        unsafe {
1360            if libc::getloadavg(&mut loadavg[0] as *mut f64, 3) != 3 {
1361                return Err(vm.new_os_error("Load averages are unobtainable".to_string()));
1362            }
1363        }
1364
1365        Ok((loadavg[0], loadavg[1], loadavg[2]))
1366    }
1367
1368    #[cfg(any(unix, windows))]
1369    #[pyfunction]
1370    fn waitstatus_to_exitcode(status: i32, vm: &VirtualMachine) -> PyResult<i32> {
1371        let status = u32::try_from(status)
1372            .map_err(|_| vm.new_value_error(format!("invalid WEXITSTATUS: {status}")))?;
1373
1374        cfg_if::cfg_if! {
1375            if #[cfg(not(windows))] {
1376                let status = status as libc::c_int;
1377                if libc::WIFEXITED(status) {
1378                    return Ok(libc::WEXITSTATUS(status));
1379                }
1380
1381                if libc::WIFSIGNALED(status) {
1382                    return Ok(-libc::WTERMSIG(status));
1383                }
1384
1385                Err(vm.new_value_error(format!("Invalid wait status: {status}")))
1386            } else {
1387                i32::try_from(status.rotate_right(8))
1388                    .map_err(|_| vm.new_value_error(format!("invalid wait status: {status}")))
1389            }
1390        }
1391    }
1392
1393    #[pyfunction]
1394    fn device_encoding(fd: i32, _vm: &VirtualMachine) -> PyResult<Option<String>> {
1395        if !isatty(fd) {
1396            return Ok(None);
1397        }
1398
1399        cfg_if::cfg_if! {
1400            if #[cfg(any(target_os = "android", target_os = "redox"))] {
1401                Ok(Some("UTF-8".to_owned()))
1402            } else if #[cfg(windows)] {
1403                use windows_sys::Win32::System::Console;
1404                let cp = match fd {
1405                    0 => unsafe { Console::GetConsoleCP() },
1406                    1 | 2 => unsafe { Console::GetConsoleOutputCP() },
1407                    _ => 0,
1408                };
1409
1410                Ok(Some(format!("cp{cp}")))
1411            } else {
1412                let encoding = unsafe {
1413                    let encoding = libc::nl_langinfo(libc::CODESET);
1414                    if encoding.is_null() || encoding.read() == '\0' as libc::c_char {
1415                        "UTF-8".to_owned()
1416                    } else {
1417                        ffi::CStr::from_ptr(encoding).to_string_lossy().into_owned()
1418                    }
1419                };
1420
1421                Ok(Some(encoding))
1422            }
1423        }
1424    }
1425
1426    #[pyattr]
1427    #[pyclass(module = "os", name = "terminal_size")]
1428    #[derive(PyStructSequence)]
1429    #[allow(dead_code)]
1430    pub(crate) struct PyTerminalSize {
1431        pub columns: usize,
1432        pub lines: usize,
1433    }
1434    #[pyclass(with(PyStructSequence))]
1435    impl PyTerminalSize {}
1436
1437    #[pyattr]
1438    #[pyclass(module = "os", name = "uname_result")]
1439    #[derive(Debug, PyStructSequence)]
1440    pub(crate) struct UnameResult {
1441        pub sysname: String,
1442        pub nodename: String,
1443        pub release: String,
1444        pub version: String,
1445        pub machine: String,
1446    }
1447
1448    #[pyclass(with(PyStructSequence))]
1449    impl UnameResult {}
1450
1451    pub(super) fn support_funcs() -> Vec<SupportFunc> {
1452        let mut supports = super::platform::module::support_funcs();
1453        supports.extend(vec![
1454            SupportFunc::new("open", Some(false), Some(OPEN_DIR_FD), Some(false)),
1455            SupportFunc::new("access", Some(false), Some(false), None),
1456            SupportFunc::new("chdir", None, Some(false), Some(false)),
1457            // chflags Some, None Some
1458            SupportFunc::new("listdir", Some(LISTDIR_FD), Some(false), Some(false)),
1459            SupportFunc::new("mkdir", Some(false), Some(MKDIR_DIR_FD), Some(false)),
1460            // mkfifo Some Some None
1461            // mknod Some Some None
1462            SupportFunc::new("readlink", Some(false), None, Some(false)),
1463            SupportFunc::new("remove", Some(false), None, Some(false)),
1464            SupportFunc::new("unlink", Some(false), None, Some(false)),
1465            SupportFunc::new("rename", Some(false), None, Some(false)),
1466            SupportFunc::new("replace", Some(false), None, Some(false)), // TODO: Fix replace
1467            SupportFunc::new("rmdir", Some(false), None, Some(false)),
1468            SupportFunc::new("scandir", None, Some(false), Some(false)),
1469            SupportFunc::new("stat", Some(true), Some(STAT_DIR_FD), Some(true)),
1470            SupportFunc::new("fstat", Some(true), Some(STAT_DIR_FD), Some(true)),
1471            SupportFunc::new("symlink", Some(false), Some(SYMLINK_DIR_FD), Some(false)),
1472            SupportFunc::new("truncate", Some(true), Some(false), Some(false)),
1473            SupportFunc::new(
1474                "utime",
1475                Some(false),
1476                Some(UTIME_DIR_FD),
1477                Some(cfg!(all(unix, not(target_os = "redox")))),
1478            ),
1479        ]);
1480        supports
1481    }
1482}
1483pub(crate) use _os::{ftruncate, isatty, lseek};
1484
1485pub(crate) struct SupportFunc {
1486    name: &'static str,
1487    // realistically, each of these is just a bool of "is this function in the supports_* set".
1488    // However, None marks that the function maybe _should_ support fd/dir_fd/follow_symlinks, but
1489    // we haven't implemented it yet.
1490    fd: Option<bool>,
1491    dir_fd: Option<bool>,
1492    follow_symlinks: Option<bool>,
1493}
1494
1495impl SupportFunc {
1496    pub(crate) fn new(
1497        name: &'static str,
1498        fd: Option<bool>,
1499        dir_fd: Option<bool>,
1500        follow_symlinks: Option<bool>,
1501    ) -> Self {
1502        Self {
1503            name,
1504            fd,
1505            dir_fd,
1506            follow_symlinks,
1507        }
1508    }
1509}
1510
1511pub fn extend_module(vm: &VirtualMachine, module: &Py<PyModule>) {
1512    let support_funcs = _os::support_funcs();
1513    let supports_fd = PySet::default().into_ref(&vm.ctx);
1514    let supports_dir_fd = PySet::default().into_ref(&vm.ctx);
1515    let supports_follow_symlinks = PySet::default().into_ref(&vm.ctx);
1516    for support in support_funcs {
1517        let func_obj = module.get_attr(support.name, vm).unwrap();
1518        if support.fd.unwrap_or(false) {
1519            supports_fd.clone().add(func_obj.clone(), vm).unwrap();
1520        }
1521        if support.dir_fd.unwrap_or(false) {
1522            supports_dir_fd.clone().add(func_obj.clone(), vm).unwrap();
1523        }
1524        if support.follow_symlinks.unwrap_or(false) {
1525            supports_follow_symlinks.clone().add(func_obj, vm).unwrap();
1526        }
1527    }
1528
1529    extend_module!(vm, module, {
1530        "supports_fd" => supports_fd,
1531        "supports_dir_fd" => supports_dir_fd,
1532        "supports_follow_symlinks" => supports_follow_symlinks,
1533        "error" => vm.ctx.exceptions.os_error.to_owned(),
1534    });
1535}
1536
1537#[cfg(not(windows))]
1538use super::posix as platform;
1539
1540#[cfg(windows)]
1541use super::nt as platform;
1542
1543pub(crate) use platform::module::MODULE_NAME;