@@ -26,6 +26,32 @@ use crate::{
2626pub use _io:: { OpenArgs , io_open as open} ;
2727use 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+
2955fn 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