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> {
37 let vroot = crate::validator::virtual_root::VirtualRoot::try_new(root)?;
38 vroot.virtual_join("")
39 }
40
41 pub fn with_root_create<P: AsRef<Path>>(root: P) -> Result<Self> {
45 let vroot = crate::validator::virtual_root::VirtualRoot::try_new_create(root)?;
46 vroot.virtual_join("")
47 }
48 #[inline]
49 pub(crate) fn new(restricted_path: StrictPath<Marker>) -> Self {
50 fn compute_virtual<Marker>(
51 system_path: &std::path::Path,
52 restriction: &crate::PathBoundary<Marker>,
53 ) -> std::path::PathBuf {
54 use std::ffi::OsString;
55 use std::path::Component;
56
57 #[cfg(windows)]
58 fn strip_verbatim(p: &std::path::Path) -> std::path::PathBuf {
59 let s = p.as_os_str().to_string_lossy();
60 if let Some(trimmed) = s.strip_prefix("\\\\?\\") {
61 return std::path::PathBuf::from(trimmed);
62 }
63 if let Some(trimmed) = s.strip_prefix("\\\\.\\") {
64 return std::path::PathBuf::from(trimmed);
65 }
66 std::path::PathBuf::from(s.to_string())
67 }
68
69 #[cfg(not(windows))]
70 fn strip_verbatim(p: &std::path::Path) -> std::path::PathBuf {
71 p.to_path_buf()
72 }
73
74 let system_norm = strip_verbatim(system_path);
75 let jail_norm = strip_verbatim(restriction.path());
76
77 if let Ok(stripped) = system_norm.strip_prefix(&jail_norm) {
78 let mut cleaned = std::path::PathBuf::new();
79 for comp in stripped.components() {
80 if let Component::Normal(name) = comp {
81 let s = name.to_string_lossy();
82 let cleaned_s = s.replace(['\n', ';'], "_");
83 if cleaned_s == s {
84 cleaned.push(name);
85 } else {
86 cleaned.push(OsString::from(cleaned_s));
87 }
88 }
89 }
90 return cleaned;
91 }
92
93 let mut strictpath_comps: Vec<_> = system_norm
94 .components()
95 .filter(|c| !matches!(c, Component::Prefix(_) | Component::RootDir))
96 .collect();
97 let mut boundary_comps: Vec<_> = jail_norm
98 .components()
99 .filter(|c| !matches!(c, Component::Prefix(_) | Component::RootDir))
100 .collect();
101
102 #[cfg(windows)]
103 fn comp_eq(a: &Component, b: &Component) -> bool {
104 match (a, b) {
105 (Component::Normal(x), Component::Normal(y)) => {
106 x.to_string_lossy().to_ascii_lowercase()
107 == y.to_string_lossy().to_ascii_lowercase()
108 }
109 _ => false,
110 }
111 }
112
113 #[cfg(not(windows))]
114 fn comp_eq(a: &Component, b: &Component) -> bool {
115 a == b
116 }
117
118 while !strictpath_comps.is_empty()
119 && !boundary_comps.is_empty()
120 && comp_eq(&strictpath_comps[0], &boundary_comps[0])
121 {
122 strictpath_comps.remove(0);
123 boundary_comps.remove(0);
124 }
125
126 let mut vb = std::path::PathBuf::new();
127 for c in strictpath_comps {
128 if let Component::Normal(name) = c {
129 let s = name.to_string_lossy();
130 let cleaned = s.replace(['\n', ';'], "_");
131 if cleaned == s {
132 vb.push(name);
133 } else {
134 vb.push(OsString::from(cleaned));
135 }
136 }
137 }
138 vb
139 }
140
141 let virtual_path = compute_virtual(restricted_path.path(), restricted_path.boundary());
142
143 Self {
144 inner: restricted_path,
145 virtual_path,
146 }
147 }
148
149 #[inline]
151 pub fn unvirtual(self) -> StrictPath<Marker> {
152 self.inner
153 }
154
155 #[inline]
160 pub fn try_into_root(self) -> crate::validator::virtual_root::VirtualRoot<Marker> {
161 self.inner.try_into_boundary().virtualize()
162 }
163
164 #[inline]
170 pub fn try_into_root_create(self) -> crate::validator::virtual_root::VirtualRoot<Marker> {
171 let boundary = self.inner.try_into_boundary();
172 if !boundary.exists() {
173 let _ = std::fs::create_dir_all(boundary.as_ref());
175 }
176 boundary.virtualize()
177 }
178
179 #[inline]
183 pub fn as_unvirtual(&self) -> &StrictPath<Marker> {
184 &self.inner
185 }
186
187 #[inline]
189 pub fn interop_path(&self) -> &OsStr {
190 self.inner.interop_path()
191 }
192
193 #[inline]
195 pub fn virtual_join<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
196 let candidate = self.virtual_path.join(path.as_ref());
198 let anchored = crate::validator::path_history::PathHistory::new(candidate)
199 .canonicalize_anchored(self.inner.boundary())?;
200 let boundary_path = clamp(self.inner.boundary(), anchored)?;
201 Ok(VirtualPath::new(boundary_path))
202 }
203
204 pub fn virtualpath_parent(&self) -> Result<Option<Self>> {
209 match self.virtual_path.parent() {
210 Some(parent_virtual_path) => {
211 let anchored = crate::validator::path_history::PathHistory::new(
212 parent_virtual_path.to_path_buf(),
213 )
214 .canonicalize_anchored(self.inner.boundary())?;
215 let restricted_path = clamp(self.inner.boundary(), anchored)?;
216 Ok(Some(VirtualPath::new(restricted_path)))
217 }
218 None => Ok(None),
219 }
220 }
221
222 #[inline]
224 pub fn virtualpath_with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Result<Self> {
225 let candidate = self.virtual_path.with_file_name(file_name);
226 let anchored = crate::validator::path_history::PathHistory::new(candidate)
227 .canonicalize_anchored(self.inner.boundary())?;
228 let restricted_path = clamp(self.inner.boundary(), anchored)?;
229 Ok(VirtualPath::new(restricted_path))
230 }
231
232 pub fn virtualpath_with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Result<Self> {
234 if self.virtual_path.file_name().is_none() {
235 return Err(StrictPathError::path_escapes_boundary(
236 self.virtual_path.clone(),
237 self.inner.boundary().path().to_path_buf(),
238 ));
239 }
240
241 let candidate = self.virtual_path.with_extension(extension);
242 let anchored = crate::validator::path_history::PathHistory::new(candidate)
243 .canonicalize_anchored(self.inner.boundary())?;
244 let restricted_path = clamp(self.inner.boundary(), anchored)?;
245 Ok(VirtualPath::new(restricted_path))
246 }
247
248 #[inline]
250 pub fn virtualpath_file_name(&self) -> Option<&OsStr> {
251 self.virtual_path.file_name()
252 }
253
254 #[inline]
256 pub fn virtualpath_file_stem(&self) -> Option<&OsStr> {
257 self.virtual_path.file_stem()
258 }
259
260 #[inline]
262 pub fn virtualpath_extension(&self) -> Option<&OsStr> {
263 self.virtual_path.extension()
264 }
265
266 #[inline]
268 pub fn virtualpath_starts_with<P: AsRef<Path>>(&self, p: P) -> bool {
269 self.virtual_path.starts_with(p)
270 }
271
272 #[inline]
274 pub fn virtualpath_ends_with<P: AsRef<Path>>(&self, p: P) -> bool {
275 self.virtual_path.ends_with(p)
276 }
277
278 #[inline]
280 pub fn virtualpath_display(&self) -> VirtualPathDisplay<'_, Marker> {
281 VirtualPathDisplay(self)
282 }
283
284 #[inline]
286 pub fn exists(&self) -> bool {
287 self.inner.exists()
288 }
289
290 #[inline]
292 pub fn is_file(&self) -> bool {
293 self.inner.is_file()
294 }
295
296 #[inline]
298 pub fn is_dir(&self) -> bool {
299 self.inner.is_dir()
300 }
301
302 #[inline]
304 pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
305 self.inner.metadata()
306 }
307
308 #[inline]
310 pub fn read_to_string(&self) -> std::io::Result<String> {
311 self.inner.read_to_string()
312 }
313
314 #[deprecated(since = "0.1.0-alpha.5", note = "Use read() instead")]
316 #[inline]
317 pub fn read_bytes(&self) -> std::io::Result<Vec<u8>> {
318 self.inner.read()
319 }
320
321 #[deprecated(since = "0.1.0-alpha.5", note = "Use write(...) instead")]
323 #[inline]
324 pub fn write_bytes(&self, data: &[u8]) -> std::io::Result<()> {
325 self.inner.write(data)
326 }
327
328 #[deprecated(since = "0.1.0-alpha.5", note = "Use write(...) instead")]
330 #[inline]
331 pub fn write_string(&self, data: &str) -> std::io::Result<()> {
332 self.inner.write(data)
333 }
334
335 #[inline]
337 pub fn read(&self) -> std::io::Result<Vec<u8>> {
338 self.inner.read()
339 }
340
341 pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
346 self.inner.read_dir()
347 }
348
349 #[inline]
351 pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> std::io::Result<()> {
352 self.inner.write(contents)
353 }
354
355 #[inline]
357 pub fn create_dir_all(&self) -> std::io::Result<()> {
358 self.inner.create_dir_all()
359 }
360
361 #[inline]
365 pub fn create_dir(&self) -> std::io::Result<()> {
366 self.inner.create_dir()
367 }
368
369 #[inline]
374 pub fn create_parent_dir(&self) -> std::io::Result<()> {
375 match self.virtualpath_parent() {
376 Ok(Some(parent)) => parent.create_dir(),
377 Ok(None) => Ok(()),
378 Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
379 Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
380 }
381 }
382
383 #[inline]
387 pub fn create_parent_dir_all(&self) -> std::io::Result<()> {
388 match self.virtualpath_parent() {
389 Ok(Some(parent)) => parent.create_dir_all(),
390 Ok(None) => Ok(()),
391 Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
392 Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
393 }
394 }
395
396 #[inline]
398 pub fn remove_file(&self) -> std::io::Result<()> {
399 self.inner.remove_file()
400 }
401
402 #[inline]
404 pub fn remove_dir(&self) -> std::io::Result<()> {
405 self.inner.remove_dir()
406 }
407
408 #[inline]
410 pub fn remove_dir_all(&self) -> std::io::Result<()> {
411 self.inner.remove_dir_all()
412 }
413
414 pub fn virtual_rename<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<Self> {
423 let dest_ref = dest.as_ref();
424 let dest_v = if dest_ref.is_absolute() {
425 match self.virtual_join(dest_ref) {
426 Ok(p) => p,
427 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
428 }
429 } else {
430 let parent = match self.virtualpath_parent() {
432 Ok(Some(p)) => p,
433 Ok(None) => match self.virtual_join("") {
434 Ok(root) => root,
435 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
436 },
437 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
438 };
439 match parent.virtual_join(dest_ref) {
440 Ok(p) => p,
441 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
442 }
443 };
444
445 let moved_strict = self.inner.strict_rename(dest_v.inner.path())?;
447 Ok(moved_strict.virtualize())
448 }
449
450 pub fn virtual_copy<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<Self> {
460 let dest_ref = dest.as_ref();
461 let dest_v = if dest_ref.is_absolute() {
462 match self.virtual_join(dest_ref) {
463 Ok(p) => p,
464 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
465 }
466 } else {
467 let parent = match self.virtualpath_parent() {
469 Ok(Some(p)) => p,
470 Ok(None) => match self.virtual_join("") {
471 Ok(root) => root,
472 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
473 },
474 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
475 };
476 match parent.virtual_join(dest_ref) {
477 Ok(p) => p,
478 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
479 }
480 };
481
482 std::fs::copy(self.inner.path(), dest_v.inner.path())?;
484 Ok(dest_v)
485 }
486}
487
488pub struct VirtualPathDisplay<'a, Marker>(&'a VirtualPath<Marker>);
489
490impl<'a, Marker> fmt::Display for VirtualPathDisplay<'a, Marker> {
491 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
492 let s_lossy = self.0.virtual_path.to_string_lossy();
494 let s_norm: std::borrow::Cow<'_, str> = {
495 #[cfg(windows)]
496 {
497 std::borrow::Cow::Owned(s_lossy.replace('\\', "/"))
498 }
499 #[cfg(not(windows))]
500 {
501 std::borrow::Cow::Borrowed(&s_lossy)
502 }
503 };
504 if s_norm.starts_with('/') {
505 write!(f, "{s_norm}")
506 } else {
507 write!(f, "/{s_norm}")
508 }
509 }
510}
511
512impl<Marker> fmt::Debug for VirtualPath<Marker> {
513 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
514 f.debug_struct("VirtualPath")
515 .field("system_path", &self.inner.path())
516 .field("virtual", &format!("{}", self.virtualpath_display()))
517 .field("boundary", &self.inner.boundary().path())
518 .field("marker", &std::any::type_name::<Marker>())
519 .finish()
520 }
521}
522
523impl<Marker> PartialEq for VirtualPath<Marker> {
524 #[inline]
525 fn eq(&self, other: &Self) -> bool {
526 self.inner.path() == other.inner.path()
527 }
528}
529
530impl<Marker> Eq for VirtualPath<Marker> {}
531
532impl<Marker> Hash for VirtualPath<Marker> {
533 #[inline]
534 fn hash<H: Hasher>(&self, state: &mut H) {
535 self.inner.path().hash(state);
536 }
537}
538
539impl<Marker> PartialEq<crate::path::strict_path::StrictPath<Marker>> for VirtualPath<Marker> {
540 #[inline]
541 fn eq(&self, other: &crate::path::strict_path::StrictPath<Marker>) -> bool {
542 self.inner.path() == other.path()
543 }
544}
545
546impl<T: AsRef<Path>, Marker> PartialEq<T> for VirtualPath<Marker> {
547 #[inline]
548 fn eq(&self, other: &T) -> bool {
549 let virtual_str = format!("{}", self.virtualpath_display());
552 let other_str = other.as_ref().to_string_lossy();
553
554 let normalized_virtual = virtual_str.as_str();
556
557 #[cfg(windows)]
558 let other_normalized = other_str.replace('\\', "/");
559 #[cfg(not(windows))]
560 let other_normalized = other_str.to_string();
561
562 let normalized_other = if other_normalized.starts_with('/') {
563 other_normalized
564 } else {
565 format!("/{}", other_normalized)
566 };
567
568 normalized_virtual == normalized_other
569 }
570}
571
572impl<T: AsRef<Path>, Marker> PartialOrd<T> for VirtualPath<Marker> {
573 #[inline]
574 fn partial_cmp(&self, other: &T) -> Option<std::cmp::Ordering> {
575 let virtual_str = format!("{}", self.virtualpath_display());
577 let other_str = other.as_ref().to_string_lossy();
578
579 let normalized_virtual = virtual_str.as_str();
581
582 #[cfg(windows)]
583 let other_normalized = other_str.replace('\\', "/");
584 #[cfg(not(windows))]
585 let other_normalized = other_str.to_string();
586
587 let normalized_other = if other_normalized.starts_with('/') {
588 other_normalized
589 } else {
590 format!("/{}", other_normalized)
591 };
592
593 Some(normalized_virtual.cmp(&normalized_other))
594 }
595}
596
597impl<Marker> PartialOrd for VirtualPath<Marker> {
598 #[inline]
599 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
600 Some(self.cmp(other))
601 }
602}
603
604impl<Marker> Ord for VirtualPath<Marker> {
605 #[inline]
606 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
607 self.inner.path().cmp(other.inner.path())
608 }
609}
610
611#[cfg(feature = "serde")]
612impl<Marker> serde::Serialize for VirtualPath<Marker> {
613 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
614 where
615 S: serde::Serializer,
616 {
617 serializer.serialize_str(&format!("{}", self.virtualpath_display()))
618 }
619}