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