1#![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#[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#[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 #[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 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 return buffer.with_ref(|buf| read_into_slice(fd, buf, vm));
390 }
391
392 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 #[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 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 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 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 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 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 let env_str = format!("{name_str}=");
619 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 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 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 #[cfg(unix)]
674 d_type: Option<u8>,
675 #[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 #[cfg(windows)]
689 find: Option<host_nt::ScandirEntry>,
690 }
691
692 impl DirEntry {
693 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 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 #[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 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 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 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 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 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 #[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 #[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 #[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 #[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 #[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 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 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 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 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 (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 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 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 #[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, pub f_frsize: libc::c_ulong, pub f_blocks: libc::fsblkcnt_t, pub f_bfree: libc::fsblkcnt_t, pub f_bavail: libc::fsblkcnt_t, pub f_files: libc::fsfilcnt_t, pub f_ffree: libc::fsfilcnt_t, pub f_favail: libc::fsfilcnt_t, pub f_flag: libc::c_ulong, pub f_namemax: libc::c_ulong, #[pystruct_sequence(skip)]
2219 pub f_fsid: libc::c_ulong, }
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 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 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)), 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 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#[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;