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#[derive(Clone, Copy, Default)]
15pub struct PathConverter {
16 pub function_name: Option<&'static str>,
18 pub argument_name: Option<&'static str>,
20 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 fn error_prefix(&self) -> String {
54 match self.function_name {
55 Some(func) => format!("{func}: "),
56 None => String::new(),
57 }
58 }
59
60 fn arg_name(&self) -> &'static str {
62 self.argument_name.unwrap_or("path")
63 }
64
65 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 pub(crate) fn try_path_inner(
83 &self,
84 obj: PyObjectRef,
85 allow_fd: bool,
86 vm: &VirtualMachine,
87 ) -> PyResult<OsPath> {
88 let obj = match self.try_match_str_bytes(obj, vm)? {
90 Ok(path) => return Ok(path),
91 Err(obj) => obj,
92 };
93
94 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 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 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 pub fn try_path(&self, obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<OsPath> {
156 self.try_path_inner(obj, false, vm)
157 }
158}
159
160#[derive(Clone)]
162pub struct OsPath {
163 pub path: std::ffi::OsString,
164 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 OutputMode::String.process_path(self.path.clone(), vm)
225 }
226 }
227
228 #[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}