Skip to main content

rustpython_vm/
ospath.rs

1use crate::{
2    PyObjectRef, PyResult, VirtualMachine,
3    builtins::{PyBytes, PyStr},
4    convert::{ToPyException, TryFromObject},
5    function::FsPath,
6};
7use core::hint::cold_path;
8use std::path::{Path, PathBuf};
9
10#[cfg(feature = "host_env")]
11pub(crate) use crate::ospath_fd::OsPathOrFd;
12
13/// path_converter
14#[derive(Clone, Copy, Default)]
15pub struct PathConverter {
16    /// Function name for error messages (e.g., "rename")
17    pub function_name: Option<&'static str>,
18    /// Argument name for error messages (e.g., "src", "dst")
19    pub argument_name: Option<&'static str>,
20    /// If true, embedded null characters are allowed
21    pub non_strict: bool,
22}
23
24impl PathConverter {
25    #[must_use]
26    pub const fn new() -> Self {
27        Self {
28            function_name: None,
29            argument_name: None,
30            non_strict: false,
31        }
32    }
33
34    #[must_use]
35    pub const fn function(mut self, name: &'static str) -> Self {
36        self.function_name = Some(name);
37        self
38    }
39
40    #[must_use]
41    pub const fn argument(mut self, name: &'static str) -> Self {
42        self.argument_name = Some(name);
43        self
44    }
45
46    #[must_use]
47    pub const fn non_strict(mut self) -> Self {
48        self.non_strict = true;
49        self
50    }
51
52    /// Generate error message prefix like "rename: "
53    fn error_prefix(&self) -> String {
54        match self.function_name {
55            Some(func) => format!("{func}: "),
56            None => String::new(),
57        }
58    }
59
60    /// Get argument name for error messages, defaults to "path"
61    fn arg_name(&self) -> &'static str {
62        self.argument_name.unwrap_or("path")
63    }
64
65    /// Format a type error message
66    fn type_error_msg(&self, type_name: &str, allow_fd: bool) -> String {
67        let expected = if allow_fd {
68            "string, bytes, os.PathLike or integer"
69        } else {
70            "string, bytes or os.PathLike"
71        };
72        format!(
73            "{}{} should be {}, not {}",
74            self.error_prefix(),
75            self.arg_name(),
76            expected,
77            type_name
78        )
79    }
80
81    /// Convert to OsPath only (no fd support)
82    pub(crate) fn try_path_inner(
83        &self,
84        obj: PyObjectRef,
85        allow_fd: bool,
86        vm: &VirtualMachine,
87    ) -> PyResult<OsPath> {
88        // Try direct str/bytes match
89        let obj = match self.try_match_str_bytes(obj, vm)? {
90            Ok(path) => return Ok(path),
91            Err(obj) => obj,
92        };
93
94        // Call __fspath__
95        let type_error_msg = || self.type_error_msg(&obj.class().name(), allow_fd);
96        let method =
97            vm.get_method_or_type_error(obj.clone(), identifier!(vm, __fspath__), type_error_msg)?;
98        if vm.is_none(&method) {
99            return Err(vm.new_type_error(type_error_msg()));
100        }
101        let result = method.call((), vm)?;
102
103        // Match __fspath__ result
104        self.try_match_str_bytes(result.clone(), vm)?.map_err(|_| {
105            vm.new_type_error(format!(
106                "{}expected {}.__fspath__() to return str or bytes, not {}",
107                self.error_prefix(),
108                obj.class().name(),
109                result.class().name(),
110            ))
111        })
112    }
113
114    /// Try to match str or bytes, returns Err(obj) if neither
115    fn try_match_str_bytes(
116        &self,
117        obj: PyObjectRef,
118        vm: &VirtualMachine,
119    ) -> PyResult<Result<OsPath, PyObjectRef>> {
120        let check_nul = |b: &[u8]| {
121            if self.non_strict || memchr::memchr(b'\0', b).is_none() {
122                Ok(())
123            } else {
124                cold_path();
125                Err(vm.new_value_error(format!(
126                    "{}embedded null character in {}",
127                    self.error_prefix(),
128                    self.arg_name()
129                )))
130            }
131        };
132
133        match_class!(match obj {
134            s @ PyStr => {
135                check_nul(s.as_bytes())?;
136                let path = vm.fsencode(&s)?.into_owned();
137                Ok(Ok(OsPath {
138                    path,
139                    origin: Some(s.into()),
140                }))
141            }
142            b @ PyBytes => {
143                check_nul(b.as_bytes())?;
144                let path = FsPath::bytes_as_os_str(b.as_bytes(), vm)?.to_owned();
145                Ok(Ok(OsPath {
146                    path,
147                    origin: Some(b.into()),
148                }))
149            }
150            obj => Ok(Err(obj)),
151        })
152    }
153
154    /// Convert to OsPath directly
155    pub fn try_path(&self, obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<OsPath> {
156        self.try_path_inner(obj, false, vm)
157    }
158}
159
160/// path_t output - the converted path
161#[derive(Clone)]
162pub struct OsPath {
163    pub path: std::ffi::OsString,
164    /// Original Python object for identity preservation in OSError
165    pub(super) origin: Option<PyObjectRef>,
166}
167
168#[derive(Debug, Copy, Clone)]
169pub enum OutputMode {
170    String,
171    Bytes,
172}
173
174impl OutputMode {
175    pub(super) fn process_path(self, path: impl Into<PathBuf>, vm: &VirtualMachine) -> PyObjectRef {
176        fn inner(mode: OutputMode, path: PathBuf, vm: &VirtualMachine) -> PyObjectRef {
177            match mode {
178                OutputMode::String => vm.fsdecode(path).into(),
179                OutputMode::Bytes => vm
180                    .ctx
181                    .new_bytes(path.into_os_string().into_encoded_bytes())
182                    .into(),
183            }
184        }
185        inner(self, path.into(), vm)
186    }
187}
188
189impl OsPath {
190    pub fn new_str(path: impl Into<std::ffi::OsString>) -> Self {
191        let path = path.into();
192        Self { path, origin: None }
193    }
194
195    #[must_use]
196    pub fn as_path(&self) -> &Path {
197        Path::new(&self.path)
198    }
199
200    #[must_use]
201    pub fn into_bytes(self) -> Vec<u8> {
202        self.path.into_encoded_bytes()
203    }
204
205    #[must_use]
206    pub fn to_string_lossy(&self) -> alloc::borrow::Cow<'_, str> {
207        self.path.to_string_lossy()
208    }
209
210    pub fn into_cstring(self, vm: &VirtualMachine) -> PyResult<alloc::ffi::CString> {
211        alloc::ffi::CString::new(self.into_bytes()).map_err(|err| err.to_pyexception(vm))
212    }
213
214    #[cfg(windows)]
215    pub fn to_wide_cstring(&self, vm: &VirtualMachine) -> PyResult<widestring::WideCString> {
216        widestring::WideCString::from_os_str(&self.path).map_err(|err| err.to_pyexception(vm))
217    }
218
219    pub fn filename(&self, vm: &VirtualMachine) -> PyObjectRef {
220        if let Some(ref origin) = self.origin {
221            origin.clone()
222        } else {
223            // Default to string when no origin (e.g., from new_str)
224            OutputMode::String.process_path(self.path.clone(), vm)
225        }
226    }
227
228    /// Get the output mode based on origin type (bytes -> Bytes, otherwise -> String)
229    #[must_use]
230    pub fn mode(&self) -> OutputMode {
231        match &self.origin {
232            Some(obj) if obj.downcast_ref::<PyBytes>().is_some() => OutputMode::Bytes,
233            _ => OutputMode::String,
234        }
235    }
236}
237
238impl AsRef<Path> for OsPath {
239    fn as_ref(&self) -> &Path {
240        self.as_path()
241    }
242}
243
244impl TryFromObject for OsPath {
245    fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
246        PathConverter::new().try_path(obj, vm)
247    }
248}