@@ -2,22 +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 ,
15174 /// Original Python object for identity preservation in OSError
16175 pub ( super ) origin : Option < PyObjectRef > ,
17176}
18177
19178#[ derive( Debug , Copy , Clone ) ]
20- pub ( super ) enum OutputMode {
179+ pub enum OutputMode {
21180 String ,
22181 Bytes ,
23182}
@@ -40,22 +199,17 @@ impl OutputMode {
40199impl OsPath {
41200 pub fn new_str ( path : impl Into < std:: ffi:: OsString > ) -> Self {
42201 let path = path. into ( ) ;
43- Self {
44- path,
45- mode : OutputMode :: String ,
46- origin : None ,
47- }
202+ Self { path, origin : None }
48203 }
49204
50205 pub ( crate ) fn from_fspath ( fspath : FsPath , vm : & VirtualMachine ) -> PyResult < Self > {
51206 let path = fspath. as_os_str ( vm) ?. into_owned ( ) ;
52- let ( mode , origin) = match fspath {
53- FsPath :: Str ( s) => ( OutputMode :: String , s. into ( ) ) ,
54- FsPath :: Bytes ( b) => ( OutputMode :: Bytes , b. into ( ) ) ,
207+ let origin = match fspath {
208+ FsPath :: Str ( s) => s. into ( ) ,
209+ FsPath :: Bytes ( b) => b. into ( ) ,
55210 } ;
56211 Ok ( Self {
57212 path,
58- mode,
59213 origin : Some ( origin)
3A80
,
60214 } )
61215 }
@@ -93,7 +247,16 @@ impl OsPath {
93247 if let Some ( ref origin) = self . origin {
94248 origin. clone ( )
95249 } else {
96- self . mode . process_path ( self . path . clone ( ) , vm)
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 ,
97260 }
98261 }
99262}
@@ -105,10 +268,8 @@ impl AsRef<Path> for OsPath {
105268}
106269
107270impl TryFromObject for OsPath {
108- // path_converter with allow_fd=0
109271 fn try_from_object ( vm : & VirtualMachine , obj : PyObjectRef ) -> PyResult < Self > {
110- let fspath = FsPath :: try_from ( obj, true , "should be string, bytes or os.PathLike" , vm) ?;
111- Self :: from_fspath ( fspath, vm)
272+ PathConverter :: new ( ) . try_path ( obj, vm)
112273 }
113274}
114275
@@ -121,15 +282,7 @@ pub(crate) enum OsPathOrFd<'fd> {
121282
122283impl TryFromObject for OsPathOrFd < ' _ > {
123284 fn try_from_object ( vm : & VirtualMachine , obj : PyObjectRef ) -> PyResult < Self > {
124- match obj. try_index_opt ( vm) {
125- Some ( int) => {
126- let fd = int?. try_to_primitive ( vm) ?;
127- unsafe { crt_fd:: Borrowed :: try_borrow_raw ( fd) }
128- . map ( Self :: Fd )
129- . map_err ( |e| e. into_pyexception ( vm) )
130- }
131- None => obj. try_into_value ( vm) . map ( Self :: Path ) ,
132- }
285+ PathConverter :: new ( ) . try_path_or_fd ( obj, vm)
133286 }
134287}
135288
0 commit comments