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