Skip to main content

rustpython_vm/function/
fspath.rs

1use crate::{
2    PyObjectRef, PyResult, TryFromObject, VirtualMachine,
3    builtins::{PyBytes, PyBytesRef, PyStrRef},
4    convert::{IntoPyException, ToPyObject},
5    function::PyStr,
6};
7use alloc::borrow::Cow;
8use core::hint::cold_path;
9use std::{ffi::OsStr, path::PathBuf};
10
11/// Helper to implement os.fspath()
12#[derive(Clone)]
13pub enum FsPath {
14    Str(PyStrRef),
15    Bytes(PyBytesRef),
16}
17
18impl FsPath {
19    pub fn try_from_path_like(
20        obj: PyObjectRef,
21        check_for_nul: bool,
22        vm: &VirtualMachine,
23    ) -> PyResult<Self> {
24        Self::try_from(
25            obj,
26            check_for_nul,
27            "expected str, bytes or os.PathLike object",
28            vm,
29        )
30    }
31
32    // PyOS_FSPath
33    pub fn try_from(
34        obj: PyObjectRef,
35        check_for_nul: bool,
36        msg: &'static str,
37        vm: &VirtualMachine,
38    ) -> PyResult<Self> {
39        let match1 = |obj: PyObjectRef| {
40            let pathlike = match_class!(match obj {
41                s @ PyStr => {
42                    if check_for_nul && s.contains_nuls() {
43                        cold_path();
44                        return Err(crate::exceptions::nul_char_error(vm));
45                    }
46                    Self::Str(s)
47                }
48                b @ PyBytes => {
49                    if check_for_nul && b.payload.contains_nuls() {
50                        cold_path();
51                        return Err(crate::exceptions::nul_char_error(vm));
52                    }
53                    Self::Bytes(b)
54                }
55                obj => return Ok(Err(obj)),
56            });
57            Ok(Ok(pathlike))
58        };
59        let obj = match match1(obj)? {
60            Ok(pathlike) => return Ok(pathlike),
61            Err(obj) => obj,
62        };
63        let not_pathlike_error = || format!("{msg}, not {}", obj.class().name());
64        let method = vm.get_method_or_type_error(
65            obj.clone(),
66            identifier!(vm, __fspath__),
67            not_pathlike_error,
68        )?;
69        // If __fspath__ is explicitly set to None, treat it as if it doesn't have __fspath__
70        if vm.is_none(&method) {
71            return Err(vm.new_type_error(not_pathlike_error()));
72        }
73        let result = method.call((), vm)?;
74        match1(result)?.map_err(|result| {
75            vm.new_type_error(format!(
76                "expected {}.__fspath__() to return str or bytes, not {}",
77                obj.class().name(),
78                result.class().name(),
79            ))
80        })
81    }
82
83    pub fn as_os_str(&self, vm: &VirtualMachine) -> PyResult<Cow<'_, OsStr>> {
84        // TODO: FS encodings
85        match self {
86            Self::Str(s) => vm.fsencode(s),
87            Self::Bytes(b) => Self::bytes_as_os_str(b.as_bytes(), vm).map(Cow::Borrowed),
88        }
89    }
90
91    #[must_use]
92    pub fn as_bytes(&self) -> &[u8] {
93        // TODO: FS encodings
94        match self {
95            Self::Str(s) => s.as_bytes(),
96            Self::Bytes(b) => b.as_bytes(),
97        }
98    }
99
100    #[must_use]
101    pub fn to_string_lossy(&self) -> Cow<'_, str> {
102        match self {
103            Self::Str(s) => s.to_string_lossy(),
104            Self::Bytes(s) => String::from_utf8_lossy(s.as_bytes()),
105        }
106    }
107
108    pub fn to_path_buf(&self, vm: &VirtualMachine) -> PyResult<PathBuf> {
109        let path = match self {
110            Self::Str(s) => PathBuf::from(vm.fsencode(s)?.as_ref() as &OsStr),
111            Self::Bytes(b) => PathBuf::from(Self::bytes_as_os_str(b.as_bytes(), vm)?),
112        };
113        Ok(path)
114    }
115
116    pub fn to_cstring(&self, vm: &VirtualMachine) -> PyResult<alloc::ffi::CString> {
117        alloc::ffi::CString::new(self.as_bytes()).map_err(|e| e.into_pyexception(vm))
118    }
119
120    #[cfg(windows)]
121    pub fn to_wide_cstring(&self, vm: &VirtualMachine) -> PyResult<widestring::WideCString> {
122        widestring::WideCString::from_os_str(self.as_os_str(vm)?)
123            .map_err(|err| err.into_pyexception(vm))
124    }
125
126    pub fn bytes_as_os_str<'a>(b: &'a [u8], vm: &VirtualMachine) -> PyResult<&'a std::ffi::OsStr> {
127        rustpython_host_env::os::bytes_as_os_str(b).map_err(|e| {
128            vm.new_unicode_decode_error(
129                vm.ctx.new_str("utf-8"),
130                vm.ctx.new_bytes(b.to_vec()),
131                e.valid_up_to(),
132                e.error_len().map_or(b.len(), |n| e.valid_up_to() + n),
133                vm.ctx.new_str("can't decode path for utf-8"),
134            )
135        })
136    }
137}
138
139impl ToPyObject for FsPath {
140    fn to_pyobject(self, _vm: &VirtualMachine) -> PyObjectRef {
141        match self {
142            Self::Str(s) => s.into(),
143            Self::Bytes(b) => b.into(),
144        }
145    }
146}
147
148impl TryFromObject for FsPath {
149    // PyUnicode_FSDecoder, which takes what PyOS_FSPath takes: str, bytes, or an
150    // object with __fspath__, and nothing that merely exports a buffer.
151    fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
152        Self::try_from_path_like(obj, true, vm)
153    }
154}