Skip to content

Commit 3ee16f5

Browse files
committed
Honor index protocol when opening files
Assisted-by: dot:frontier-intelligence
1 parent 4f6990d commit 3ee16f5

2 files changed

Lines changed: 68 additions & 79 deletions

File tree

‎crates/vm/src/ospath_fd.rs‎

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,6 @@ impl PathConverter {
2828
self.try_pathlike(obj, true, vm).map(OsPathOrFd::Path)
2929
}
3030

31-
/// Preserve descriptor-first validation for the I/O open path.
32-
#[cfg(all(unix, feature = "host_env"))]
33-
pub(crate) fn try_fd_first_path_or_fd<'fd>(
34-
&self,
35-
obj: PyObjectRef,
36-
vm: &VirtualMachine,
37-
) -> PyResult<OsPathOrFd<'fd>> {
38-
if let Some(fd) = self.try_index_as_fd(&obj, vm) {
39-
return fd;
40-
}
41-
self.try_path_inner(obj, true, vm).map(OsPathOrFd::Path)
42-
}
43-
4431
fn try_index_as_fd<'fd>(
4532
&self,
4633
obj: &PyObjectRef,

‎crates/vm/src/stdlib/_io.rs‎

Lines changed: 68 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,32 @@ use crate::{
2626
pub use _io::{OpenArgs, io_open as open};
2727
use rustpython_host_env::io as host_io;
2828

29+
/// Mirror the file descriptor conversion used by FileIO and WindowsConsoleIO.
30+
#[cfg(feature = "host_env")]
31+
fn open_file_descriptor(name: &PyObject, vm: &VirtualMachine) -> PyResult<Option<i32>> {
32+
if name.class().is(vm.ctx.types.bool_type) {
33+
crate::stdlib::_warnings::warn(
34+
vm.ctx.exceptions.runtime_warning,
35+
"bool is used as a file descriptor".to_owned(),
36+
1,
37+
vm,
38+
)?;
39+
}
40+
// CPython clears failed integer conversions and then tries a path. Do not
41+
// turn a failed conversion into a negative descriptor error.
42+
let fd = if let Some(i) = name.downcast_ref::<crate::builtins::PyInt>() {
43+
i.try_to_primitive::<i32>(vm).ok()
44+
} else {
45+
name.try_index_opt(vm)
46+
.and_then(Result::ok)
47+
.and_then(|i| i.try_to_primitive::<i32>(vm).ok())
48+
};
49+
if fd.is_some_and(|fd| fd < 0) {
50+
return Err(vm.new_value_error("negative file descriptor"));
51+
}
52+
Ok(fd)
53+
}
54+
2955
fn file_closed(file: &PyObject, vm: &VirtualMachine) -> PyResult<bool> {
3056
file.get_attr("closed", vm)?.try_to_bool(vm)
3157
}
@@ -5789,6 +5815,14 @@ mod _io {
57895815
opts: OpenArgs,
57905816
vm: &VirtualMachine,
57915817
) -> PyResult {
5818+
// Match _io.open: numeric objects go to FileIO unchanged; other
5819+
// arguments are resolved through the path protocol before mode parsing.
5820+
let file = if crate::protocol::PyNumber::check(&file) {
5821+
file
5822+
} else {
5823+
FsPath::try_from_path_like(file.clone(), false, vm)?.to_pyobject(vm)
5824+
};
5825+
57925826
// mode is optional: 'rt' is the default mode (open from reading text)
57935827
let mode_string = mode.unwrap_or("r");
57945828
let mode = mode_string
@@ -5810,24 +5844,6 @@ mod _io {
58105844
}
58115845
}
58125846

5813-
// Match CPython's _pyio.open (Lib/_pyio.py): resolve PathLike here so
5814-
// _io.FileIO still receives (and preserves on .name) the raw argument
5815-
// when called directly.
5816-
let file = if file.fast_isinstance(vm.ctx.types.int_type) {
5817-
file
5818-
} else {
5819-
FsPath::try_from_path_like(file, true, vm)?.to_pyobject(vm)
5820-
};
5821-
5822-
// check file descriptor validity
5823-
#[cfg(all(unix, feature = "host_env"))]
5824-
if let Ok(crate::ospath::OsPathOrFd::Fd(fd)) =
5825-
crate::ospath::PathConverter::new().try_fd_first_path_or_fd(file.clone(), vm)
5826-
{
5827-
rustpython_host_env::fcntl::validate_fd(fd.as_raw())
5828-
.map_err(|_| vm.new_last_errno_error())?;
5829-
}
5830-
58315847
// Construct a RawIO (subclass of RawIOBase)
58325848
// On Windows, use _WindowsConsoleIO for console handles.
58335849
// This is subsequently consumed by a Buffered Class.
@@ -6105,7 +6121,7 @@ mod fileio {
61056121
builtins::{PyBaseExceptionRef, PyUtf8Str, PyUtf8StrRef},
61066122
common::wtf8::Wtf8Buf,
61076123
convert::{IntoPyException, ToPyException},
6108-
exceptions::OSErrorBuilder,
6124+
exceptions::{OSErrorBuilder, ToOSErrorBuilder},
61096125
function::{ArgBytesLike, ArgMemoryBuffer},
61106126
ospath::{OsPath, OsPathOrFd},
61116127
stdlib::os,
@@ -6162,21 +6178,10 @@ mod fileio {
61626178
fn init(zelf: &Py<Self>, args: Self::Args, vm: &VirtualMachine) -> PyResult<()> {
61636179
// TODO: let atomic_flag_works
61646180
let name = args.file;
6165-
// Check if bool is used as file descriptor
6166-
if name.class().is(vm.ctx.types.bool_type) {
6167-
crate::stdlib::_warnings::warn(
6168-
vm.ctx.exceptions.runtime_warning,
6169-
"bool is used as a file descriptor".to_owned(),
6170-
1,
6171-
vm,
6172-
)?;
6173-
}
6174-
let arg_fd = if let Some(i) = name.downcast_ref::<crate::builtins::PyInt>() {
6175-
let fd = i.try_to_primitive(vm)?;
6176-
if fd < 0 {
6177-
return Err(vm.new_value_error("negative file descriptor"));
6178-
}
6179-
Some(fd)
6181+
let arg_fd = super::open_file_descriptor(&name, vm)?;
6182+
// Resolve the path before mode validation, even with a custom opener.
6183+
let mut path = if arg_fd.is_none() {
6184+
Some(OsPath::try_from_fspath(name.clone(), vm)?)
61806185
} else {
61816186
None
61826187
};
@@ -6191,7 +6196,7 @@ mod fileio {
61916196
let flags = parsed.flags;
61926197
zelf.mode.store(mode);
61936198

6194-
let (fd, filename) = if let Some(fd) = arg_fd {
6199+
let (fd, _filename) = if let Some(fd) = arg_fd {
61956200
zelf.closefd.store(args.closefd);
61966201
(fd, None)
61976202
} else {
@@ -6211,7 +6216,7 @@ mod fileio {
62116216
}
62126217
(fd, None)
62136218
} else {
6214-
let path = OsPath::try_from_fspath(name.clone(), vm)?;
6219+
let path = path.take().expect("a filename was resolved above");
62156220
#[cfg(any(unix, target_os = "wasi"))]
62166221
let raw_path = path.clone().into_cstring(vm)?;
62176222
#[cfg(windows)]
@@ -6233,8 +6238,6 @@ mod fileio {
62336238
let fd_is_own = arg_fd.is_none();
62346239
zelf.fd.store(fd);
62356240
let fd = unsafe { crt_fd::Borrowed::borrow_raw(fd) };
6236-
let filename = filename.unwrap_or(OsPathOrFd::Fd(fd));
6237-
62386241
// TODO: _Py_set_inheritable
62396242

62406243
match host_io::inspect_file_target(fd) {
@@ -6247,7 +6250,13 @@ mod fileio {
62476250
if host_io::should_forget_fd_after_inspect_error(&err, fd_is_own) {
62486251
zelf.fd.store(-1);
62496252
}
6250-
return Err(OSErrorBuilder::with_filename(&err, filename, vm));
6253+
if err.posix_errno() == libc::EBADF {
6254+
return Err(err.to_pyexception(vm));
6255+
}
6256+
return Err(err
6257+
.to_os_error_builder(vm)
6258+
.filename(name.clone())
6259+
.into_pyexception(vm));
62516260
}
62526261
}
62536262

@@ -6645,7 +6654,7 @@ mod winconsoleio {
66456654
builtins::{PyBaseExceptionRef, PyUtf8StrRef},
66466655
common::{lock::PyMutex, wtf8::Wtf8Buf},
66476656
convert::{IntoPyException, ToPyException},
6648-
function::{ArgBytesLike, ArgMemoryBuffer},
6657+
function::{ArgBytesLike, ArgMemoryBuffer, FsPath},
66496658
types::{Constructor, DefaultConstructor, Destructor, Initializer, Representable},
66506659
};
66516660
use crossbeam_utils::atomic::AtomicCell;
@@ -6742,24 +6751,22 @@ mod winconsoleio {
67426751
}
67436752
}
67446753

6745-
// Warn if bool is used as file descriptor
6746-
if nameobj.class().is(vm.ctx.types.bool_type) {
6747-
crate::stdlib::_warnings::warn(
6748-
vm.ctx.exceptions.runtime_warning,
6749-
"bool is used as a file descriptor".to_owned(),
6750-
1,
6751-
vm,
6752-
)?;
6753-
}
6754-
6755-
// Try to get fd from integer
6756-
let mut fd: i32 = -1;
6757-
if let Some(i) = nameobj.downcast_ref::<crate::builtins::PyInt>() {
6758-
fd = i.try_to_primitive::<i32>(vm).unwrap_or(-1);
6759-
if fd < 0 {
6760-
return Err(vm.new_value_error("negative file descriptor"));
6761-
}
6762-
}
6754+
let mut fd = super::open_file_descriptor(&nameobj, vm)?.unwrap_or(-1);
6755+
let path = if fd < 0 {
6756+
let path = FsPath::try_from_path_like(nameobj.clone(), true, vm)?;
6757+
let decoded = match &path {
6758+
FsPath::Str(s) => s.to_owned(),
6759+
FsPath::Bytes(_) => vm.fsdecode(path.as_os_str(vm)?.into_owned()),
6760+
};
6761+
let wide = decoded
6762+
.as_wtf8()
6763+
.to_wide_cstring()
6764+
.map_err(|e| e.to_pyexception(vm))?;
6765+
let console_type = pyio_get_console_type(&decoded, vm);
6766+
Some((wide, console_type))
6767+
} else {
6768+
None
6769+
};
67636770

67646771
// Parse mode
67656772
let mode_str: &str = args.mode.as_deref().map_or("r", |s| s.as_str());
@@ -6802,8 +6809,9 @@ mod winconsoleio {
68026809
zelf.writable.store(writable);
68036810

68046811
if fd < 0 {
6805-
// Get console type from name
6806-
console_type = pyio_get_console_type(&nameobj, vm);
6812+
// The decoded name was resolved before mode validation.
6813+
let (wide, detected_type) = path.expect("a filename was resolved above");
6814+
console_type = detected_type;
68076815
if console_type == 'x' {
68086816
if writable {
68096817
console_type = 'w';
@@ -6818,12 +6826,6 @@ mod winconsoleio {
68186826
return Err(vm.new_value_error("Cannot use closefd=False with file name"));
68196827
}
68206828

6821-
let name_str = nameobj.str(vm)?;
6822-
let wide = name_str
6823-
.as_wtf8()
6824-
.to_wide_cstring()
6825-
.map_err(|e| e.to_pyexception(vm))?;
6826-
68276829
fd = host_nt::open_console_path_fd(&wide, writable)
68286830
.map_err(|err| err.to_pyexception(vm))?;
68296831
} else {

0 commit comments

Comments
 (0)