@@ -2,20 +2,181 @@ use rustpython_common::crt_fd;
22
33use crate :: {
44 PyObjectRef , PyResult , VirtualMachine ,
5+ builtins:: { PyBytes , PyStr } ,
56 convert:: { IntoPyException , ToPyException , ToPyObject , TryFromObject } ,
67 function:: FsPath ,
78} ;
89use std:: path:: { Path , PathBuf } ;
910
10- // path_ without allow_fd in CPython
11+ /// path_converter
12+ #[ derive( Clone , Copy , Default ) ]
13+ pub struct PathConverter {
14+ /// Function name for error messages (e.g., "rename")
15+ pub function_name : Option < & ' static str > ,
16+ /// Argument name for error messages (e.g., "src", "dst")
17+ pub argument_name : Option < & ' static str > ,
18+ /// If true, embedded null characters are allowed
19+ pub non_strict : bool ,
20+ }
21+
22+ impl PathConverter {
23+ pub const fn new ( ) -> Self {
24+ Self {
25+ function_name : None ,
26+ argument_name : None ,
27+ non_strict : false ,
28+ }
29+ }
30+
31+ pub const fn function ( mut self , name : & ' static str ) -> Self {
32+ self . function_name = Some ( name) ;
33+ self
34+ }
35+
36+ pub const fn argument ( mut self , name : & ' static str ) -> Self {
37+ self . argument_name = Some ( name) ;
38+ self
39+ }
40+
41+ pub const fn non_strict ( mut self ) -> Self {
42+ self . non_strict = true ;
43+ self
44+ }
45+
46+ /// Generate error message prefix like "rename: "
47+ fn error_prefix ( & self ) -> String {
48+ match self . function_name {
49+ Some ( func) => format ! ( "{}: " , func) ,
50+ None => String :: new ( ) ,
51+ }
52+ }
53+
54+ /// Get argument name for error messages, defaults to "path"
55+ fn arg_name ( & self ) -> & ' static str {
56+ self . argument_name . unwrap_or ( "path" )
57+ }
58+
59+ /// Format a type error message
60+ fn type_error_msg ( & self , type_name : & str , allow_fd : bool ) -> String {
61+ let expected = if allow_fd {
62+ "string, bytes, os.PathLike or integer"
63+ } else {
64+ "string, bytes or os.PathLike"
65+ } ;
66+ format ! (
67+ "{}{} should be {}, not {}" ,
68+ self . error_prefix( ) ,
69+ self . arg_name( ) ,
70+ expected,
71+ type_name
72+ )
73+ }
74+
75+ /// Convert to OsPathOrFd (path or file descriptor)
76+ pub ( crate ) fn try_path_or_fd < ' fd > (
77+ & self ,
78+ obj : PyObjectRef ,
79+ vm : & VirtualMachine ,
80+ ) -> PyResult < OsPathOrFd < ' fd > > {
81+ // Handle fd (before __fspath__ check, like CPython)
82+ if let Some ( int) = obj. try_index_opt ( vm) {
83+ let fd = int?. try_to_primitive ( vm) ?;
84+ return unsafe { crt_fd:: Borrowed :: try_borrow_raw ( fd) }
85+ . map ( OsPathOrFd :: Fd )
86+ . map_err ( |e| e. into_pyexception ( vm) ) ;
87+ }
88+
89+ self . try_path_inner ( obj, true , vm) . map ( OsPathOrFd :: Path )
90+ }
91+
92+ /// Convert to OsPath only (no fd support)
93+ fn try_path_inner (
94+ & self ,
95+ obj : PyObjectRef ,
96+ allow_fd : bool ,
97+ vm : & VirtualMachine ,
98+ ) -> PyResult < OsPath > {
99+ // Try direct str/bytes match
100+ let obj = match self . try_match_str_bytes ( obj. clone ( ) , vm) ? {
101+ Ok ( path) => return Ok ( path) ,
102+ Err ( obj) => obj,
103+ } ;
104+
105+ // Call __fspath__
106+ let type_error_msg = || self . type_error_msg ( & obj. class ( ) . name ( ) , allow_fd) ;
107+ let method =
108+ vm. get_method_or_type_error ( obj. clone ( ) , identifier ! ( vm, __fspath__) , type_error_msg) ?;
109+ if vm. is_none ( & method) {
110+ return Err ( vm. new_type_error ( type_error_msg ( ) ) ) ;
111+ }
112+ let result = method. call ( ( ) , vm) ?;
113+
114+ // Match __fspath__ result
115+ self . try_match_str_bytes ( result. clone ( ) , vm) ?. map_err ( |_| {
116+ vm. new_type_error ( format ! (
117+ "{}expected {}.__fspath__() to return str or bytes, not {}" ,
118+ self . error_prefix( ) ,
119+ obj. class( ) . name( ) ,
120+ result. class( ) . name( ) ,
121+ ) )
122+ } )
123+ }
124+
125+ /// Try to match str or bytes, returns Err(obj) if neither
126+ fn try_match_str_bytes (
127+ & self ,
128+ obj : PyObjectRef ,
129+ vm : & VirtualMachine ,
130+ ) -> PyResult < Result < OsPath , PyObjectRef > > {
131+ let check_nul = |b : & [ u8 ] | {
132+ if self . non_strict || memchr:: memchr ( b'\0' , b) . is_none ( ) {
133+ Ok ( ( ) )
134+ } else {
135+ Err ( vm. new_value_error ( format ! (
136+ "{}embedded null character in {}" ,
137+ self . error_prefix( ) ,
138+ self . arg_name( )
139+ ) ) )
140+ }
141+ } ;
142+
143+ match_class ! ( match obj {
144+ s @ PyStr => {
145+ check_nul( s. as_bytes( ) ) ?;
146+ let path = vm. fsencode( & s) ?. into_owned( ) ;
147+ Ok ( Ok ( OsPath {
148+ path,
149+ origin: Some ( s. into( ) ) ,
150+ } ) )
151+ }
152+ b @ PyBytes => {
153+ check_nul( & b) ?;
154+ let path = FsPath :: bytes_as_os_str( & b, vm) ?. to_owned( ) ;
155+ Ok ( Ok ( OsPath {
156+ path,
157+ origin: Some ( b. into( ) ) ,
158+ } ) )
159+ }
160+ obj => Ok ( Err ( obj) ) ,
161+ } )
162+ }
163+
164+ /// Convert to OsPath directly
165+ pub fn try_path ( & self , obj : PyObjectRef , vm : & VirtualMachine ) -> PyResult < OsPath > {
166+ self . try_path_inner ( obj, false , vm)
167+ }
168+ }
169+
170+ /// path_t output - the converted path
11171#[ derive( Clone ) ]
12172pub struct OsPath {
13173 pub path : std:: ffi:: OsString ,
14- pub ( super ) mode : OutputMode ,
174+ /// Original Python object for identity preservation in OSError
175+ pub ( super ) origin : Option < PyObjectRef > ,
15176}
16177
17178#[ derive( Debug , Copy , Clone ) ]
18- pub ( super ) enum OutputMode {
179+ pub enum OutputMode {
19180 String ,
20181 Bytes ,
21182}
@@ -38,19 +199,19 @@ impl OutputMode {
38199impl OsPath {
39200 pub fn new_str ( path : impl Into < std:: ffi:: OsString > ) -> Self {
40201 let path = path. into ( ) ;
41- Self {
42- path,
43- mode : OutputMode :: String ,
44- }
202+ Self { path, origin : None }
45203 }
46204
47205 pub ( crate ) fn from_fspath ( fspath : FsPath , vm : & VirtualMachine ) -> PyResult < Self > {
48206 let path = fspath. as_os_str ( vm) ?. into_owned ( ) ;
49- let mode = match fspath {
50- FsPath :: Str ( _ ) => OutputMode :: String ,
51- FsPath :: Bytes ( _ ) => OutputMode :: Bytes ,
207+ let origin = match fspath {
208+ FsPath :: Str ( s ) => s . into ( ) ,
209+ FsPath :: Bytes ( b ) => b . into ( ) ,
52210 } ;
53- Ok ( Self { path, mode } )
211+ Ok ( Self {
212+ path,
213+ origin : Some ( origin) ,
214+ } )
54215 }
55216
56217 /// Convert an object to OsPath using the os.fspath-style error message.
@@ -83,7 +244,20 @@ impl OsPath {
83244 }
84245
85246 pub fn filename ( & self , vm : & VirtualMachine ) -> PyObjectRef {
86- self . mode . process_path ( self . path . clone ( ) , vm)
247+ if let Some ( ref origin) = self . origin {
248+ origin. clone ( )
249+ } else {
250+ // Default to string when no origin (e.g., from new_str)
251+ OutputMode :: String . process_path ( self . path . clone ( ) , vm)
252+ }
253+ }
254+
255+ /// Get the output mode based on origin type (bytes -> Bytes, otherwise -> String)
256+ pub fn mode ( & self ) -> OutputMode {
257+ match & self . origin {
258+ Some ( obj) if obj. downcast_ref :: < PyBytes > ( ) . is_some ( ) => OutputMode :: Bytes ,
259+ _ => OutputMode :: String ,
260+ }
87261 }
88262}
89263
@@ -94,15 +268,8 @@ impl AsRef<Path> for OsPath {
94268}
95269
96270impl TryFromObject for OsPath {
97- // TODO: path_converter with allow_fd=0 in CPython
98271 fn try_from_object ( vm : & VirtualMachine , obj : PyObjectRef ) -> PyResult < Self > {
99- let fspath = FsPath :: try_from (
100- obj,
101- true ,
102- "should be string, bytes, os.PathLike or integer" ,
103- vm,
104- ) ?;
105- Self :: from_fspath ( fspath, vm)
272+ PathConverter :: new ( ) . try_path ( obj, vm)
106273 }
107274}
108275
@@ -115,15 +282,7 @@ pub(crate) enum OsPathOrFd<'fd> {
115282
116283impl TryFromObject for OsPathOrFd < ' _ > {
117284 fn try_from_object ( vm : & VirtualMachine , obj : PyObjectRef ) -> PyResult < Self > {
118- match obj. try_index_opt ( vm) {
119- Some ( int) => {
120- let fd = int?. try_to_primitive ( vm) ?;
121- unsafe { crt_fd:: Borrowed :: try_borrow_raw ( fd) }
122- . map ( Self :: Fd )
123- . map_err ( |e| e. into_pyexception ( vm) )
124- }
125- None => obj. try_into_value ( vm) . map ( Self :: Path ) ,
126- }
285+ PathConverter :: new ( ) . try_path_or_fd ( obj, vm)
127286 }
128287}
129288
0 commit comments