Repository navigation
Prioritize string paths over index conversion #9570
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
32834df
4e3005a
f664e6d
67ee8eb
6d1bff3
7c4bc15
fd6088c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,32 @@ use crate::{ | |
| pub use _io::{OpenArgs, io_open as open}; | ||
| use rustpython_host_env::io as host_io; | ||
|
|
||
| /// Mirror the file descriptor conversion used by FileIO and WindowsConsoleIO. | ||
| #[cfg(feature = "host_env")] | ||
| fn open_file_descriptor(name: &PyObject, vm: &VirtualMachine) -> PyResult<Option<i32>> { | ||
| if name.class().is(vm.ctx.types.bool_type) { | ||
| crate::stdlib::_warnings::warn( | ||
| vm.ctx.exceptions.runtime_warning, | ||
| "bool is used as a file descriptor".to_owned(), | ||
| 1, | ||
| vm, | ||
| )?; | ||
| } | ||
| // CPython clears failed integer conversions and then tries a path. Do not | ||
| // turn a failed conversion into a negative descriptor error. | ||
| let fd = if let Some(i) = name.downcast_ref::<crate::builtins::PyInt>() { | ||
| i.try_to_primitive::<i32>(vm).ok() | ||
| } else { | ||
| name.try_index_opt(vm) | ||
| .and_then(Result::ok) | ||
| .and_then(|i| i.try_to_primitive::<i32>(vm).ok()) | ||
| }; | ||
| if fd.is_some_and(|fd| fd < 0) { | ||
| return Err(vm.new_value_error("negative file descriptor")); | ||
| } | ||
| Ok(fd) | ||
| } | ||
|
|
||
| fn file_closed(file: &PyObject, vm: &VirtualMachine) -> PyResult<bool> { | ||
| file.get_attr("closed", vm)?.try_to_bool(vm) | ||
| } | ||
|
|
@@ -5789,6 +5815,15 @@ mod _io { | |
| opts: OpenArgs, | ||
| vm: &VirtualMachine, | ||
| ) -> PyResult { | ||
| // Match _io.open: numeric objects go to FileIO unchanged; other | ||
| // arguments are resolved through the path protocol before mode parsing. | ||
| let file = if crate::protocol::PyNumber::check(&file) { | ||
| file | ||
| } else { | ||
| // Keep the original argument alive through opening and wrapper setup. | ||
| FsPath::try_from_path_like(file.as_object().to_owned(), false, vm)?.to_pyobject(vm) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| }; | ||
|
|
||
| // mode is optional: 'rt' is the default mode (open from reading text) | ||
| let mode_string = mode.unwrap_or("r"); | ||
| let mode = mode_string | ||
|
|
@@ -5810,22 +5845,6 @@ mod _io { | |
| } | ||
| } | ||
|
|
||
| // Match CPython's _pyio.open (Lib/_pyio.py): resolve PathLike here so | ||
| // _io.FileIO still receives (and preserves on .name) the raw argument | ||
| // when called directly. | ||
| let file = if file.fast_isinstance(vm.ctx.types.int_type) { | ||
| file | ||
| } else { | ||
| FsPath::try_from_path_like(file, true, vm)?.to_pyobject(vm) | ||
| }; | ||
|
|
||
| // check file descriptor validity | ||
| #[cfg(all(unix, feature = "host_env"))] | ||
| if let Ok(crate::ospath::OsPathOrFd::Fd(fd)) = file.clone().try_into_value(vm) { | ||
| rustpython_host_env::fcntl::validate_fd(fd.as_raw()) | ||
| .map_err(|_| vm.new_last_errno_error())?; | ||
| } | ||
|
|
||
| // Construct a RawIO (subclass of RawIOBase) | ||
| // On Windows, use _WindowsConsoleIO for console handles. | ||
| // This is subsequently consumed by a Buffered Class. | ||
|
|
@@ -6103,7 +6122,7 @@ mod fileio { | |
| builtins::{PyBaseExceptionRef, PyUtf8Str, PyUtf8StrRef}, | ||
| common::wtf8::Wtf8Buf, | ||
| convert::{IntoPyException, ToPyException}, | ||
| exceptions::OSErrorBuilder, | ||
| exceptions::{OSErrorBuilder, ToOSErrorBuilder}, | ||
| function::{ArgBytesLike, ArgMemoryBuffer}, | ||
| ospath::{OsPath, OsPathOrFd}, | ||
| stdlib::os, | ||
|
|
@@ -6160,21 +6179,10 @@ mod fileio { | |
| fn init(zelf: &Py<Self>, args: Self::Args, vm: &VirtualMachine) -> PyResult<()> { | ||
| // TODO: let atomic_flag_works | ||
| let name = args.file; | ||
| // Check if bool is used as file descriptor | ||
| if name.class().is(vm.ctx.types.bool_type) { | ||
| crate::stdlib::_warnings::warn( | ||
| vm.ctx.exceptions.runtime_warning, | ||
| "bool is used as a file descriptor".to_owned(), | ||
| 1, | ||
| vm, | ||
| )?; | ||
| } | ||
| let arg_fd = if let Some(i) = name.downcast_ref::<crate::builtins::PyInt>() { | ||
| let fd = i.try_to_primitive(vm)?; | ||
| if fd < 0 { | ||
| return Err(vm.new_value_error("negative file descriptor")); | ||
| } | ||
| Some(fd) | ||
| let arg_fd = super::open_file_descriptor(&name, vm)?; | ||
| // Resolve the path before mode validation, even with a custom opener. | ||
| let mut path = if arg_fd.is_none() { | ||
| Some(OsPath::try_from_fspath(name.clone(), vm)?) | ||
| } else { | ||
| None | ||
| }; | ||
|
|
@@ -6189,7 +6197,7 @@ mod fileio { | |
| let flags = parsed.flags; | ||
| zelf.mode.store(mode); | ||
|
|
||
| let (fd, filename) = if let Some(fd) = arg_fd { | ||
| let (fd, _filename) = if let Some(fd) = arg_fd { | ||
| zelf.closefd.store(args.closefd); | ||
| (fd, None) | ||
| } else { | ||
|
|
@@ -6209,7 +6217,7 @@ mod fileio { | |
| } | ||
| (fd, None) | ||
| } else { | ||
| let path = OsPath::try_from_fspath(name.clone(), vm)?; | ||
| let path = path.take().expect("a filename was resolved above"); | ||
| #[cfg(any(unix, target_os = "wasi"))] | ||
| let raw_path = path.clone().into_cstring(vm)?; | ||
| #[cfg(windows)] | ||
|
|
@@ -6231,8 +6239,6 @@ mod fileio { | |
| let fd_is_own = arg_fd.is_none(); | ||
| zelf.fd.store(fd); | ||
| let fd = unsafe { crt_fd::Borrowed::borrow_raw(fd) }; | ||
| let filename = filename.unwrap_or(OsPathOrFd::Fd(fd)); | ||
|
|
||
| // TODO: _Py_set_inheritable | ||
|
|
||
| match host_io::inspect_file_target(fd) { | ||
|
|
@@ -6245,7 +6251,14 @@ mod fileio { | |
| if host_io::should_forget_fd_after_inspect_error(&err, fd_is_own) { | ||
| zelf.fd.store(-1); | ||
| } | ||
| return Err(OSErrorBuilder::with_filename(&err, filename, vm)); | ||
| if err.posix_errno() == libc::EBADF { | ||
| return Err(err.to_pyexception(vm)); | ||
| } | ||
| // Keep the original name alive through error construction and local cleanup. | ||
| return Err(err | ||
| .to_os_error_builder(vm) | ||
| .filename(name.as_object().to_owned()) | ||
| .into_pyexception(vm)); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -6644,7 +6657,7 @@ mod winconsoleio { | |
| common::{lock::PyMutex, wtf8::Wtf8Buf}, | ||
| convert::{IntoPyException, ToPyException}, | ||
| exceptions::nul_char_error, | ||
| function::{ArgBytesLike, ArgMemoryBuffer, OptionalArg}, | ||
| function::{ArgBytesLike, ArgMemoryBuffer, FsPath, OptionalArg}, | ||
| types::{Constructor, DefaultConstructor, Destructor, Initializer, Representable}, | ||
| }; | ||
| use crossbeam_utils::atomic::AtomicCell; | ||
|
|
@@ -6668,14 +6681,25 @@ mod winconsoleio { | |
| /// Returns 'r' (input), 'w' (output), 'x' (generic CON), or '\0' (not a console). | ||
| pub(super) fn pyio_get_console_type(path_or_fd: &PyObject, vm: &VirtualMachine) -> char { | ||
| // Try as integer fd first | ||
| if let Ok(fd) = i32::try_from_object(vm, path_or_fd.to_owned()) { | ||
| if let Ok(fd) = i32::try_from_object(vm, path_or_fd.to_owned()) | ||
| && fd >= 0 | ||
| { | ||
| return host_nt::console_type_from_fd(fd); | ||
| } | ||
|
|
||
| // Try as string path | ||
| let Ok(name) = path_or_fd.str(vm) else { | ||
| let Ok(path) = FsPath::try_from_path_like(path_or_fd.to_owned(), true, vm) else { | ||
| return '\0'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| }; | ||
| let name = match &path { | ||
| FsPath::Str(name) => name.to_owned(), | ||
| FsPath::Bytes(_) => { | ||
| let Ok(path) = path.as_os_str(vm) else { | ||
| return '\0'; | ||
| }; | ||
| vm.fsdecode(path.into_owned()) | ||
| } | ||
| }; | ||
| let Some(name_str) = name.to_str() else { | ||
| // Surrogate strings can't be console device names | ||
| return '\0'; | ||
|
|
@@ -6754,24 +6778,22 @@ mod winconsoleio { | |
| } | ||
| } | ||
|
|
||
| // Warn if bool is used as file descriptor | ||
| if nameobj.class().is(vm.ctx.types.bool_type) { | ||
| crate::stdlib::_warnings::warn( | ||
| vm.ctx.exceptions.runtime_warning, | ||
| "bool is used as a file descriptor".to_owned(), | ||
| 1, | ||
| vm, | ||
| )?; | ||
| } | ||
|
|
||
| // Try to get fd from integer | ||
| let mut fd: i32 = -1; | ||
| if let Some(i) = nameobj.downcast_ref::<crate::builtins::PyInt>() { | ||
| fd = i.try_to_primitive::<i32>(vm).unwrap_or(-1); | ||
| if fd < 0 { | ||
| return Err(vm.new_value_error("negative file descriptor")); | ||
| } | ||
| } | ||
| let mut fd = super::open_file_descriptor(&nameobj, vm)?.unwrap_or(-1); | ||
| let path = if fd < 0 { | ||
| let path = FsPath::try_from_path_like(nameobj.clone(), true, vm)?; | ||
| let decoded = match &path { | ||
| FsPath::Str(s) => s.to_owned(), | ||
| FsPath::Bytes(_) => vm.fsdecode(path.as_os_str(vm)?.into_owned()), | ||
| }; | ||
| let wide = decoded | ||
| .as_wtf8() | ||
| .to_wide_cstring() | ||
| .map_err(|e| e.to_pyexception(vm))?; | ||
| let console_type = pyio_get_console_type(decoded.as_object(), vm); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| Some((wide, console_type)) | ||
| } else { | ||
| None | ||
| }; | ||
|
|
||
| // Parse mode | ||
| let mut rwa = false; | ||
|
|
@@ -6812,8 +6834,9 @@ mod winconsoleio { | |
| zelf.writable.store(writable); | ||
|
|
||
| if fd < 0 { | ||
| // Get console type from name | ||
| console_type = pyio_get_console_type(&nameobj, vm); | ||
| // The decoded name was resolved before mode validation. | ||
| let (wide, detected_type) = path.expect("a filename was resolved above"); | ||
| console_type = detected_type; | ||
| if console_type == 'x' { | ||
| if writable { | ||
| console_type = 'w'; | ||
|
|
@@ -6828,12 +6851,6 @@ mod winconsoleio { | |
| return Err(vm.new_value_error("Cannot use closefd=False with file name")); | ||
| } | ||
|
|
||
| let name_str = nameobj.str(vm)?; | ||
| let wide = name_str | ||
| .as_wtf8() | ||
| .to_wide_cstring() | ||
| .map_err(|e| e.to_pyexception(vm))?; | ||
|
|
||
| fd = host_nt::open_console_path_fd(&wide, writable) | ||
| .map_err(|err| err.to_pyexception(vm))?; | ||
| } else { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On Unix builds with
host_env,_io::io_openalso invokes this converter for its descriptor-validity check (crates/vm/src/stdlib/_io.rs:5822–5826). Astrorbytessubclass whose__index__returns a closed, nonnegative descriptor previously entered that check and raisedEBADF. This earlyPathreturn now bypasses validation, andFileIO::initonly recognizes actualPyIntobjects as descriptors, soopen(value, "w")proceeds to create or truncate the string's filename instead of raising. Preserve descriptor-first handling for the I/O caller separately from the new OS-path dispatch.Useful? React with 👍 / 👎.