Skip to main content

rustpython_host_env/
os.rs

1// spell-checker:disable
2// TODO: we can move more os-specific bindings/interfaces from stdlib::{os, posix, nt} to here
3
4use crate::crt_fd;
5#[cfg(windows)]
6use crate::fs;
7#[cfg(windows)]
8pub use crate::posix::rename;
9#[cfg(any(unix, target_os = "wasi"))]
10pub use crate::posix_unix_like::rename;
11#[cfg(any(unix, windows))]
12use core::ffi::CStr;
13use core::str::Utf8Error;
14#[cfg(windows)]
15use core::time::Duration;
16use std::{
17    env,
18    ffi::{OsStr, OsString},
19    io,
20    path::PathBuf,
21    process::ExitCode,
22};
23#[cfg(windows)]
24use {
25    std::{os::windows::io::AsRawHandle, path::Path},
26    windows_sys::Win32::{
27        Foundation::FILETIME,
28        Storage::FileSystem::{FILE_FLAG_BACKUP_SEMANTICS, SetFilePointerEx, SetFileTime},
29        System::SystemInformation::{GetSystemInfo, SYSTEM_INFO},
30    },
31};
32
33bitflagset::bitflag! {
34    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
35    #[repr(u8)]
36    pub enum AccessFlag {
37        X = 0,
38        W = 1,
39        R = 2,
40    }
41}
42
43bitflagset::bitflagset! {
44    #[derive(Copy, Clone, PartialEq, Eq)]
45    pub struct AccessMode(u8): AccessFlag
46}
47
48pub const F_OK: u8 = AccessMode::empty().bits();
49pub const X_OK: u8 = AccessMode::from_element(AccessFlag::X).bits();
50pub const W_OK: u8 = AccessMode::from_element(AccessFlag::W).bits();
51pub const R_OK: u8 = AccessMode::from_element(AccessFlag::R).bits();
52
53#[cfg(any(unix, target_os = "wasi"))]
54pub use libc::AT_FDCWD;
55#[cfg(not(any(unix, target_os = "wasi")))]
56pub const AT_FDCWD: i32 = -100;
57
58#[cfg(unix)]
59pub use libc::{AT_REMOVEDIR, AT_SYMLINK_FOLLOW, AT_SYMLINK_NOFOLLOW};
60
61#[cfg(any(
62    target_os = "freebsd",
63    target_os = "linux",
64    target_os = "netbsd",
65    target_vendor = "apple"
66))]
67pub use libc::AT_EACCESS;
68
69/// bionic does not export `AT_EACCESS`; the value is the Linux `fcntl.h` one.
70#[cfg(target_os = "android")]
71pub const AT_EACCESS: i32 = 0x200;
72
73#[cfg(all(unix, not(target_os = "redox")))]
74pub use libc::{ST_NOSUID, ST_RDONLY};
75
76/// `open(2)` flags. libc on hosts that bind them; Darwin/BSD numbers on
77/// `wasm32-unknown-unknown`, matching the guest errno table.
78#[cfg(any(unix, windows, target_os = "wasi"))]
79pub use libc::{O_APPEND, O_CREAT, O_EXCL, O_RDONLY, O_RDWR, O_TRUNC, O_WRONLY};
80
81#[cfg(unix)]
82pub use libc::{O_ACCMODE, O_CLOEXEC, O_DIRECTORY, O_NOFOLLOW, O_NONBLOCK};
83
84#[cfg(any(
85    target_os = "android",
86    target_os = "dragonfly",
87    target_os = "freebsd",
88    target_os = "linux",
89    target_os = "macos",
90    target_os = "netbsd",
91    target_os = "redox"
92))]
93pub use libc::{O_ASYNC, O_NDELAY, O_NOCTTY};
94
95#[cfg(any(
96    target_os = "android",
97    target_os = "freebsd",
98    target_os = "linux",
99    target_os = "macos",
100    target_os = "netbsd"
101))]
102pub use libc::O_DSYNC;
103
104#[cfg(any(
105    target_os = "android",
106    target_os = "dragonfly",
107    target_os = "freebsd",
108    target_os = "linux",
109    target_os = "macos",
110    target_os = "netbsd"
111))]
112pub use libc::O_SYNC;
113
114#[cfg(any(
115    target_os = "dragonfly",
116    target_os = "freebsd",
117    target_os = "macos",
118    target_os = "netbsd",
119    target_os = "redox"
120))]
121pub use libc::O_FSYNC;
122
123#[cfg(any(target_os = "linux", target_os = "android"))]
124pub use libc::{O_DIRECT, O_LARGEFILE, O_NOATIME, O_PATH, O_RSYNC, O_TMPFILE};
125
126#[cfg(target_os = "freebsd")]
127pub use libc::{O_DIRECT, O_PATH};
128
129#[cfg(target_os = "redox")]
130pub use libc::O_PATH;
131
132#[cfg(target_os = "netbsd")]
133pub use libc::{O_DIRECT, O_RSYNC};
134
135#[cfg(target_os = "dragonfly")]
136pub use libc::O_DIRECT;
137
138#[cfg(any(target_os = "macos", target_os = "ios"))]
139pub use libc::{O_EVTONLY, O_EXEC, O_EXLOCK, O_NOFOLLOW_ANY, O_SEARCH, O_SHLOCK, O_SYMLINK};
140
141#[cfg(any(
142    target_os = "dragonfly",
143    target_os = "freebsd",
144    target_os = "netbsd",
145    target_os = "redox"
146))]
147pub use libc::{O_EXLOCK, O_SHLOCK};
148
149#[cfg(target_os = "redox")]
150pub use libc::O_SYMLINK;
151
152#[cfg(windows)]
153pub use libc::{O_BINARY, O_NOINHERIT, O_RANDOM, O_SEQUENTIAL, O_TEMPORARY, O_TEXT};
154
155#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
156mod wasm_oflag {
157    pub const O_RDONLY: i32 = 0x0000;
158    pub const O_WRONLY: i32 = 0x0001;
159    pub const O_RDWR: i32 = 0x0002;
160    pub const O_ACCMODE: i32 = 0x0003;
161    pub const O_NONBLOCK: i32 = 0x0004;
162    pub const O_APPEND: i32 = 0x0008;
163    pub const O_SYNC: i32 = 0x0080;
164    pub const O_NOFOLLOW: i32 = 0x0100;
165    pub const O_CREAT: i32 = 0x0200;
166    pub const O_TRUNC: i32 = 0x0400;
167    pub const O_EXCL: i32 = 0x0800;
168    pub const O_DIRECTORY: i32 = 0x0010_0000;
169    pub const O_CLOEXEC: i32 = 0x0100_0000;
170    pub const O_NDELAY: i32 = O_NONBLOCK;
171}
172
173#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
174pub use wasm_oflag::*;
175
176bitflagset::bitflag! {
177    /// BSD `chflags` bits. Values are bit *positions*; masks are `1 << pos`.
178    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
179    #[repr(u8)]
180    pub enum ChFlag {
181        UfNodump = 0,
182        UfImmutable = 1,
183        UfAppend = 2,
184        UfOpaque = 3,
185        UfNounlink = 4,
186        UfCompressed = 5,
187        UfTracked = 6,
188        UfDatavault = 7,
189        UfHidden = 15,
190        SfArchived = 16,
191        SfImmutable = 17,
192        SfAppend = 18,
193        SfNounlink = 20,
194        SfSnapshot = 21,
195        SfFirmlink = 23,
196        SfDataless = 30,
197    }
198}
199
200bitflagset::bitflagset! {
201    #[derive(Copy, Clone, PartialEq, Eq)]
202    pub struct ChFlags(u32): ChFlag
203}
204
205impl ChFlags {
206    /// Owner-settable bits, including reserved user-flag positions.
207    pub const UF_SETTABLE: Self = Self::from_bits_retain(0x0000ffff);
208    pub const SF_SETTABLE: Self = Self::from_bits_retain(if cfg!(target_os = "macos") {
209        0x3fff0000
210    } else {
211        0xffff0000
212    });
213    #[cfg(target_os = "macos")]
214    pub const SF_SUPPORTED: Self = Self::from_bits_retain(0x009f0000);
215    #[cfg(target_os = "macos")]
216    pub const SF_SYNTHETIC: Self = Self::from_bits_retain(0xc0000000);
217}
218
219pub const UF_NODUMP: u32 = ChFlags::from_element(ChFlag::UfNodump).bits();
220pub const UF_IMMUTABLE: u32 = ChFlags::from_element(ChFlag::UfImmutable).bits();
221pub const UF_APPEND: u32 = ChFlags::from_element(ChFlag::UfAppend).bits();
222pub const UF_OPAQUE: u32 = ChFlags::from_element(ChFlag::UfOpaque).bits();
223pub const UF_NOUNLINK: u32 = ChFlags::from_element(ChFlag::UfNounlink).bits();
224pub const UF_COMPRESSED: u32 = ChFlags::from_element(ChFlag::UfCompressed).bits();
225pub const UF_TRACKED: u32 = ChFlags::from_element(ChFlag::UfTracked).bits();
226pub const UF_DATAVAULT: u32 = ChFlags::from_element(ChFlag::UfDatavault).bits();
227pub const UF_HIDDEN: u32 = ChFlags::from_element(ChFlag::UfHidden).bits();
228pub const UF_SETTABLE: u32 = ChFlags::UF_SETTABLE.bits();
229pub const SF_ARCHIVED: u32 = ChFlags::from_element(ChFlag::SfArchived).bits();
230pub const SF_IMMUTABLE: u32 = ChFlags::from_element(ChFlag::SfImmutable).bits();
231pub const SF_APPEND: u32 = ChFlags::from_element(ChFlag::SfAppend).bits();
232pub const SF_NOUNLINK: u32 = ChFlags::from_element(ChFlag::SfNounlink).bits();
233pub const SF_SNAPSHOT: u32 = ChFlags::from_element(ChFlag::SfSnapshot).bits();
234pub const SF_FIRMLINK: u32 = ChFlags::from_element(ChFlag::SfFirmlink).bits();
235pub const SF_DATALESS: u32 = ChFlags::from_element(ChFlag::SfDataless).bits();
236pub const SF_SETTABLE: u32 = ChFlags::SF_SETTABLE.bits();
237#[cfg(target_os = "macos")]
238pub const SF_SUPPORTED: u32 = ChFlags::SF_SUPPORTED.bits();
239#[cfg(target_os = "macos")]
240pub const SF_SYNTHETIC: u32 = ChFlags::SF_SYNTHETIC.bits();
241
242const _: () = {
243    assert!(UF_NODUMP == 0x00000001);
244    assert!(UF_IMMUTABLE == 0x00000002);
245    assert!(UF_APPEND == 0x00000004);
246    assert!(UF_OPAQUE == 0x00000008);
247    assert!(UF_NOUNLINK == 0x00000010);
248    assert!(UF_COMPRESSED == 0x00000020);
249    assert!(UF_TRACKED == 0x00000040);
250    assert!(UF_DATAVAULT == 0x00000080);
251    assert!(UF_HIDDEN == 0x00008000);
252    assert!(UF_SETTABLE == 0x0000ffff);
253    assert!(SF_ARCHIVED == 0x00010000);
254    assert!(SF_IMMUTABLE == 0x00020000);
255    assert!(SF_APPEND == 0x00040000);
256    assert!(SF_NOUNLINK == 0x00100000);
257    assert!(SF_SNAPSHOT == 0x00200000);
258    assert!(SF_FIRMLINK == 0x00800000);
259    assert!(SF_DATALESS == 0x40000000);
260};
261
262/// Solaris door/port and BSD whiteout. `_stat` publishes 0 where the
263/// platform has no such file type.
264pub const S_IFDOOR: u32 = 0;
265pub const S_IFPORT: u32 = 0;
266pub const S_IFWHT: u32 = if cfg!(target_os = "macos") {
267    0o160000
268} else {
269    0
270};
271
272#[cfg(not(any(unix, windows, target_os = "wasi")))]
273pub fn rename(
274    from: impl AsRef<std::path::Path>,
275    from_fd: Option<crt_fd::Borrowed<'_>>,
276    to: impl AsRef<std::path::Path>,
277    to_fd: Option<crt_fd::Borrowed<'_>>,
278) -> io::Result<()> {
279    if from_fd.is_none() && to_fd.is_none() {
280        std::fs::rename(from, to)
281    } else {
282        Err(io::Error::new(
283            io::ErrorKind::Unsupported,
284            "renameat is not available on this platform",
285        ))
286    }
287}
288
289/// Convert exit code to std::process::ExitCode
290///
291/// On Windows, this supports the full u32 range including STATUS_CONTROL_C_EXIT (0xC000013A).
292/// On other platforms, only the lower 8 bits are used.
293pub fn exit_code(code: u32) -> ExitCode {
294    #[cfg(windows)]
295    {
296        // For large exit codes like STATUS_CONTROL_C_EXIT (0xC000013A),
297        // we need to call std::process::exit() directly since ExitCode::from(u8)
298        // would truncate the value, and ExitCode::from_raw() is still unstable.
299        // FIXME: side effect in exit_code is not ideal.
300        if code > u8::MAX as u32 {
301            std::process::exit(code as i32)
302        }
303    }
304    ExitCode::from(code as u8)
305}
306
307pub fn current_dir() -> io::Result<PathBuf> {
308    env::current_dir()
309}
310
311#[must_use]
312pub fn temp_dir() -> PathBuf {
313    env::temp_dir()
314}
315
316pub fn var(key: &str) -> Result<String, env::VarError> {
317    env::var(key)
318}
319
320pub fn var_os(key: impl AsRef<OsStr>) -> Option<OsString> {
321    env::var_os(key)
322}
323
324#[must_use]
325pub fn vars_os() -> env::VarsOs {
326    env::vars_os()
327}
328
329#[must_use]
330pub fn vars() -> env::Vars {
331    env::vars()
332}
333
334/// # Safety
335/// The caller must ensure no other threads can concurrently read or write
336/// the process environment while this mutation is performed.
337pub unsafe fn set_var(key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) {
338    unsafe { env::set_var(key, value) };
339}
340
341/// # Safety
342/// The caller must ensure no other threads can concurrently read or write
343/// the process environment while this mutation is performed.
344pub unsafe fn remove_var(key: impl AsRef<OsStr>) {
345    unsafe { env::remove_var(key) };
346}
347
348/// Publish the working directory as `=X:` for the drive it is on.
349///
350/// CRT `_wspawnve` / `_wexecve` walk the environment for the first `=X:`
351/// entry with no bound; a process that was not started by cmd.exe has none
352/// and that walk runs off the block. A UNC-like path is on no drive and
353/// publishes nothing.
354#[cfg(windows)]
355pub fn publish_drive_current_directory() -> io::Result<()> {
356    use std::os::windows::ffi::OsStrExt;
357    use windows_sys::Win32::System::Environment::SetEnvironmentVariableW;
358
359    let mut cwd_wide: Vec<u16> = env::current_dir()?.as_os_str().encode_wide().collect();
360    let unc_like = cwd_wide.len() >= 2
361        && ((cwd_wide[0] == b'\\' as u16 && cwd_wide[1] == b'\\' as u16)
362            || (cwd_wide[0] == b'/' as u16 && cwd_wide[1] == b'/' as u16));
363    if unc_like || cwd_wide.is_empty() {
364        return Ok(());
365    }
366    let env_name = [b'=' as u16, cwd_wide[0], b':' as u16, 0];
367    cwd_wide.push(0);
368    let ok = unsafe { SetEnvironmentVariableW(env_name.as_ptr(), cwd_wide.as_ptr()) };
369    if ok == 0 {
370        Err(io::Error::last_os_error())
371    } else {
372        Ok(())
373    }
374}
375
376/// Whether the process environment already has an `=X:` drive-cwd entry.
377#[cfg(windows)]
378pub fn environment_has_drive_current_directory() -> bool {
379    use windows_sys::Win32::System::Environment::{
380        FreeEnvironmentStringsW, GetEnvironmentStringsW,
381    };
382
383    let block = unsafe { GetEnvironmentStringsW() };
384    if block.is_null() {
385        return false;
386    }
387    let mut present = false;
388    let mut entry = block;
389    unsafe {
390        while *entry != 0 {
391            if *entry == b'=' as u16 {
392                present = true;
393                break;
394            }
395            while *entry != 0 {
396                entry = entry.add(1);
397            }
398            entry = entry.add(1);
399        }
400        FreeEnvironmentStringsW(block);
401    }
402    present
403}
404
405/// Publish `=X:` when the process environment has none, so a later
406/// `_wspawnv` / `_wexecv` walk has a first entry.
407#[cfg(windows)]
408pub fn ensure_drive_current_directory() {
409    if !environment_has_drive_current_directory() {
410        let _ = publish_drive_current_directory();
411    }
412}
413
414pub fn set_current_dir(path: impl AsRef<std::path::Path>) -> io::Result<()> {
415    env::set_current_dir(&path)?;
416    #[cfg(windows)]
417    publish_drive_current_directory()?;
418    Ok(())
419}
420
421#[must_use]
422pub fn process_id() -> u32 {
423    std::process::id()
424}
425
426#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
427pub fn cpu_count() -> usize {
428    num_cpus::get()
429}
430
431#[cfg(not(any(not(target_arch = "wasm32"), target_os = "wasi")))]
432pub fn cpu_count() -> usize {
433    1
434}
435
436#[cfg(unix)]
437pub fn page_size() -> usize {
438    rustix::param::page_size()
439}
440
441#[cfg(target_arch = "wasm32")]
442pub const fn page_size() -> usize {
443    // WebAssembly's page size is a constant defined by the spec.
444    1024 * 64
445}
446
447#[cfg(windows)]
448pub fn page_size() -> usize {
449    let mut info = SYSTEM_INFO::default();
450    unsafe {
451        GetSystemInfo(&mut info);
452    }
453    info.dwPageSize as _
454}
455
456#[cfg(unix)]
457pub fn alloc_granularity() -> usize {
458    // On Unix-likes, the page size is the smallest allocation unit rather than a separate concept
459    // of allocation granularity.
460    page_size()
461}
462
463#[cfg(target_arch = "wasm32")]
464pub const fn alloc_granularity() -> usize {
465    // Like Unix, WebAssembly doesn't separate page size and alloc granularity.
466    page_size()
467}
468
469#[cfg(windows)]
470pub fn alloc_granularity() -> usize {
471    let mut info = SYSTEM_INFO::default();
472    unsafe {
473        GetSystemInfo(&mut info);
474    }
475    info.dwAllocationGranularity as _
476}
477
478pub fn device_encoding(_fd: i32) -> Option<String> {
479    #[cfg(any(
480        target_os = "android",
481        target_os = "redox",
482        all(target_arch = "wasm32", not(target_os = "wasi"))
483    ))]
484    {
485        Some("UTF-8".to_owned())
486    }
487
488    #[cfg(windows)]
489    {
490        use windows_sys::Win32::System::Console;
491        let cp = match _fd {
492            0 => unsafe { Console::GetConsoleCP() },
493            1 | 2 => unsafe { Console::GetConsoleOutputCP() },
494            _ => 0,
495        };
496
497        Some(format!("cp{cp}"))
498    }
499
500    #[cfg(not(any(
501        target_os = "android",
502        target_os = "redox",
503        windows,
504        all(target_arch = "wasm32", not(target_os = "wasi"))
505    )))]
506    {
507        let encoding = unsafe {
508            let encoding = libc::nl_langinfo(libc::CODESET);
509            if encoding.is_null() || encoding.read() == b'\0' as libc::c_char {
510                "UTF-8".to_owned()
511            } else {
512                core::ffi::CStr::from_ptr(encoding)
513                    .to_string_lossy()
514                    .into_owned()
515            }
516        };
517
518        Some(encoding)
519    }
520}
521
522pub fn exit(code: i32) -> ! {
523    std::process::exit(code)
524}
525
526/// Wrapper around the C `abort()` call: terminates the process abnormally.
527pub fn abort() -> ! {
528    unsafe extern "C" {
529        fn abort() -> !;
530    }
531    unsafe { abort() }
532}
533
534/// Read `size` cryptographically random bytes from the OS.
535pub fn urandom(size: usize) -> io::Result<Vec<u8>> {
536    let mut buf = vec![0u8; size];
537    getrandom::fill(&mut buf).map_err(io::Error::from)?;
538    Ok(buf)
539}
540
541#[cfg(any(unix, windows, target_os = "wasi"))]
542pub fn isatty(fd: i32) -> bool {
543    unsafe { suppress_iph!(libc::isatty(fd)) != 0 }
544}
545
546#[cfg(not(any(unix, windows, target_os = "wasi")))]
547pub fn isatty(_fd: i32) -> bool {
548    false
549}
550
551#[cfg(any(unix, windows))]
552pub fn system(command: &CStr) -> libc::c_int {
553    unsafe { libc::system(command.as_ptr()) }
554}
555
556#[cfg(target_os = "linux")]
557pub fn copy_file_range(
558    src: crt_fd::Borrowed<'_>,
559    offset_src: Option<&mut u64>,
560    dst: crt_fd::Borrowed<'_>,
561    offset_dst: Option<&mut u64>,
562    count: usize,
563) -> rustix::io::Result<usize> {
564    // `copy_file_range` isn't wrapped in every libc (i.e. musl).
565    // However, Rustix is a safe wrapper around the syscall that bypasses libc.
566    rustix::fs::copy_file_range(src, offset_src, dst, offset_dst, count)
567}
568
569#[cfg(windows)]
570pub fn seek_fd(
571    fd: crt_fd::Borrowed<'_>,
572    position: crt_fd::Offset,
573    how: i32,
574) -> io::Result<crt_fd::Offset> {
575    use crate::windows::CheckWin32Bool;
576
577    let handle = crt_fd::as_handle(fd)?;
578    // `SetFilePointer` returns the low half of the new position and reports
579    // failure with the value a position four gigabytes in also has, so the two
580    // are only told apart through the error code. The `Ex` form answers with
581    // the whole position and a success flag of its own.
582    let mut new_position = 0;
583    unsafe {
584        SetFilePointerEx(
585            handle.as_raw_handle(),
586            position,
587            &mut new_position,
588            how as _,
589        )
590    }
591    .check_win32_bool()?;
592    Ok(new_position)
593}
594
595#[cfg(any(unix, target_os = "wasi"))]
596pub fn seek_fd(
597    fd: crt_fd::Borrowed<'_>,
598    position: crt_fd::Offset,
599    how: i32,
600) -> io::Result<crt_fd::Offset> {
601    unsafe { suppress_iph!(libc::lseek(fd.as_raw(), position, how)) }.check_libc_neg()
602}
603
604#[cfg(windows)]
605fn filetime_from_duration(duration: Duration) -> FILETIME {
606    let intervals = ((duration.as_secs() as i64 + 11644473600) * 10_000_000)
607        + (duration.subsec_nanos() as i64 / 100);
608    FILETIME {
609        dwLowDateTime: intervals as u32,
610        dwHighDateTime: (intervals >> 32) as u32,
611    }
612}
613
614#[cfg(windows)]
615pub fn set_file_times(
616    path: impl AsRef<Path>,
617    access: Duration,
618    modified: Duration,
619) -> io::Result<()> {
620    use crate::windows::CheckWin32Bool;
621    let access = filetime_from_duration(access);
622    let modified = filetime_from_duration(modified);
623    let file = fs::open_write_with_custom_flags(path, FILE_FLAG_BACKUP_SEMANTICS)?;
624    unsafe {
625        SetFileTime(
626            file.as_raw_handle() as _,
627            core::ptr::null(),
628            &access,
629            &modified,
630        )
631    }
632    .check_win32_bool()
633}
634
635pub trait ErrorExt {
636    fn posix_errno(&self) -> i32;
637}
638
639impl ErrorExt for io::Error {
640    #[cfg(not(windows))]
641    fn posix_errno(&self) -> i32 {
642        self.raw_os_error().unwrap_or(0)
643    }
644    #[cfg(windows)]
645    fn posix_errno(&self) -> i32 {
646        // A C runtime error carries its exact errno as the payload; report it
647        // directly instead of round-tripping through a Win32 error code.
648        if let Some(crt) = self.get_ref().and_then(|e| e.downcast_ref::<CrtErrno>()) {
649            return crt.0;
650        }
651        let winerror = self.raw_os_error().unwrap_or(0);
652        winerror_to_errno(winerror)
653    }
654}
655
656/// Wraps a raw C runtime `errno` inside an [`io::Error`].
657///
658/// CRT functions (`open`, `read`, `dup`, ...) report failures through `errno`,
659/// not `GetLastError`. Translating that `errno` into a Win32 error code is
660/// lossy — any value missing from [`errno_to_winerror`] collapses to `EINVAL` —
661/// and also attaches a spurious `winerror` to the resulting `OSError`. Carrying
662/// the `errno` as the error payload lets [`ErrorExt::posix_errno`] recover it
663/// exactly while leaving `raw_os_error()` empty, so no `winerror` is reported.
664#[cfg(windows)]
665#[derive(Debug)]
666struct CrtErrno(i32);
667
668#[cfg(windows)]
669impl core::fmt::Display for CrtErrno {
670    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
671        match crate::errno::strerror_string(self.0) {
672            Some(msg) => f.write_str(&msg),
673            None => write!(f, "os error {}", self.0),
674        }
675    }
676}
677
678#[cfg(windows)]
679impl core::error::Error for CrtErrno {}
680
681/// Build an [`io::Error`] that preserves a raw C runtime `errno`.
682///
683/// The [`io::ErrorKind`] is derived from the closest Win32 mapping so callers
684/// matching on `kind()` keep working, while the exact `errno` is preserved for
685/// [`ErrorExt::posix_errno`].
686#[cfg(windows)]
687#[must_use]
688pub fn io_error_from_errno(errno: i32) -> io::Error {
689    let kind = io::Error::from_raw_os_error(errno_to_winerror(errno)).kind();
690    io::Error::new(kind, CrtErrno(errno))
691}
692
693#[cfg(all(not(windows), not(target_arch = "wasm32")))]
694impl ErrorExt for rustix::io::Errno {
695    fn posix_errno(&self) -> i32 {
696        self.raw_os_error()
697    }
698}
699
700/// Get the last error from C runtime library functions (like _dup, _dup2, _fstat, etc.)
701/// CRT functions set errno, not GetLastError(), so we need to read errno directly.
702#[cfg(windows)]
703#[must_use]
704pub fn errno_io_error() -> io::Error {
705    io_error_from_errno(get_errno())
706}
707
708#[cfg(not(windows))]
709#[must_use]
710pub fn errno_io_error() -> io::Error {
711    std::io::Error::last_os_error()
712}
713
714/// Convert a libc-style return value into an `io::Result`.
715///
716/// Negative values are treated as errors; errno is read via [`errno_io_error`].
717/// Modeled after PyPy's `rposix.handle_posix_error`.
718pub trait CheckLibcResult: Sized {
719    /// Returns `Ok(self)` if non-negative, otherwise `Err` with the current errno.
720    fn check_libc_neg(self) -> io::Result<Self>;
721}
722
723macro_rules! impl_check_libc_result {
724    ($($ty:ty),* $(,)?) => {
725        $(
726            impl CheckLibcResult for $ty {
727                #[inline]
728                fn check_libc_neg(self) -> io::Result<Self> {
729                    if self < 0 { Err(errno_io_error()) } else { Ok(self) }
730                }
731            }
732        )*
733    };
734}
735
736impl_check_libc_result!(i16, i32, i64, isize);
737
738/// libc convention where `0` means success and any non-zero value indicates failure
739/// (with errno set). Used by APIs like `sigemptyset`, `sigaction`, `pthread_*`, etc.
740pub trait CheckLibcZero {
741    /// Returns `Ok(())` if `self == 0`, otherwise the current errno as an `io::Error`.
742    fn check_libc_zero(self) -> io::Result<()>;
743}
744
745macro_rules! impl_check_libc_zero {
746    ($($ty:ty),* $(,)?) => {
747        $(
748            impl CheckLibcZero for $ty {
749                #[inline]
750                fn check_libc_zero(self) -> io::Result<()> {
751                    if self == 0 { Ok(()) } else { Err(errno_io_error()) }
752                }
753            }
754        )*
755    };
756}
757
758impl_check_libc_zero!(i32, i64, isize);
759
760#[cfg(windows)]
761pub fn get_errno() -> i32 {
762    unsafe extern "C" {
763        fn _get_errno(pValue: *mut i32) -> i32;
764    }
765    let mut errno = 0;
766    unsafe { suppress_iph!(_get_errno(&mut errno)) };
767    errno
768}
769
770#[cfg(not(windows))]
771#[must_use]
772pub fn get_errno() -> i32 {
773    std::io::Error::last_os_error().posix_errno()
774}
775
776pub fn clear_errno() {
777    set_errno(0);
778}
779
780/// Set errno to the specified value.
781#[cfg(windows)]
782pub fn set_errno(value: i32) {
783    unsafe extern "C" {
784        fn _set_errno(value: i32) -> i32;
785    }
786    unsafe { suppress_iph!(_set_errno(value)) };
787}
788
789#[cfg(unix)]
790pub fn set_errno(value: i32) {
791    nix::errno::Errno::from_raw(value).set();
792}
793
794#[cfg(target_os = "wasi")]
795pub fn set_errno(value: i32) {
796    unsafe {
797        *libc::__errno_location() = value;
798    }
799}
800
801#[cfg(not(any(unix, windows, target_os = "wasi")))]
802pub fn set_errno(_value: i32) {}
803
804// WASIp1, like Unix, provides byte-preserving OsStr conversions.
805#[cfg(any(unix, all(target_os = "wasi", not(target_env = "p2"))))]
806pub fn bytes_as_os_str(b: &[u8]) -> Result<&std::ffi::OsStr, Utf8Error> {
807    use self::ffi::OsStrExt;
808    Ok(std::ffi::OsStr::from_bytes(b))
809}
810
811#[cfg(not(any(unix, all(target_os = "wasi", not(target_env = "p2")))))]
812pub fn bytes_as_os_str(b: &[u8]) -> Result<&std::ffi::OsStr, Utf8Error> {
813    Ok(core::str::from_utf8(b)?.as_ref())
814}
815
816#[cfg(unix)]
817pub use std::os::unix::ffi;
818
819// WASIp1 uses stable std::os::wasi::ffi
820#[cfg(all(target_os = "wasi", not(target_env = "p2")))]
821pub use std::os::wasi::ffi;
822
823// WASIp2: std::os::wasip2::ffi is unstable, so we provide a stable implementation
824// leveraging WASI's UTF-8 string guarantee
825#[cfg(all(target_os = "wasi", target_env = "p2"))]
826pub mod ffi {
827    use std::ffi::{OsStr, OsString};
828
829    pub trait OsStrExt: sealed::Sealed {
830        fn as_bytes(&self) -> &[u8];
831        fn from_bytes(slice: &[u8]) -> &Self;
832    }
833
834    impl OsStrExt for OsStr {
835        fn as_bytes(&self) -> &[u8] {
836            // WASI strings are guaranteed to be UTF-8
837            self.to_str().expect("wasip2 strings are UTF-8").as_bytes()
838        }
839
840        fn from_bytes(slice: &[u8]) -> &OsStr {
841            // WASI strings are guaranteed to be UTF-8
842            OsStr::new(core::str::from_utf8(slice).expect("wasip2 strings are UTF-8"))
843        }
844    }
845
846    pub trait OsStringExt: sealed::Sealed {
847        fn from_vec(vec: Vec<u8>) -> Self;
848        fn into_vec(self) -> Vec<u8>;
849    }
850
851    impl OsStringExt for OsString {
852        fn from_vec(vec: Vec<u8>) -> OsString {
853            // WASI strings are guaranteed to be UTF-8
854            OsString::from(String::from_utf8(vec).expect("wasip2 strings are UTF-8"))
855        }
856
857        fn into_vec(self) -> Vec<u8> {
858            // WASI strings are guaranteed to be UTF-8
859            self.to_str()
860                .expect("wasip2 strings are UTF-8")
861                .as_bytes()
862                .to_vec()
863        }
864    }
865
866    mod sealed {
867        pub trait Sealed {}
868        impl Sealed for std::ffi::OsStr {}
869        impl Sealed for std::ffi::OsString {}
870    }
871}
872
873#[cfg(windows)]
874#[must_use]
875pub fn errno_to_winerror(errno: i32) -> i32 {
876    use libc::*;
877    use windows_sys::Win32::Foundation::*;
878    let winerror = match errno {
879        ENOENT => ERROR_FILE_NOT_FOUND,
880        E2BIG => ERROR_BAD_ENVIRONMENT,
881        ENOEXEC => ERROR_BAD_FORMAT,
882        EBADF => ERROR_INVALID_HANDLE,
883        ECHILD => ERROR_WAIT_NO_CHILDREN,
884        EAGAIN => ERROR_NO_PROC_SLOTS,
885        ENOMEM => ERROR_NOT_ENOUGH_MEMORY,
886        EACCES => ERROR_ACCESS_DENIED,
887        EEXIST => ERROR_FILE_EXISTS,
888        EXDEV => ERROR_NOT_SAME_DEVICE,
889        ENOTDIR => ERROR_DIRECTORY,
890        EMFILE => ERROR_TOO_MANY_OPEN_FILES,
891        ENOSPC => ERROR_DISK_FULL,
892        EPIPE => ERROR_BROKEN_PIPE,
893        ENOTEMPTY => ERROR_DIR_NOT_EMPTY,
894        EILSEQ => ERROR_NO_UNICODE_TRANSLATION,
895        EINVAL => ERROR_INVALID_FUNCTION,
896        _ => ERROR_INVALID_FUNCTION,
897    };
898    winerror as _
899}
900
901// winerror: https://learn.microsoft.com/windows/win32/debug/system-error-codes--0-499-
902// errno: https://learn.microsoft.com/cpp/c-runtime-library/errno-constants?view=msvc-170
903#[cfg(windows)]
904#[must_use]
905pub fn winerror_to_errno(winerror: i32) -> i32 {
906    use libc::*;
907    use windows_sys::Win32::{
908        Foundation::*,
909        Networking::WinSock::{
910            WSAEACCES, WSAEBADF, WSAECONNABORTED, WSAECONNREFUSED, WSAECONNRESET, WSAEFAULT,
911            WSAEINTR, WSAEINVAL, WSAEMFILE,
912        },
913    };
914    // Unwrap FACILITY_WIN32 HRESULT errors.
915    // if ((winerror & 0xFFFF0000) == 0x80070000) {
916    //     winerror &= 0x0000FFFF;
917    // }
918
919    // Winsock error codes (10000-11999) are errno values.
920    if (10000..12000).contains(&winerror) {
921        match winerror {
922            WSAEINTR | WSAEBADF | WSAEACCES | WSAEFAULT | WSAEINVAL | WSAEMFILE => {
923                // Winsock definitions of errno values. See WinSock2.h
924                return winerror - 10000;
925            }
926            _ => return winerror as _,
927        }
928    }
929
930    #[allow(non_upper_case_globals)]
931    match winerror as u32 {
932        ERROR_FILE_NOT_FOUND
933        | ERROR_PATH_NOT_FOUND
934        | ERROR_INVALID_DRIVE
935        | ERROR_NO_MORE_FILES
936        | ERROR_BAD_NETPATH
937        | ERROR_BAD_NET_NAME
938        | ERROR_BAD_PATHNAME
939        | ERROR_FILENAME_EXCED_RANGE => ENOENT,
940        ERROR_BAD_ENVIRONMENT => E2BIG,
941        ERROR_BAD_FORMAT
942        | ERROR_INVALID_STARTING_CODESEG
943        | ERROR_INVALID_STACKSEG
944        | ERROR_INVALID_MODULETYPE
945        | ERROR_INVALID_EXE_SIGNATURE
946        | ERROR_EXE_MARKED_INVALID
947        | ERROR_BAD_EXE_FORMAT
948        | ERROR_ITERATED_DATA_EXCEEDS_64k
949        | ERROR_INVALID_MINALLOCSIZE
950        | ERROR_DYNLINK_FROM_INVALID_RING
951        | ERROR_IOPL_NOT_ENABLED
952        | ERROR_INVALID_SEGDPL
953        | ERROR_AUTODATASEG_EXCEEDS_64k
954        | ERROR_RING2SEG_MUST_BE_MOVABLE
955        | ERROR_RELOC_CHAIN_XEEDS_SEGLIM
956        | ERROR_INFLOOP_IN_RELOC_CHAIN => ENOEXEC,
957        ERROR_INVALID_HANDLE | ERROR_INVALID_TARGET_HANDLE | ERROR_DIRECT_ACCESS_HANDLE => EBADF,
958        ERROR_WAIT_NO_CHILDREN | ERROR_CHILD_NOT_COMPLETE => ECHILD,
959        ERROR_NO_PROC_SLOTS | ERROR_MAX_THRDS_REACHED | ERROR_NESTING_NOT_ALLOWED => EAGAIN,
960        ERROR_ARENA_TRASHED
961        | ERROR_NOT_ENOUGH_MEMORY
962        | ERROR_INVALID_BLOCK
963        | ERROR_NOT_ENOUGH_QUOTA => ENOMEM,
964        ERROR_ACCESS_DENIED
965        | ERROR_CURRENT_DIRECTORY
966        | ERROR_WRITE_PROTECT
967        | ERROR_BAD_UNIT
968        | ERROR_NOT_READY
969        | ERROR_BAD_COMMAND
970        | ERROR_CRC
971        | ERROR_BAD_LENGTH
972        | ERROR_SEEK
973        | ERROR_NOT_DOS_DISK
974        | ERROR_SECTOR_NOT_FOUND
975        | ERROR_OUT_OF_PAPER
976        | ERROR_WRITE_FAULT
977        | ERROR_READ_FAULT
978        | ERROR_GEN_FAILURE
979        | ERROR_SHARING_VIOLATION
980        | ERROR_LOCK_VIOLATION
981        | ERROR_WRONG_DISK
982        | ERROR_SHARING_BUFFER_EXCEEDED
983        | ERROR_NETWORK_ACCESS_DENIED
984        | ERROR_CANNOT_MAKE
985        | ERROR_FAIL_I24
986        | ERROR_DRIVE_LOCKED
987        | ERROR_SEEK_ON_DEVICE
988        | ERROR_NOT_LOCKED
989        | ERROR_LOCK_FAILED
990        | 35 => EACCES,
991        ERROR_FILE_EXISTS | ERROR_ALREADY_EXISTS => EEXIST,
992        ERROR_NOT_SAME_DEVICE => EXDEV,
993        ERROR_DIRECTORY => ENOTDIR,
994        ERROR_TOO_MANY_OPEN_FILES => EMFILE,
995        ERROR_DISK_FULL => ENOSPC,
996        ERROR_BROKEN_PIPE | ERROR_NO_DATA => EPIPE,
997        ERROR_DIR_NOT_EMPTY => ENOTEMPTY,
998        ERROR_NO_UNICODE_TRANSLATION => EILSEQ,
999        // Connection-related Windows error codes - map to Winsock error codes
1000        // which Python uses on Windows (errno.ECONNREFUSED = 10061, etc.)
1001        ERROR_CONNECTION_REFUSED => WSAECONNREFUSED,
1002        ERROR_CONNECTION_ABORTED => WSAECONNABORTED,
1003        ERROR_NETNAME_DELETED => WSAECONNRESET,
1004        ERROR_INVALID_FUNCTION
1005        | ERROR_INVALID_ACCESS
1006        | ERROR_INVALID_DATA
1007        | ERROR_INVALID_PARAMETER
1008        | ERROR_NEGATIVE_SEEK => EINVAL,
1009        _ => EINVAL,
1010    }
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015    #[test]
1016    fn o_rdonly_is_zero() {
1017        assert_eq!(super::O_RDONLY, 0);
1018    }
1019
1020    #[test]
1021    fn o_wronly_is_one() {
1022        assert_eq!(super::O_WRONLY, 1);
1023    }
1024}