vfs/
vfs_path.rs

1//! Abstract-ish representation of paths for VFS.
2use std::fmt;
3
4use paths::{AbsPath, AbsPathBuf, RelPath};
5
6/// Path in [`Vfs`].
7///
8/// Long-term, we want to support files which do not reside in the file-system,
9/// so we treat `VfsPath`s as opaque identifiers.
10///
11/// [`Vfs`]: crate::Vfs
12#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
13pub struct VfsPath(VfsPathRepr);
14
15impl VfsPath {
16    /// Creates an "in-memory" path from `/`-separated string.
17    ///
18    /// This is most useful for testing, to avoid windows/linux differences
19    ///
20    /// # Panics
21    ///
22    /// Panics if `path` does not start with `'/'`.
23    pub fn new_virtual_path(path: String) -> VfsPath {
24        assert!(path.starts_with('/'));
25        VfsPath(VfsPathRepr::VirtualPath(VirtualPath(path)))
26    }
27
28    /// Create a path from string. Input should be a string representation of
29    /// an absolute path inside filesystem
30    pub fn new_real_path(path: String) -> VfsPath {
31        VfsPath::from(AbsPathBuf::assert(path.into()))
32    }
33
34    /// Returns the `AbsPath` representation of `self` if `self` is on the file system.
35    pub fn as_path(&self) -> Option<&AbsPath> {
36        match &self.0 {
37            VfsPathRepr::PathBuf(it) => Some(it.as_path()),
38            VfsPathRepr::VirtualPath(_) => None,
39        }
40    }
41
42    pub fn into_abs_path(self) -> Option<AbsPathBuf> {
43        match self.0 {
44            VfsPathRepr::PathBuf(it) => Some(it),
45            VfsPathRepr::VirtualPath(_) => None,
46        }
47    }
48
49    /// Creates a new `VfsPath` with `path` adjoined to `self`.
50    pub fn join(&self, path: &str) -> Option<VfsPath> {
51        match &self.0 {
52            VfsPathRepr::PathBuf(it) => {
53                let res = it.join(path).normalize();
54                Some(VfsPath(VfsPathRepr::PathBuf(res)))
55            }
56            VfsPathRepr::VirtualPath(it) => {
57                let res = it.join(path)?;
58                Some(VfsPath(VfsPathRepr::VirtualPath(res)))
59            }
60        }
61    }
62
63    /// Remove the last component of `self` if there is one.
64    ///
65    /// If `self` has no component, returns `false`; else returns `true`.
66    ///
67    /// # Example
68    ///
69    /// ```ignore
70    /// # use vfs::{AbsPathBuf, VfsPath};
71    /// let mut path = VfsPath::from(AbsPathBuf::assert("/foo/bar".into()));
72    /// assert!(path.pop());
73    /// assert_eq!(path, VfsPath::from(AbsPathBuf::assert("/foo".into())));
74    /// assert!(path.pop());
75    /// assert_eq!(path, VfsPath::from(AbsPathBuf::assert("/".into())));
76    /// assert!(!path.pop());
77    /// ```
78    pub fn pop(&mut self) -> bool {
79        match &mut self.0 {
80            VfsPathRepr::PathBuf(it) => it.pop(),
81            VfsPathRepr::VirtualPath(it) => it.pop(),
82        }
83    }
84
85    /// Returns `true` if `other` is a prefix of `self`.
86    pub fn starts_with(&self, other: &VfsPath) -> bool {
87        match (&self.0, &other.0) {
88            (VfsPathRepr::PathBuf(lhs), VfsPathRepr::PathBuf(rhs)) => lhs.starts_with(rhs),
89            (VfsPathRepr::VirtualPath(lhs), VfsPathRepr::VirtualPath(rhs)) => lhs.starts_with(rhs),
90            (VfsPathRepr::PathBuf(_) | VfsPathRepr::VirtualPath(_), _) => false,
91        }
92    }
93
94    pub fn strip_prefix(&self, other: &VfsPath) -> Option<&RelPath> {
95        match (&self.0, &other.0) {
96            (VfsPathRepr::PathBuf(lhs), VfsPathRepr::PathBuf(rhs)) => lhs.strip_prefix(rhs),
97            (VfsPathRepr::VirtualPath(lhs), VfsPathRepr::VirtualPath(rhs)) => lhs.strip_prefix(rhs),
98            (VfsPathRepr::PathBuf(_) | VfsPathRepr::VirtualPath(_), _) => None,
99        }
100    }
101
102    /// Returns the `VfsPath` without its final component, if there is one.
103    ///
104    /// Returns [`None`] if the path is a root or prefix.
105    pub fn parent(&self) -> Option<VfsPath> {
106        let mut parent = self.clone();
107        if parent.pop() { Some(parent) } else { None }
108    }
109
110    /// Returns `self`'s base name and file extension.
111    pub fn name_and_extension(&self) -> Option<(&str, Option<&str>)> {
112        match &self.0 {
113            VfsPathRepr::PathBuf(p) => p.name_and_extension(),
114            VfsPathRepr::VirtualPath(p) => p.name_and_extension(),
115        }
116    }
117
118    /// **Don't make this `pub`**
119    ///
120    /// Encode the path in the given buffer.
121    ///
122    /// The encoding will be `0` if [`AbsPathBuf`], `1` if [`VirtualPath`], followed
123    /// by `self`'s representation.
124    ///
125    /// Note that this encoding is dependent on the operating system.
126    pub(crate) fn encode(&self, buf: &mut Vec<u8>) {
127        let tag = match &self.0 {
128            VfsPathRepr::PathBuf(_) => 0,
129            VfsPathRepr::VirtualPath(_) => 1,
130        };
131        buf.push(tag);
132        match &self.0 {
133            VfsPathRepr::PathBuf(path) => {
134                #[cfg(windows)]
135                {
136                    use windows_paths::Encode;
137                    let path: &std::path::Path = path.as_ref();
138                    let components = path.components();
139                    let mut add_sep = false;
140                    for component in components {
141                        if add_sep {
142                            windows_paths::SEP.encode(buf);
143                        }
144                        let len_before = buf.len();
145                        match component {
146                            std::path::Component::Prefix(prefix) => {
147                                // kind() returns a normalized and comparable path prefix.
148                                prefix.kind().encode(buf);
149                            }
150                            std::path::Component::RootDir => {
151                                if !add_sep {
152                                    component.as_os_str().encode(buf);
153                                }
154                            }
155                            _ => component.as_os_str().encode(buf),
156                        }
157
158                        // some components may be encoded empty
159                        add_sep = len_before != buf.len();
160                    }
161                }
162                #[cfg(unix)]
163                {
164                    use std::os::unix::ffi::OsStrExt;
165                    buf.extend(path.as_os_str().as_bytes());
166                }
167                #[cfg(not(any(windows, unix)))]
168                {
169                    buf.extend(path.as_os_str().to_string_lossy().as_bytes());
170                }
171            }
172            VfsPathRepr::VirtualPath(VirtualPath(s)) => buf.extend(s.as_bytes()),
173        }
174    }
175}
176
177#[cfg(windows)]
178mod windows_paths {
179    pub(crate) trait Encode {
180        fn encode(&self, buf: &mut Vec<u8>);
181    }
182
183    impl Encode for std::ffi::OsStr {
184        fn encode(&self, buf: &mut Vec<u8>) {
185            use std::os::windows::ffi::OsStrExt;
186            for wchar in self.encode_wide() {
187                buf.extend(wchar.to_le_bytes().iter().copied());
188            }
189        }
190    }
191
192    impl Encode for u8 {
193        fn encode(&self, buf: &mut Vec<u8>) {
194            let wide = *self as u16;
195            buf.extend(wide.to_le_bytes().iter().copied())
196        }
197    }
198
199    impl Encode for &str {
200        fn encode(&self, buf: &mut Vec<u8>) {
201            debug_assert!(self.is_ascii());
202            for b in self.as_bytes() {
203                b.encode(buf)
204            }
205        }
206    }
207
208    pub(crate) const SEP: &str = "\\";
209    const VERBATIM: &str = "\\\\?\\";
210    const UNC: &str = "UNC";
211    const DEVICE: &str = "\\\\.\\";
212    const COLON: &str = ":";
213
214    impl Encode for std::path::Prefix<'_> {
215        fn encode(&self, buf: &mut Vec<u8>) {
216            match self {
217                std::path::Prefix::Verbatim(c) => {
218                    VERBATIM.encode(buf);
219                    c.encode(buf);
220                }
221                std::path::Prefix::VerbatimUNC(server, share) => {
222                    VERBATIM.encode(buf);
223                    UNC.encode(buf);
224                    SEP.encode(buf);
225                    server.encode(buf);
226                    SEP.encode(buf);
227                    share.encode(buf);
228                }
229                std::path::Prefix::VerbatimDisk(d) => {
230                    VERBATIM.encode(buf);
231                    d.encode(buf);
232                    COLON.encode(buf);
233                }
234                std::path::Prefix::DeviceNS(device) => {
235                    DEVICE.encode(buf);
236                    device.encode(buf);
237                }
238                std::path::Prefix::UNC(server, share) => {
239                    SEP.encode(buf);
240                    SEP.encode(buf);
241                    server.encode(buf);
242                    SEP.encode(buf);
243                    share.encode(buf);
244                }
245                std::path::Prefix::Disk(d) => {
246                    d.encode(buf);
247                    COLON.encode(buf);
248                }
249            }
250        }
251    }
252    #[test]
253    fn paths_encoding() {
254        // drive letter casing agnostic
255        test_eq("C:/x.rs", "c:/x.rs");
256        // separator agnostic
257        test_eq("C:/x/y.rs", "C:\\x\\y.rs");
258
259        fn test_eq(a: &str, b: &str) {
260            let mut b1 = Vec::new();
261            let mut b2 = Vec::new();
262            vfs(a).encode(&mut b1);
263            vfs(b).encode(&mut b2);
264            assert_eq!(b1, b2);
265        }
266    }
267
268    #[test]
269    fn test_sep_root_dir_encoding() {
270        let mut buf = Vec::new();
271        vfs("C:/x/y").encode(&mut buf);
272        assert_eq!(&buf, &[0, 67, 0, 58, 0, 92, 0, 120, 0, 92, 0, 121, 0])
273    }
274
275    #[cfg(test)]
276    fn vfs(str: &str) -> super::VfsPath {
277        use super::{AbsPathBuf, VfsPath};
278        VfsPath::from(AbsPathBuf::try_from(str).unwrap())
279    }
280}
281
282/// Internal, private representation of [`VfsPath`].
283#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
284enum VfsPathRepr {
285    PathBuf(AbsPathBuf),
286    VirtualPath(VirtualPath),
287}
288
289impl From<AbsPathBuf> for VfsPath {
290    fn from(v: AbsPathBuf) -> Self {
291        VfsPath(VfsPathRepr::PathBuf(v.normalize()))
292    }
293}
294
295impl fmt::Display for VfsPath {
296    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297        match &self.0 {
298            VfsPathRepr::PathBuf(it) => it.fmt(f),
299            VfsPathRepr::VirtualPath(VirtualPath(it)) => it.fmt(f),
300        }
301    }
302}
303
304impl fmt::Debug for VfsPath {
305    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306        fmt::Debug::fmt(&self.0, f)
307    }
308}
309
310impl fmt::Debug for VfsPathRepr {
311    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312        match &self {
313            VfsPathRepr::PathBuf(it) => it.fmt(f),
314            VfsPathRepr::VirtualPath(VirtualPath(it)) => it.fmt(f),
315        }
316    }
317}
318
319impl PartialEq<AbsPath> for VfsPath {
320    fn eq(&self, other: &AbsPath) -> bool {
321        match &self.0 {
322            VfsPathRepr::PathBuf(lhs) => lhs == other,
323            VfsPathRepr::VirtualPath(_) => false,
324        }
325    }
326}
327impl PartialEq<VfsPath> for AbsPath {
328    fn eq(&self, other: &VfsPath) -> bool {
329        other == self
330    }
331}
332
333/// `/`-separated virtual path.
334///
335/// This is used to describe files that do not reside on the file system.
336#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
337struct VirtualPath(String);
338
339impl VirtualPath {
340    /// Returns `true` if `other` is a prefix of `self` (as strings).
341    fn starts_with(&self, other: &VirtualPath) -> bool {
342        self.0.starts_with(&other.0)
343    }
344
345    fn strip_prefix(&self, base: &VirtualPath) -> Option<&RelPath> {
346        <_ as AsRef<paths::Utf8Path>>::as_ref(&self.0)
347            .strip_prefix(&base.0)
348            .ok()
349            .map(RelPath::new_unchecked)
350    }
351
352    /// Remove the last component of `self`.
353    ///
354    /// This will find the last `'/'` in `self`, and remove everything after it,
355    /// including the `'/'`.
356    ///
357    /// If `self` contains no `'/'`, returns `false`; else returns `true`.
358    ///
359    /// # Example
360    ///
361    /// ```rust,ignore
362    /// let mut path = VirtualPath("/foo/bar".to_string());
363    /// path.pop();
364    /// assert_eq!(path.0, "/foo");
365    /// path.pop();
366    /// assert_eq!(path.0, "");
367    /// ```
368    fn pop(&mut self) -> bool {
369        let pos = match self.0.rfind('/') {
370            Some(pos) => pos,
371            None => return false,
372        };
373        self.0 = self.0[..pos].to_string();
374        true
375    }
376
377    /// Append the given *relative* path `path` to `self`.
378    ///
379    /// This will resolve any leading `"../"` in `path` before appending it.
380    ///
381    /// Returns [`None`] if `path` has more leading `"../"` than the number of
382    /// components in `self`.
383    ///
384    /// # Notes
385    ///
386    /// In practice, appending here means `self/path` as strings.
387    fn join(&self, mut path: &str) -> Option<VirtualPath> {
388        let mut res = self.clone();
389        while path.starts_with("../") {
390            if !res.pop() {
391                return None;
392            }
393            path = &path["../".len()..];
394        }
395        path = path.trim_start_matches("./");
396        res.0 = format!("{}/{path}", res.0);
397        Some(res)
398    }
399
400    /// Returns `self`'s base name and file extension.
401    ///
402    /// # Returns
403    /// - `None` if `self` ends with `"//"`.
404    /// - `Some((name, None))` if `self`'s base contains no `.`, or only one `.` at the start.
405    /// - `Some((name, Some(extension))` else.
406    ///
407    /// # Note
408    /// The extension will not contains `.`. This means `"/foo/bar.baz.rs"` will
409    /// return `Some(("bar.baz", Some("rs"))`.
410    fn name_and_extension(&self) -> Option<(&str, Option<&str>)> {
411        let file_path = if self.0.ends_with('/') { &self.0[..&self.0.len() - 1] } else { &self.0 };
412        let file_name = match file_path.rfind('/') {
413            Some(position) => &file_path[position + 1..],
414            None => file_path,
415        };
416
417        if file_name.is_empty() {
418            None
419        } else {
420            let mut file_stem_and_extension = file_name.rsplitn(2, '.');
421            let extension = file_stem_and_extension.next();
422            let file_stem = file_stem_and_extension.next();
423
424            match (file_stem, extension) {
425                (None, None) => None,
426                (None | Some(""), Some(_)) => Some((file_name, None)),
427                (Some(file_stem), extension) => Some((file_stem, extension)),
428            }
429        }
430    }
431}
432
433#[cfg(test)]
434mod tests;