typst_syntax/
path.rs

1use std::fmt::{self, Debug, Display, Formatter};
2use std::path::{Component, Path, PathBuf};
3
4/// An absolute path in the virtual file system of a project or package.
5#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
6pub struct VirtualPath(PathBuf);
7
8impl VirtualPath {
9    /// Create a new virtual path.
10    ///
11    /// Even if it doesn't start with `/` or `\`, it is still interpreted as
12    /// starting from the root.
13    pub fn new(path: impl AsRef<Path>) -> Self {
14        Self::new_impl(path.as_ref())
15    }
16
17    /// Non generic new implementation.
18    fn new_impl(path: &Path) -> Self {
19        let mut out = Path::new(&Component::RootDir).to_path_buf();
20        for component in path.components() {
21            match component {
22                Component::Prefix(_) | Component::RootDir => {}
23                Component::CurDir => {}
24                Component::ParentDir => match out.components().next_back() {
25                    Some(Component::Normal(_)) => {
26                        out.pop();
27                    }
28                    _ => out.push(component),
29                },
30                Component::Normal(_) => out.push(component),
31            }
32        }
33        Self(out)
34    }
35
36    /// Create a virtual path from a real path and a real root.
37    ///
38    /// Returns `None` if the file path is not contained in the root (i.e. if
39    /// `root` is not a lexical prefix of `path`). No file system operations are
40    /// performed.
41    pub fn within_root(path: &Path, root: &Path) -> Option<Self> {
42        path.strip_prefix(root).ok().map(Self::new)
43    }
44
45    /// Get the underlying path with a leading `/` or `\`.
46    pub fn as_rooted_path(&self) -> &Path {
47        &self.0
48    }
49
50    /// Get the underlying path without a leading `/` or `\`.
51    pub fn as_rootless_path(&self) -> &Path {
52        self.0.strip_prefix(Component::RootDir).unwrap_or(&self.0)
53    }
54
55    /// Resolve the virtual path relative to an actual file system root
56    /// (where the project or package resides).
57    ///
58    /// Returns `None` if the path lexically escapes the root. The path might
59    /// still escape through symlinks.
60    pub fn resolve(&self, root: &Path) -> Option<PathBuf> {
61        let root_len = root.as_os_str().len();
62        let mut out = root.to_path_buf();
63        for component in self.0.components() {
64            match component {
65                Component::Prefix(_) => {}
66                Component::RootDir => {}
67                Component::CurDir => {}
68                Component::ParentDir => {
69                    out.pop();
70                    if out.as_os_str().len() < root_len {
71                        return None;
72                    }
73                }
74                Component::Normal(_) => out.push(component),
75            }
76        }
77        Some(out)
78    }
79
80    /// Resolve a path relative to this virtual path.
81    pub fn join(&self, path: impl AsRef<Path>) -> Self {
82        if let Some(parent) = self.0.parent() {
83            Self::new(parent.join(path))
84        } else {
85            Self::new(path)
86        }
87    }
88
89    /// The same path, but with a different extension.
90    pub fn with_extension(&self, extension: &str) -> Self {
91        Self(self.0.with_extension(extension))
92    }
93}
94
95impl Debug for VirtualPath {
96    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
97        Display::fmt(&self.0.display(), f)
98    }
99}