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