8000 impl PathConverter · RustPython/RustPython@36bbf14 · GitHub
[go: up one dir, main page]

Skip to content

Commit 36bbf14

Browse files
committed
impl PathConverter
1 parent 7bdcef1 commit 36bbf14

File tree

4 files changed

+200
-35
lines changed

4 files changed

+200
-35
lines changed

crates/vm/src/function/fspath.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::{
77
};
88
use std::{borrow::Cow, ffi::OsStr, path::PathBuf};
99

10+
/// Helper to implement os.fspath()
1011
#[derive(Clone)]
1112
pub enum FsPath {
1213
Str(PyStrRef),
@@ -27,7 +28,7 @@ impl FsPath {
2728
)
2829
}
2930

30-
// PyOS_FSPath in CPython
31+
// PyOS_FSPath
3132
pub fn try_from(
3233
obj: PyObjectRef,
3334
check_for_nul: bool,

crates/vm/src/ospath.rs

Lines changed: 178 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,181 @@ use rustpython_common::crt_fd;
22

33
use crate::{
44
PyObjectRef, PyResult, VirtualMachine,
5+
builtins::{PyBytes, PyStr},
56
convert::{IntoPyException, ToPyException, ToPyObject, TryFromObject},
67
function::FsPath,
78
};
89
use 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)]
12172
pub 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 {
40199
impl 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

107270
impl 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

122283
impl 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

crates/vm/src/stdlib/nt.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1053,7 +1053,7 @@ pub(crate) mod module {
10531053
use crate::builtins::{PyBytes, PyStr};
10541054
use rustpython_common::wtf8::Wtf8Buf;
10551055

1056-
// Handle path-like objects via os.fspath, but without null check (nonstrict=True)
1056+
// Handle path-like objects via os.fspath, but without null check (non_strict=True)
10571057
let path = if let Some(fspath) = vm.get_method(path.clone(), identifier!(vm, __fspath__)) {
10581058
fspath?.call((), vm)?
10591059
} else {

crates/vm/src/stdlib/os.rs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ pub(super) mod _os {
164164
convert::{IntoPyException, ToPyObject},
165165
exceptions::OSErrorBuilder,
166166
function::{ArgBytesLike, FsPath, FuncArgs, OptionalArg},
167-
ospath::{OsPath, OsPathOrFd, OutputMode},
167+
ospath::{OsPath, OsPathOrFd, OutputMode, PathConverter},
168168
protocol::PyIterReturn,
169169
recursion::ReprGuard,
170170
types::{IterNext, Iterable, PyStructSequence, Representable, SelfIter},
@@ -366,10 +366,12 @@ pub(super) mod _os {
366366

367367
#[pyfunction]
368368
fn listdir(
369-
path: OptionalArg<OsPathOrFd<'_>>,
369+
path: OptionalArg<Option<OsPathOrFd<'_>>>,
370370
vm: &VirtualMachine,
371371
) -> PyResult<Vec<PyObjectRef>> {
372-
let path = path.unwrap_or_else(|| OsPathOrFd::Path(OsPath::new_str(".")));
372+
let path = path
373+
.flatten()
374+
.unwrap_or_else(|| OsPathOrFd::Path(OsPath::new_str(".")));
373375
let list = match path {
374376
OsPathOrFd::Path(path) => {
375377
let dir_iter = match fs::read_dir(&path) {
@@ -378,9 +380,10 @@ pub(super) mod _os {
378380
return Err(OSErrorBuilder::with_filename(&err, path, vm));
379381
}
380382
};
383+
let mode = path.mode();
381384
dir_iter
382385
.map(|entry| match entry {
383-
Ok(entry_path) => Ok(path.mode.process_path(entry_path.file_name(), vm)),
386+
Ok(entry_path) => Ok(mode.process_path(entry_path.file_name(), vm)),
384387
Err(err) => Err(OSErrorBuilder::with_filename(&err, path.clone(), vm)),
385388
})
386389
.collect::<PyResult<_>>()?
@@ -545,7 +548,7 @@ pub(super) mod _os {
545548

546549
#[pyfunction]
547550
fn readlink(path: OsPath, dir_fd: DirFd<'_, 0>, vm: &VirtualMachine) -> PyResult {
548-
let mode = path.mode;
551+
let mode = path.mode();
549552
let [] = dir_fd.0;
550553
let path =
551554
fs::read_link(&path).map_err(|err| OSErrorBuilder::with_filename(&err, path, vm))?;
@@ -640,7 +643,6 @@ pub(super) mod _os {
640643
stat(
641644
OsPath {
642645
path: self.pathval.as_os_str().to_owned(),
643-
mode: OutputMode::String,
644646
origin: None,
645647
}
646648
.into(),
@@ -872,7 +874,7 @@ pub(super) mod _os {
872874
.map_err(|err| OSErrorBuilder::with_filename(&err, path.clone(), vm))?;
873875
Ok(ScandirIterator {
874876
entries: PyRwLock::new(Some(entries)),
875-
mode: path.mode,
877+
mode: path.mode(),
876878
}
877879
.into_ref(&vm.ctx)
878880
.into())
@@ -1135,7 +1137,16 @@ pub(super) mod _os {
11351137

11361138
#[pyfunction]
11371139
#[pyfunction(name = "replace")]
1138-
fn rename(src: OsPath, dst: OsPath, vm: &VirtualMachine) -> PyResult<()> {
1140+
fn rename(src: PyObjectRef, dst: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
1141+
let src = PathConverter::new()
1142+
.function("rename")
1143+
.argument("src")
1144+
.try_path(src, vm)?;
1145+
let dst = PathConverter::new()
1146+
.function("rename")
1147+
.argument("dst")
1148+
.try_path(dst, vm)?;
1149+
11391150
fs::rename(&src.path, &dst.path).map_err(|err| {
11401151
let builder = err.to_os_error_builder(vm);
11411152
let builder = builder.filename(src.filename(vm));

0 commit comments

Comments
 (0)
0