Skip to main content

rustpython_host_env/
io.rs

1#[cfg(any(unix, target_os = "wasi"))]
2use core::ffi::CStr;
3use std::io;
4
5#[cfg(any(unix, target_os = "wasi"))]
6use rustix::{fs::FileType, io::Errno};
7
8#[cfg(any(unix, target_os = "wasi"))]
9use crate::fileutils;
10use crate::{crt_fd, os};
11
12bitflagset::bitflag! {
13    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
14    #[repr(u8)]
15    pub enum FileModeFlag {
16        Created = 0,
17        Readable = 1,
18        Writable = 2,
19        Appending = 3,
20    }
21}
22
23bitflagset::bitflagset! {
24    #[derive(Copy, Clone, PartialEq, Eq)]
25    pub struct FileMode(u8): FileModeFlag
26}
27
28impl FileMode {
29    pub const CREATED: Self = Self::from_element(FileModeFlag::Created);
30    pub const READABLE: Self = Self::from_element(FileModeFlag::Readable);
31    pub const WRITABLE: Self = Self::from_element(FileModeFlag::Writable);
32    pub const APPENDING: Self = Self::from_element(FileModeFlag::Appending);
33}
34
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub enum FileModeError {
37    Invalid,
38    BadRwa,
39}
40
41impl FileModeError {
42    pub fn error_msg(self, mode_str: &str) -> String {
43        match self {
44            Self::Invalid => format!("invalid mode: {mode_str}"),
45            Self::BadRwa => {
46                "Must have exactly one of create/read/write/append mode and at most one plus"
47                    .to_owned()
48            }
49        }
50    }
51}
52
53#[derive(Clone, Copy, Debug)]
54pub struct ParsedFileMode {
55    pub mode: FileMode,
56    pub flags: i32,
57}
58
59impl FileMode {
60    pub const fn raw_mode(self) -> &'static str {
61        if self.contains(&FileModeFlag::Created) {
62            if self.contains(&FileModeFlag::Readable) {
63                "xb+"
64            } else {
65                "xb"
66            }
67        } else if self.contains(&FileModeFlag::Appending) {
68            if self.contains(&FileModeFlag::Readable) {
69                "ab+"
70            } else {
71                "ab"
72            }
73        } else if self.contains(&FileModeFlag::Readable) {
74            if self.contains(&FileModeFlag::Writable) {
75                "rb+"
76            } else {
77                "rb"
78            }
79        } else {
80            "wb"
81        }
82    }
83}
84
85pub fn parse_fileio_mode(mode_str: &str) -> Result<ParsedFileMode, FileModeError> {
86    let mut flags = 0;
87    let mut plus = false;
88    let mut rwa = false;
89    let mut mode = FileMode::empty();
90    for c in mode_str.bytes() {
91        match c {
92            b'x' => {
93                if rwa {
94                    return Err(FileModeError::BadRwa);
95                }
96                rwa = true;
97                mode |= FileMode::WRITABLE | FileMode::CREATED;
98                flags |= os::O_EXCL | os::O_CREAT;
99            }
100            b'r' => {
101                if rwa {
102                    return Err(FileModeError::BadRwa);
103                }
104                rwa = true;
105                mode |= FileMode::READABLE;
106            }
107            b'w' => {
108                if rwa {
109                    return Err(FileModeError::BadRwa);
110                }
111                rwa = true;
112                mode |= FileMode::WRITABLE;
113                flags |= os::O_CREAT | os::O_TRUNC;
114            }
115            b'a' => {
116                if rwa {
117                    return Err(FileModeError::BadRwa);
118                }
119                rwa = true;
120                mode |= FileMode::WRITABLE | FileMode::APPENDING;
121                flags |= os::O_APPEND | os::O_CREAT;
122            }
123            b'+' => {
124                if plus {
125                    return Err(FileModeError::BadRwa);
126                }
127                plus = true;
128                mode |= FileMode::READABLE | FileMode::WRITABLE;
129            }
130            b'b' => {}
131            _ => return Err(FileModeError::Invalid),
132        }
133    }
134
135    if !rwa {
136        return Err(FileModeError::BadRwa);
137    }
138
139    if mode.is_superset(&(FileMode::READABLE | FileMode::WRITABLE)) {
140        flags |= os::O_RDWR;
141    } else if mode.contains(&FileModeFlag::Readable) {
142        flags |= os::O_RDONLY;
143    } else {
144        flags |= os::O_WRONLY;
145    }
146
147    #[cfg(windows)]
148    {
149        flags |= os::O_BINARY | os::O_NOINHERIT;
150    }
151    #[cfg(unix)]
152    {
153        flags |= os::O_CLOEXEC;
154    }
155
156    Ok(ParsedFileMode { mode, flags })
157}
158
159#[derive(Clone, Copy, Debug)]
160pub struct FileTargetInfo {
161    pub blksize: Option<i64>,
162}
163
164#[cfg(any(unix, target_os = "wasi"))]
165pub fn inspect_file_target(fd: crt_fd::Borrowed<'_>) -> io::Result<FileTargetInfo> {
166    let status = fileutils::fstat(fd)?;
167    if FileType::from_raw_mode(status.st_mode).is_dir() {
168        return Err(io::Error::from(Errno::ISDIR));
169    }
170    #[allow(clippy::useless_conversion, reason = "needed for 32-bit platforms")]
171    let blksize = (status.st_blksize > 1).then(|| i64::from(status.st_blksize));
172    Ok(FileTargetInfo { blksize })
173}
174
175#[cfg(windows)]
176pub fn inspect_file_target(fd: crt_fd::Borrowed<'_>) -> io::Result<FileTargetInfo> {
177    if !crate::nt::fd_exists(fd) {
178        return Err(io::Error::from_raw_os_error(
179            crate::nt::ERROR_INVALID_HANDLE_I32,
180        ));
181    }
182    Ok(FileTargetInfo { blksize: None })
183}
184
185#[cfg(any(unix, target_os = "wasi"))]
186pub fn open_path(path: &CStr, flags: i32, mode: i32) -> io::Result<crt_fd::Owned> {
187    crt_fd::open(path, flags, mode)
188}
189
190#[cfg(windows)]
191pub fn open_path(path: &widestring::WideCStr, flags: i32, mode: i32) -> io::Result<crt_fd::Owned> {
192    crt_fd::wopen(path, flags, mode)
193}
194
195#[cfg(windows)]
196pub fn should_forget_fd_after_inspect_error(err: &io::Error, _fd_is_own: bool) -> bool {
197    err.raw_os_error() == Some(crate::nt::ERROR_INVALID_HANDLE_I32)
198}
199
200#[cfg(any(unix, target_os = "wasi"))]
201pub fn should_forget_fd_after_inspect_error(err: &io::Error, fd_is_own: bool) -> bool {
202    let errno = err.raw_os_error();
203    (errno == Some(libc::EISDIR) || errno == Some(libc::EBADF))
204        && (!fd_is_own || errno == Some(libc::EBADF))
205}
206
207pub fn seek_to_end(fd: crt_fd::Borrowed<'_>) -> io::Result<crt_fd::Offset> {
208    os::seek_fd(fd, 0, libc::SEEK_END)
209}
210
211pub fn is_seekable(fd: crt_fd::Borrowed<'_>) -> bool {
212    os::seek_fd(fd, 0, libc::SEEK_CUR).is_ok()
213}
214
215/// Whether a read from `fd` answers from data the file already holds, rather
216/// than waiting for whoever writes the other end.
217///
218/// Seeking answers this everywhere but Windows, where a pipe seeks too --
219/// `lseek` on one succeeds and reports a position, so a reader that took
220/// seekability for an answer would wait on a peer while holding whatever it
221/// holds for the length of the call.
222#[cfg(not(windows))]
223pub fn reads_without_waiting(fd: crt_fd::Borrowed<'_>) -> bool {
224    is_seekable(fd)
225}
226
227#[cfg(windows)]
228pub fn reads_without_waiting(fd: crt_fd::Borrowed<'_>) -> bool {
229    use std::os::windows::io::AsRawHandle;
230    use windows_sys::Win32::Storage::FileSystem::{FILE_TYPE_DISK, GetFileType};
231
232    let Ok(handle) = crt_fd::as_handle(fd) else {
233        return false;
234    };
235    unsafe { GetFileType(handle.as_raw_handle() as _) == FILE_TYPE_DISK }
236}
237
238pub fn validate_whence(whence: i32) -> bool {
239    let standard = (0..=2).contains(&whence);
240    #[cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "linux"))]
241    {
242        standard || matches!(whence, libc::SEEK_DATA | libc::SEEK_HOLE)
243    }
244    #[cfg(not(any(target_os = "dragonfly", target_os = "freebsd", target_os = "linux")))]
245    {
246        standard
247    }
248}
249
250pub fn is_interrupted_errno(errno: i32) -> bool {
251    errno == libc::EINTR
252}
253
254pub fn is_interrupted_error(err: &io::Error) -> bool {
255    err.raw_os_error() == Some(libc::EINTR)
256}
257
258pub fn is_would_block_error(err: &io::Error) -> bool {
259    err.kind() == io::ErrorKind::WouldBlock || err.raw_os_error() == Some(libc::EAGAIN)
260}
261
262pub fn seek(
263    fd: crt_fd::Borrowed<'_>,
264    offset: crt_fd::Offset,
265    how: i32,
266) -> io::Result<crt_fd::Offset> {
267    os::seek_fd(fd, offset, how)
268}
269
270pub fn tell(fd: crt_fd::Borrowed<'_>) -> io::Result<crt_fd::Offset> {
271    os::seek_fd(fd, 0, libc::SEEK_CUR)
272}
273
274pub fn isatty(fd: i32) -> bool {
275    os::isatty(fd)
276}
277
278pub fn read_once(fd: crt_fd::Borrowed<'_>, buf: &mut [u8]) -> io::Result<usize> {
279    crt_fd::read(fd, buf)
280}
281
282pub fn read_all(fd: crt_fd::Borrowed<'_>, out: &mut Vec<u8>) -> io::Result<()> {
283    let mut fd = fd;
284    std::io::Read::read_to_end(&mut fd, out).map(|_| ())
285}
286
287pub fn write_once(fd: crt_fd::Borrowed<'_>, buf: &[u8]) -> io::Result<usize> {
288    crt_fd::write(fd, buf)
289}
290
291pub fn close_owned_fd(fd: crt_fd::Owned) -> io::Result<()> {
292    crt_fd::close(fd)
293}
294
295/// Async-signal-safe raw write to the platform stderr file descriptor.
296/// Avoids `std::io::stderr()` locking so it is safe to call from fork
297/// children and signal handlers.
298#[cfg(unix)]
299pub fn write_stderr_raw(buf: &[u8]) {
300    unsafe {
301        let _ = libc::write(libc::STDERR_FILENO, buf.as_ptr().cast(), buf.len());
302    }
303}