1use 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 pub fn with_root<P: AsRef<Path>>(root: P) -> Result<Self> {
36 let vroot = crate::validator::virtual_root::VirtualRoot::try_new(root)?;
37 vroot.virtual_join("")
38 }
39
40 pub fn with_root_create<P: AsRef<Path>>(root: P) -> Result<Self> {
44 let vroot = crate::validator::virtual_root::VirtualRoot::try_new_create(root)?;
45 vroot.virtual_join("")
46 }
47 #[inline]
48 pub(crate) fn new(restricted_path: StrictPath<Marker>) -> Self {
49 fn compute_virtual<Marker>(
50 system_path: &std::path::Path,
51 restriction: &crate::PathBoundary<Marker>,
52 ) -> std::path::PathBuf {
53 use std::ffi::OsString;
54 use std::path::Component;
55
56 #[cfg(windows)]
57 fn strip_verbatim(p: &std::path::Path) -> std::path::PathBuf {
58 let s = p.as_os_str().to_string_lossy();
59 if let Some(trimmed) = s.strip_prefix("\\\\?\\") {
60 return std::path::PathBuf::from(trimmed);
61 }
62 if let Some(trimmed) = s.strip_prefix("\\\\.\\") {
63 return std::path::PathBuf::from(trimmed);
64 }
65 std::path::PathBuf::from(s.to_string())
66 }
67
68 #[cfg(not(windows))]
69 fn strip_verbatim(p: &std::path::Path) -> std::path::PathBuf {
70 p.to_path_buf()
71 }
72
73 let system_norm = strip_verbatim(system_path);
74 let jail_norm = strip_verbatim(restriction.path());
75
76 if let Ok(stripped) = system_norm.strip_prefix(&jail_norm) {
77 let mut cleaned = std::path::PathBuf::new();
78 for comp in stripped.components() {
79 if let Component::Normal(name) = comp {
80 let s = name.to_string_lossy();
81 let cleaned_s = s.replace(['\n', ';'], "_");
82 if cleaned_s == s {
83 cleaned.push(name);
84 } else {
85 cleaned.push(OsString::from(cleaned_s));
86 }
87 }
88 }
89 return cleaned;
90 }
91
92 let mut strictpath_comps: Vec<_> = system_norm
93 .components()
94 .filter(|c| !matches!(c, Component::Prefix(_) | Component::RootDir))
95 .collect();
96 let mut boundary_comps: Vec<_> = jail_norm
97 .components()
98 .filter(|c| !matches!(c, Component::Prefix(_) | Component::RootDir))
99 .collect();
100
101 #[cfg(windows)]
102 fn comp_eq(a: &Component, b: &Component) -> bool {
103 match (a, b) {
104 (Component::Normal(x), Component::Normal(y)) => {
105 x.to_string_lossy().to_ascii_lowercase()
106 == y.to_string_lossy().to_ascii_lowercase()
107 }
108 _ => false,
109 }
110 }
111
112 #[cfg(not(windows))]
113 fn comp_eq(a: &Component, b: &Component) -> bool {
114 a == b
115 }
116
117 while !strictpath_comps.is_empty()
118 && !boundary_comps.is_empty()
119 && comp_eq(&strictpath_comps[0], &boundary_comps[0])
120 {
121 strictpath_comps.remove(0);
122 boundary_comps.remove(0);
123 }
124
125 let mut vb = std::path::PathBuf::new();
126 for c in strictpath_comps {
127 if let Component::Normal(name) = c {
128 let s = name.to_string_lossy();
129 let cleaned = s.replace(['\n', ';'], "_");
130 if cleaned == s {
131 vb.push(name);
132 } else {
133 vb.push(OsString::from(cleaned));
134 }
135 }
136 }
137 vb
138 }
139
140 let virtual_path = compute_virtual(restricted_path.path(), restricted_path.boundary());
141
142 Self {
143 inner: restricted_path,
144 virtual_path,
145 }
146 }
147
148 #[inline]
150 pub fn unvirtual(self) -> StrictPath<Marker> {
151 self.inner
152 }
153
154 #[inline]
158 pub fn as_unvirtual(&self) -> &StrictPath<Marker> {
159 &self.inner
160 }
161
162 #[inline]
164 pub fn interop_path(&self) -> &OsStr {
165 self.inner.interop_path()
166 }
167
168 #[inline]
170 pub fn virtual_join<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
171 let candidate = self.virtual_path.join(path.as_ref());
173 let anchored = crate::validator::path_history::PathHistory::new(candidate)
174 .canonicalize_anchored(self.inner.boundary())?;
175 let boundary_path = clamp(self.inner.boundary(), anchored)?;
176 Ok(VirtualPath::new(boundary_path))
177 }
178
179 pub fn virtualpath_parent(&self) -> Result<Option<Self>> {
184 match self.virtual_path.parent() {
185 Some(parent_virtual_path) => {
186 let anchored = crate::validator::path_history::PathHistory::new(
187 parent_virtual_path.to_path_buf(),
188 )
189 .canonicalize_anchored(self.inner.boundary())?;
190 let restricted_path = clamp(self.inner.boundary(), anchored)?;
191 Ok(Some(VirtualPath::new(restricted_path)))
192 }
193 None => Ok(None),
194 }
195 }
196
197 #[inline]
199 pub fn virtualpath_with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Result<Self> {
200 let candidate = self.virtual_path.with_file_name(file_name);
201 let anchored = crate::validator::path_history::PathHistory::new(candidate)
202 .canonicalize_anchored(self.inner.boundary())?;
203 let restricted_path = clamp(self.inner.boundary(), anchored)?;
204 Ok(VirtualPath::new(restricted_path))
205 }
206
207 pub fn virtualpath_with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Result<Self> {
209 if self.virtual_path.file_name().is_none() {
210 return Err(StrictPathError::path_escapes_boundary(
211 self.virtual_path.clone(),
212 self.inner.boundary().path().to_path_buf(),
213 ));
214 }
215
216 let candidate = self.virtual_path.with_extension(extension);
217 let anchored = crate::validator::path_history::PathHistory::new(candidate)
218 .canonicalize_anchored(self.inner.boundary())?;
219 let restricted_path = clamp(self.inner.boundary(), anchored)?;
220 Ok(VirtualPath::new(restricted_path))
221 }
222
223 #[inline]
225 pub fn virtualpath_file_name(&self) -> Option<&OsStr> {
226 self.virtual_path.file_name()
227 }
228
229 #[inline]
231 pub fn virtualpath_file_stem(&self) -> Option<&OsStr> {
232 self.virtual_path.file_stem()
233 }
234
235 #[inline]
237 pub fn virtualpath_extension(&self) -> Option<&OsStr> {
238 self.virtual_path.extension()
239 }
240
241 #[inline]
243 pub fn virtualpath_starts_with<P: AsRef<Path>>(&self, p: P) -> bool {
244 self.virtual_path.starts_with(p)
245 }
246
247 #[inline]
249 pub fn virtualpath_ends_with<P: AsRef<Path>>(&self, p: P) -> bool {
250 self.virtual_path.ends_with(p)
251 }
252
253 #[inline]
255 pub fn virtualpath_display(&self) -> VirtualPathDisplay<'_, Marker> {
256 VirtualPathDisplay(self)
257 }
258
259 #[inline]
261 pub fn exists(&self) -> bool {
262 self.inner.exists()
263 }
264
265 #[inline]
267 pub fn is_file(&self) -> bool {
268 self.inner.is_file()
269 }
270
271 #[inline]
273 pub fn is_dir(&self) -> bool {
274 self.inner.is_dir()
275 }
276
277 #[inline]
279 pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
280 self.inner.metadata()
281 }
282
283 #[inline]
285 pub fn read_to_string(&self) -> std::io::Result<String> {
286 self.inner.read_to_string()
287 }
288
289 #[inline]
291 pub fn read_bytes(&self) -> std::io::Result<Vec<u8>> {
292 self.inner.read_bytes()
293 }
294
295 #[inline]
297 pub fn write_bytes(&self, data: &[u8]) -> std::io::Result<()> {
298 self.inner.write_bytes(data)
299 }
300
301 #[inline]
303 pub fn write_string(&self, data: &str) -> std::io::Result<()> {
304 self.inner.write_string(data)
305 }
306
307 #[inline]
309 pub fn create_dir_all(&self) -> std::io::Result<()> {
310 self.inner.create_dir_all()
311 }
312
313 #[inline]
317 pub fn create_dir(&self) -> std::io::Result<()> {
318 self.inner.create_dir()
319 }
320
321 #[inline]
326 pub fn create_parent_dir(&self) -> std::io::Result<()> {
327 match self.virtualpath_parent() {
328 Ok(Some(parent)) => parent.create_dir(),
329 Ok(None) => Ok(()),
330 Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
331 Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
332 }
333 }
334
335 #[inline]
339 pub fn create_parent_dir_all(&self) -> std::io::Result<()> {
340 match self.virtualpath_parent() {
341 Ok(Some(parent)) => parent.create_dir_all(),
342 Ok(None) => Ok(()),
343 Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
344 Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
345 }
346 }
347
348 #[inline]
350 pub fn remove_file(&self) -> std::io::Result<()> {
351 self.inner.remove_file()
352 }
353
354 #[inline]
356 pub fn remove_dir(&self) -> std::io::Result<()> {
357 self.inner.remove_dir()
358 }
359
360 #[inline]
362 pub fn remove_dir_all(&self) -> std::io::Result<()> {
363 self.inner.remove_dir_all()
364 }
365
366 pub fn virtual_rename<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<Self> {
375 let dest_ref = dest.as_ref();
376 let dest_v = if dest_ref.is_absolute() {
377 match self.virtual_join(dest_ref) {
378 Ok(p) => p,
379 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
380 }
381 } else {
382 let parent = match self.virtualpath_parent() {
384 Ok(Some(p)) => p,
385 Ok(None) => match self.virtual_join("") {
386 Ok(root) => root,
387 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
388 },
389 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
390 };
391 match parent.virtual_join(dest_ref) {
392 Ok(p) => p,
393 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
394 }
395 };
396
397 let moved_strict = self.inner.strict_rename(dest_v.inner.path())?;
399 Ok(moved_strict.virtualize())
400 }
401}
402
403pub struct VirtualPathDisplay<'a, Marker>(&'a VirtualPath<Marker>);
404
405impl<'a, Marker> fmt::Display for VirtualPathDisplay<'a, Marker> {
406 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407 let s_lossy = self.0.virtual_path.to_string_lossy();
409 let s_norm: std::borrow::Cow<'_, str> = {
410 #[cfg(windows)]
411 {
412 std::borrow::Cow::Owned(s_lossy.replace('\\', "/"))
413 }
414 #[cfg(not(windows))]
415 {
416 std::borrow::Cow::Borrowed(&s_lossy)
417 }
418 };
419 if s_norm.starts_with('/') {
420 write!(f, "{s_norm}")
421 } else {
422 write!(f, "/{s_norm}")
423 }
424 }
425}
426
427impl<Marker> fmt::Debug for VirtualPath<Marker> {
428 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
429 f.debug_struct("VirtualPath")
430 .field("system_path", &self.inner.path())
431 .field("virtual", &format!("{}", self.virtualpath_display()))
432 .field("boundary", &self.inner.boundary().path())
433 .field("marker", &std::any::type_name::<Marker>())
434 .finish()
435 }
436}
437
438impl<Marker> PartialEq for VirtualPath<Marker> {
439 #[inline]
440 fn eq(&self, other: &Self) -> bool {
441 self.inner.path() == other.inner.path()
442 }
443}
444
445impl<Marker> Eq for VirtualPath<Marker> {}
446
447impl<Marker> Hash for VirtualPath<Marker> {
448 #[inline]
449 fn hash<H: Hasher>(&self, state: &mut H) {
450 self.inner.path().hash(state);
451 }
452}
453
454impl<Marker> PartialEq<crate::path::strict_path::StrictPath<Marker>> for VirtualPath<Marker> {
455 #[inline]
456 fn eq(&self, other: &crate::path::strict_path::StrictPath<Marker>) -> bool {
457 self.inner.path() == other.path()
458 }
459}
460
461impl<T: AsRef<Path>, Marker> PartialEq<T> for VirtualPath<Marker> {
462 #[inline]
463 fn eq(&self, other: &T) -> bool {
464 let virtual_str = format!("{}", self.virtualpath_display());
467 let other_str = other.as_ref().to_string_lossy();
468
469 let normalized_virtual = virtual_str.as_str();
471
472 #[cfg(windows)]
473 let other_normalized = other_str.replace('\\', "/");
474 #[cfg(not(windows))]
475 let other_normalized = other_str.to_string();
476
477 let normalized_other = if other_normalized.starts_with('/') {
478 other_normalized
479 } else {
480 format!("/{}", other_normalized)
481 };
482
483 normalized_virtual == normalized_other
484 }
485}
486
487impl<T: AsRef<Path>, Marker> PartialOrd<T> for VirtualPath<Marker> {
488 #[inline]
489 fn partial_cmp(&self, other: &T) -> Option<std::cmp::Ordering> {
490 let virtual_str = format!("{}", self.virtualpath_display());
492 let other_str = other.as_ref().to_string_lossy();
493
494 let normalized_virtual = virtual_str.as_str();
496
497 #[cfg(windows)]
498 let other_normalized = other_str.replace('\\', "/");
499 #[cfg(not(windows))]
500 let other_normalized = other_str.to_string();
501
502 let normalized_other = if other_normalized.starts_with('/') {
503 other_normalized
504 } else {
505 format!("/{}", other_normalized)
506 };
507
508 Some(normalized_virtual.cmp(&normalized_other))
509 }
510}
511
512impl<Marker> PartialOrd for VirtualPath<Marker> {
513 #[inline]
514 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
515 Some(self.cmp(other))
516 }
517}
518
519impl<Marker> Ord for VirtualPath<Marker> {
520 #[inline]
521 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
522 self.inner.path().cmp(other.inner.path())
523 }
524}
525
526#[cfg(feature = "serde")]
527impl<Marker> serde::Serialize for VirtualPath<Marker> {
528 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
529 where
530 S: serde::Serializer,
531 {
532 serializer.serialize_str(&format!("{}", self.virtualpath_display()))
533 }
534}