strict_path/path/
virtual_path.rs1use crate::error::StrictPathError;
3use crate::path::strict_path::StrictPath;
4use crate::validator::path_history::{Canonicalized, PathHistory};
5use crate::PathBoundary;
6use crate::Result;
7use std::ffi::OsStr;
8use std::fmt;
9use std::hash::{Hash, Hasher};
10use std::path::{Path, PathBuf};
11
12#[derive(Clone)]
18pub struct VirtualPath<Marker = ()> {
19 inner: StrictPath<Marker>,
20 virtual_path: PathBuf,
21}
22
23#[inline]
24fn clamp<Marker, H>(
25 restriction: &PathBoundary<Marker>,
26 anchored: PathHistory<(H, Canonicalized)>,
27) -> crate::Result<crate::path::strict_path::StrictPath<Marker>> {
28 restriction.strict_join(anchored.into_inner())
29}
30
31impl<Marker> VirtualPath<Marker> {
32 #[inline]
33 pub(crate) fn new(restricted_path: StrictPath<Marker>) -> Self {
34 fn compute_virtual<Marker>(
35 system_path: &std::path::Path,
36 restriction: &crate::PathBoundary<Marker>,
37 ) -> std::path::PathBuf {
38 use std::ffi::OsString;
39 use std::path::Component;
40
41 #[cfg(windows)]
42 fn strip_verbatim(p: &std::path::Path) -> std::path::PathBuf {
43 let s = p.as_os_str().to_string_lossy();
44 if let Some(trimmed) = s.strip_prefix("\\\\?\\") {
45 return std::path::PathBuf::from(trimmed);
46 }
47 if let Some(trimmed) = s.strip_prefix("\\\\.\\") {
48 return std::path::PathBuf::from(trimmed);
49 }
50 std::path::PathBuf::from(s.to_string())
51 }
52
53 #[cfg(not(windows))]
54 fn strip_verbatim(p: &std::path::Path) -> std::path::PathBuf {
55 p.to_path_buf()
56 }
57
58 let system_norm = strip_verbatim(system_path);
59 let jail_norm = strip_verbatim(restriction.path());
60
61 if let Ok(stripped) = system_norm.strip_prefix(&jail_norm) {
62 let mut cleaned = std::path::PathBuf::new();
63 for comp in stripped.components() {
64 if let Component::Normal(name) = comp {
65 let s = name.to_string_lossy();
66 let cleaned_s = s.replace(['\n', ';'], "_");
67 if cleaned_s == s {
68 cleaned.push(name);
69 } else {
70 cleaned.push(OsString::from(cleaned_s));
71 }
72 }
73 }
74 return cleaned;
75 }
76
77 let mut strictpath_comps: Vec<_> = system_norm
78 .components()
79 .filter(|c| !matches!(c, Component::Prefix(_) | Component::RootDir))
80 .collect();
81 let mut boundary_comps: Vec<_> = jail_norm
82 .components()
83 .filter(|c| !matches!(c, Component::Prefix(_) | Component::RootDir))
84 .collect();
85
86 #[cfg(windows)]
87 fn comp_eq(a: &Component, b: &Component) -> bool {
88 match (a, b) {
89 (Component::Normal(x), Component::Normal(y)) => {
90 x.to_string_lossy().to_ascii_lowercase()
91 == y.to_string_lossy().to_ascii_lowercase()
92 }
93 _ => false,
94 }
95 }
96
97 #[cfg(not(windows))]
98 fn comp_eq(a: &Component, b: &Component) -> bool {
99 a == b
100 }
101
102 while !strictpath_comps.is_empty()
103 && !boundary_comps.is_empty()
104 && comp_eq(&strictpath_comps[0], &boundary_comps[0])
105 {
106 strictpath_comps.remove(0);
107 boundary_comps.remove(0);
108 }
109
110 let mut vb = std::path::PathBuf::new();
111 for c in strictpath_comps {
112 if let Component::Normal(name) = c {
113 let s = name.to_string_lossy();
114 let cleaned = s.replace(['\n', ';'], "_");
115 if cleaned == s {
116 vb.push(name);
117 } else {
118 vb.push(OsString::from(cleaned));
119 }
120 }
121 }
122 vb
123 }
124
125 let virtual_path = compute_virtual(restricted_path.path(), restricted_path.restriction());
126
127 Self {
128 inner: restricted_path,
129 virtual_path,
130 }
131 }
132
133 #[inline]
135 pub fn unvirtual(self) -> StrictPath<Marker> {
136 self.inner
137 }
138
139 #[inline]
143 pub fn as_unvirtual(&self) -> &StrictPath<Marker> {
144 &self.inner
145 }
146
147 #[inline]
149 pub fn interop_path(&self) -> &OsStr {
150 self.inner.interop_path()
151 }
152
153 #[inline]
155 pub fn virtual_join<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
156 let candidate = self.virtual_path.join(path.as_ref());
158 let anchored = crate::validator::path_history::PathHistory::new(candidate)
159 .canonicalize_anchored(self.inner.restriction())?;
160 let restricted_path = clamp(self.inner.restriction(), anchored)?;
161 Ok(VirtualPath::new(restricted_path))
162 }
163
164 pub fn virtualpath_parent(&self) -> Result<Option<Self>> {
169 match self.virtual_path.parent() {
170 Some(parent_virtual_path) => {
171 let anchored = crate::validator::path_history::PathHistory::new(
172 parent_virtual_path.to_path_buf(),
173 )
174 .canonicalize_anchored(self.inner.restriction())?;
175 let restricted_path = clamp(self.inner.restriction(), anchored)?;
176 Ok(Some(VirtualPath::new(restricted_path)))
177 }
178 None => Ok(None),
179 }
180 }
181
182 #[inline]
184 pub fn virtualpath_with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Result<Self> {
185 let candidate = self.virtual_path.with_file_name(file_name);
186 let anchored = crate::validator::path_history::PathHistory::new(candidate)
187 .canonicalize_anchored(self.inner.restriction())?;
188 let restricted_path = clamp(self.inner.restriction(), anchored)?;
189 Ok(VirtualPath::new(restricted_path))
190 }
191
192 pub fn virtualpath_with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Result<Self> {
194 if self.virtual_path.file_name().is_none() {
195 return Err(StrictPathError::path_escapes_boundary(
196 self.virtual_path.clone(),
197 self.inner.restriction().path().to_path_buf(),
198 ));
199 }
200
201 let candidate = self.virtual_path.with_extension(extension);
202 let anchored = crate::validator::path_history::PathHistory::new(candidate)
203 .canonicalize_anchored(self.inner.restriction())?;
204 let restricted_path = clamp(self.inner.restriction(), anchored)?;
205 Ok(VirtualPath::new(restricted_path))
206 }
207
208 #[inline]
210 pub fn virtualpath_file_name(&self) -> Option<&OsStr> {
211 self.virtual_path.file_name()
212 }
213
214 #[inline]
216 pub fn virtualpath_file_stem(&self) -> Option<&OsStr> {
217 self.virtual_path.file_stem()
218 }
219
220 #[inline]
222 pub fn virtualpath_extension(&self) -> Option<&OsStr> {
223 self.virtual_path.extension()
224 }
225
226 #[inline]
228 pub fn virtualpath_starts_with<P: AsRef<Path>>(&self, p: P) -> bool {
229 self.virtual_path.starts_with(p)
230 }
231
232 #[inline]
234 pub fn virtualpath_ends_with<P: AsRef<Path>>(&self, p: P) -> bool {
235 self.virtual_path.ends_with(p)
236 }
237
238 #[inline]
240 pub fn virtualpath_display(&self) -> VirtualPathDisplay<'_, Marker> {
241 VirtualPathDisplay(self)
242 }
243
244 #[inline]
246 pub fn exists(&self) -> bool {
247 self.inner.exists()
248 }
249
250 #[inline]
252 pub fn is_file(&self) -> bool {
253 self.inner.is_file()
254 }
255
256 #[inline]
258 pub fn is_dir(&self) -> bool {
259 self.inner.is_dir()
260 }
261
262 #[inline]
264 pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
265 self.inner.metadata()
266 }
267
268 #[inline]
270 pub fn read_to_string(&self) -> std::io::Result<String> {
271 self.inner.read_to_string()
272 }
273
274 #[inline]
276 pub fn read_bytes(&self) -> std::io::Result<Vec<u8>> {
277 self.inner.read_bytes()
278 }
279
280 #[inline]
282 pub fn write_bytes(&self, data: &[u8]) -> std::io::Result<()> {
283 self.inner.write_bytes(data)
284 }
285
286 #[inline]
288 pub fn write_string(&self, data: &str) -> std::io::Result<()> {
289 self.inner.write_string(data)
290 }
291
292 #[inline]
294 pub fn create_dir_all(&self) -> std::io::Result<()> {
295 self.inner.create_dir_all()
296 }
297
298 #[inline]
302 pub fn create_dir(&self) -> std::io::Result<()> {
303 self.inner.create_dir()
304 }
305
306 #[inline]
311 pub fn create_parent_dir(&self) -> std::io::Result<()> {
312 match self.virtualpath_parent() {
313 Ok(Some(parent)) => parent.create_dir(),
314 Ok(None) => Ok(()),
315 Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
316 Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
317 }
318 }
319
320 #[inline]
324 pub fn create_parent_dir_all(&self) -> std::io::Result<()> {
325 match self.virtualpath_parent() {
326 Ok(Some(parent)) => parent.create_dir_all(),
327 Ok(None) => Ok(()),
328 Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
329 Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
330 }
331 }
332
333 #[inline]
335 pub fn remove_file(&self) -> std::io::Result<()> {
336 self.inner.remove_file()
337 }
338
339 #[inline]
341 pub fn remove_dir(&self) -> std::io::Result<()> {
342 self.inner.remove_dir()
343 }
344
345 #[inline]
347 pub fn remove_dir_all(&self) -> std::io::Result<()> {
348 self.inner.remove_dir_all()
349 }
350}
351
352pub struct VirtualPathDisplay<'a, Marker>(&'a VirtualPath<Marker>);
353
354impl<'a, Marker> fmt::Display for VirtualPathDisplay<'a, Marker> {
355 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
356 let s_lossy = self.0.virtual_path.to_string_lossy();
358 let s_norm: std::borrow::Cow<'_, str> = {
359 #[cfg(windows)]
360 {
361 std::borrow::Cow::Owned(s_lossy.replace('\\', "/"))
362 }
363 #[cfg(not(windows))]
364 {
365 std::borrow::Cow::Borrowed(&s_lossy)
366 }
367 };
368 if s_norm.starts_with('/') {
369 write!(f, "{s_norm}")
370 } else {
371 write!(f, "/{s_norm}")
372 }
373 }
374}
375
376impl<Marker> fmt::Debug for VirtualPath<Marker> {
377 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378 f.debug_struct("VirtualPath")
379 .field("system_path", &self.inner.path())
380 .field("virtual", &format!("{}", self.virtualpath_display()))
381 .field("restriction", &self.inner.restriction().path())
382 .field("marker", &std::any::type_name::<Marker>())
383 .finish()
384 }
385}
386
387impl<Marker> PartialEq for VirtualPath<Marker> {
388 #[inline]
389 fn eq(&self, other: &Self) -> bool {
390 self.inner.path() == other.inner.path()
391 }
392}
393
394impl<Marker> Eq for VirtualPath<Marker> {}
395
396impl<Marker> Hash for VirtualPath<Marker> {
397 #[inline]
398 fn hash<H: Hasher>(&self, state: &mut H) {
399 self.inner.path().hash(state);
400 }
401}
402
403impl<Marker> PartialEq<crate::path::strict_path::StrictPath<Marker>> for VirtualPath<Marker> {
404 #[inline]
405 fn eq(&self, other: &crate::path::strict_path::StrictPath<Marker>) -> bool {
406 self.inner.path() == other.path()
407 }
408}
409
410impl<T: AsRef<Path>, Marker> PartialEq<T> for VirtualPath<Marker> {
411 #[inline]
412 fn eq(&self, other: &T) -> bool {
413 let virtual_str = format!("{}", self.virtualpath_display());
416 let other_str = other.as_ref().to_string_lossy();
417
418 let normalized_virtual = virtual_str.as_str();
420
421 #[cfg(windows)]
422 let other_normalized = other_str.replace('\\', "/");
423 #[cfg(not(windows))]
424 let other_normalized = other_str.to_string();
425
426 let normalized_other = if other_normalized.starts_with('/') {
427 other_normalized
428 } else {
429 format!("/{}", other_normalized)
430 };
431
432 normalized_virtual == normalized_other
433 }
434}
435
436impl<T: AsRef<Path>, Marker> PartialOrd<T> for VirtualPath<Marker> {
437 #[inline]
438 fn partial_cmp(&self, other: &T) -> Option<std::cmp::Ordering> {
439 let virtual_str = format!("{}", self.virtualpath_display());
441 let other_str = other.as_ref().to_string_lossy();
442
443 let normalized_virtual = virtual_str.as_str();
445
446 #[cfg(windows)]
447 let other_normalized = other_str.replace('\\', "/");
448 #[cfg(not(windows))]
449 let other_normalized = other_str.to_string();
450
451 let normalized_other = if other_normalized.starts_with('/') {
452 other_normalized
453 } else {
454 format!("/{}", other_normalized)
455 };
456
457 Some(normalized_virtual.cmp(&normalized_other))
458 }
459}
460
461impl<Marker> PartialOrd for VirtualPath<Marker> {
462 #[inline]
463 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
464 Some(self.cmp(other))
465 }
466}
467
468impl<Marker> Ord for VirtualPath<Marker> {
469 #[inline]
470 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
471 self.inner.path().cmp(other.inner.path())
472 }
473}
474
475#[cfg(feature = "serde")]
476impl<Marker> serde::Serialize for VirtualPath<Marker> {
477 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
478 where
479 S: serde::Serializer,
480 {
481 serializer.serialize_str(&format!("{}", self.virtualpath_display()))
482 }
483}