Skip to main content

ruff_db/system/
path.rs

1use camino::{Utf8Path, Utf8PathBuf};
2use std::borrow::Borrow;
3use std::fmt::Formatter;
4use std::ops::Deref;
5use std::path::{Path, PathBuf, StripPrefixError};
6
7/// A slice of a path on [`System`](super::System) (akin to [`str`]).
8///
9/// The path is guaranteed to be valid UTF-8.
10#[repr(transparent)]
11#[derive(Eq, PartialEq, Hash, PartialOrd, Ord)]
12pub struct SystemPath(Utf8Path);
13
14impl SystemPath {
15    pub fn new(path: &(impl AsRef<Utf8Path> + ?Sized)) -> &Self {
16        let path = path.as_ref();
17        // SAFETY: FsPath is marked as #[repr(transparent)] so the conversion from a
18        // *const Utf8Path to a *const FsPath is valid.
19        unsafe { &*(path as *const Utf8Path as *const SystemPath) }
20    }
21
22    /// Takes any path, and when possible, converts Windows UNC paths to regular paths.
23    /// If the path can't be converted, it's returned unmodified.
24    ///
25    /// On non-Windows this is no-op.
26    ///
27    /// `\\?\C:\Windows` will be converted to `C:\Windows`,
28    /// but `\\?\C:\COM` will be left as-is (due to a reserved filename).
29    ///
30    /// Use this to pass arbitrary paths to programs that may not be UNC-aware.
31    ///
32    /// It's generally safe to pass UNC paths to legacy programs, because
33    /// these paths contain a reserved prefix, so will gracefully fail
34    /// if used with legacy APIs that don't support UNC.
35    ///
36    /// This function does not perform any I/O.
37    ///
38    /// Currently paths with unpaired surrogates aren't converted even if they
39    /// could be, due to limitations of Rust's `OsStr` API.
40    ///
41    /// To check if a path remained as UNC, use `path.as_os_str().as_encoded_bytes().starts_with(b"\\\\")`.
42    #[inline]
43    pub fn simplified(&self) -> &SystemPath {
44        // SAFETY: simplified only trims the path, that means the returned path must be a valid UTF-8 path.
45        SystemPath::from_std_path(dunce::simplified(self.as_std_path())).unwrap()
46    }
47
48    /// Returns `true` if the `SystemPath` is absolute, i.e., if it is independent of
49    /// the current directory.
50    ///
51    /// * On Unix, a path is absolute if it starts with the root, so
52    ///   `is_absolute` and [`has_root`] are equivalent.
53    ///
54    /// * On Windows, a path is absolute if it has a prefix and starts with the
55    ///   root: `c:\windows` is absolute, while `c:temp` and `\temp` are not.
56    ///
57    /// # Examples
58    ///
59    /// ```
60    /// use ruff_db::system::SystemPath;
61    ///
62    /// assert!(!SystemPath::new("foo.txt").is_absolute());
63    /// ```
64    ///
65    /// [`has_root`]: Utf8Path::has_root
66    #[inline]
67    #[must_use]
68    pub fn is_absolute(&self) -> bool {
69        self.0.is_absolute()
70    }
71
72    /// Extracts the file extension, if possible.
73    ///
74    /// The extension is:
75    ///
76    /// * [`None`], if there is no file name;
77    /// * [`None`], if there is no embedded `.`;
78    /// * [`None`], if the file name begins with `.` and has no other `.`s within;
79    /// * Otherwise, the portion of the file name after the final `.`
80    ///
81    /// # Examples
82    ///
83    /// ```
84    /// use ruff_db::system::SystemPath;
85    ///
86    /// assert_eq!("rs", SystemPath::new("foo.rs").extension().unwrap());
87    /// assert_eq!("gz", SystemPath::new("foo.tar.gz").extension().unwrap());
88    /// ```
89    ///
90    /// See [`Path::extension`] for more details.
91    #[inline]
92    #[must_use]
93    pub fn extension(&self) -> Option<&str> {
94        self.0.extension()
95    }
96
97    /// Determines whether `base` is a prefix of `self`.
98    ///
99    /// Only considers whole path components to match.
100    ///
101    /// # Examples
102    ///
103    /// ```
104    /// use ruff_db::system::SystemPath;
105    ///
106    /// let path = SystemPath::new("/etc/passwd");
107    ///
108    /// assert!(path.starts_with("/etc"));
109    /// assert!(path.starts_with("/etc/"));
110    /// assert!(path.starts_with("/etc/passwd"));
111    /// assert!(path.starts_with("/etc/passwd/")); // extra slash is okay
112    /// assert!(path.starts_with("/etc/passwd///")); // multiple extra slashes are okay
113    ///
114    /// assert!(!path.starts_with("/e"));
115    /// assert!(!path.starts_with("/etc/passwd.txt"));
116    ///
117    /// assert!(!SystemPath::new("/etc/foo.rs").starts_with("/etc/foo"));
118    /// ```
119    #[inline]
120    #[must_use]
121    pub fn starts_with(&self, base: impl AsRef<SystemPath>) -> bool {
122        self.0.starts_with(base.as_ref())
123    }
124
125    /// Determines whether `child` is a suffix of `self`.
126    ///
127    /// Only considers whole path components to match.
128    ///
129    /// # Examples
130    ///
131    /// ```
132    /// use ruff_db::system::SystemPath;
133    ///
134    /// let path = SystemPath::new("/etc/resolv.conf");
135    ///
136    /// assert!(path.ends_with("resolv.conf"));
137    /// assert!(path.ends_with("etc/resolv.conf"));
138    /// assert!(path.ends_with("/etc/resolv.conf"));
139    ///
140    /// assert!(!path.ends_with("/resolv.conf"));
141    /// assert!(!path.ends_with("conf")); // use .extension() instead
142    /// ```
143    #[inline]
144    #[must_use]
145    pub fn ends_with(&self, child: impl AsRef<SystemPath>) -> bool {
146        self.0.ends_with(child.as_ref())
147    }
148
149    /// Returns the `FileSystemPath` without its final component, if there is one.
150    ///
151    /// Returns [`None`] if the path terminates in a root or prefix.
152    ///
153    /// # Examples
154    ///
155    /// ```
156    /// use ruff_db::system::SystemPath;
157    ///
158    /// let path = SystemPath::new("/foo/bar");
159    /// let parent = path.parent().unwrap();
160    /// assert_eq!(parent, SystemPath::new("/foo"));
161    ///
162    /// let grand_parent = parent.parent().unwrap();
163    /// assert_eq!(grand_parent, SystemPath::new("/"));
164    /// assert_eq!(grand_parent.parent(), None);
165    /// ```
166    #[inline]
167    #[must_use]
168    pub fn parent(&self) -> Option<&SystemPath> {
169        self.0.parent().map(SystemPath::new)
170    }
171
172    /// Produces an iterator over `SystemPath` and its ancestors.
173    ///
174    /// The iterator will yield the `SystemPath` that is returned if the [`parent`] method is used zero
175    /// or more times. That means, the iterator will yield `&self`, `&self.parent().unwrap()`,
176    /// `&self.parent().unwrap().parent().unwrap()` and so on. If the [`parent`] method returns
177    /// [`None`], the iterator will do likewise. The iterator will always yield at least one value,
178    /// namely `&self`.
179    ///
180    /// # Examples
181    ///
182    /// ```
183    /// use ruff_db::system::SystemPath;
184    ///
185    /// let mut ancestors = SystemPath::new("/foo/bar").ancestors();
186    /// assert_eq!(ancestors.next(), Some(SystemPath::new("/foo/bar")));
187    /// assert_eq!(ancestors.next(), Some(SystemPath::new("/foo")));
188    /// assert_eq!(ancestors.next(), Some(SystemPath::new("/")));
189    /// assert_eq!(ancestors.next(), None);
190    ///
191    /// let mut ancestors = SystemPath::new("../foo/bar").ancestors();
192    /// assert_eq!(ancestors.next(), Some(SystemPath::new("../foo/bar")));
193    /// assert_eq!(ancestors.next(), Some(SystemPath::new("../foo")));
194    /// assert_eq!(ancestors.next(), Some(SystemPath::new("..")));
195    /// assert_eq!(ancestors.next(), Some(SystemPath::new("")));
196    /// assert_eq!(ancestors.next(), None);
197    /// ```
198    ///
199    /// [`parent`]: SystemPath::parent
200    #[inline]
201    pub fn ancestors(&self) -> impl Iterator<Item = &SystemPath> {
202        self.0.ancestors().map(SystemPath::new)
203    }
204
205    /// Produces an iterator over the [`camino::Utf8Component`]s of the path.
206    ///
207    /// When parsing the path, there is a small amount of normalization:
208    ///
209    /// * Repeated separators are ignored, so `a/b` and `a//b` both have
210    ///   `a` and `b` as components.
211    ///
212    /// * Occurrences of `.` are normalized away, except if they are at the
213    ///   beginning of the path. For example, `a/./b`, `a/b/`, `a/b/.` and
214    ///   `a/b` all have `a` and `b` as components, but `./a/b` starts with
215    ///   an additional [`CurDir`] component.
216    ///
217    /// * A trailing slash is normalized away, `/a/b` and `/a/b/` are equivalent.
218    ///
219    /// Note that no other normalization takes place; in particular, `a/c`
220    /// and `a/b/../c` are distinct, to account for the possibility that `b`
221    /// is a symbolic link (so its parent isn't `a`).
222    ///
223    /// # Examples
224    ///
225    /// ```
226    /// use camino::{Utf8Component};
227    /// use ruff_db::system::SystemPath;
228    ///
229    /// let mut components = SystemPath::new("/tmp/foo.txt").components();
230    ///
231    /// assert_eq!(components.next(), Some(Utf8Component::RootDir));
232    /// assert_eq!(components.next(), Some(Utf8Component::Normal("tmp")));
233    /// assert_eq!(components.next(), Some(Utf8Component::Normal("foo.txt")));
234    /// assert_eq!(components.next(), None)
235    /// ```
236    ///
237    /// [`CurDir`]: camino::Utf8Component::CurDir
238    #[inline]
239    pub fn components(&self) -> camino::Utf8Components<'_> {
240        self.0.components()
241    }
242
243    /// Returns the final component of the `FileSystemPath`, if there is one.
244    ///
245    /// If the path is a normal file, this is the file name. If it's the path of a directory, this
246    /// is the directory name.
247    ///
248    /// Returns [`None`] if the path terminates in `..`.
249    ///
250    /// # Examples
251    ///
252    /// ```
253    /// use camino::Utf8Path;
254    /// use ruff_db::system::SystemPath;
255    ///
256    /// assert_eq!(Some("bin"), SystemPath::new("/usr/bin/").file_name());
257    /// assert_eq!(Some("foo.txt"), SystemPath::new("tmp/foo.txt").file_name());
258    /// assert_eq!(Some("foo.txt"), SystemPath::new("foo.txt/.").file_name());
259    /// assert_eq!(Some("foo.txt"), SystemPath::new("foo.txt/.//").file_name());
260    /// assert_eq!(None, SystemPath::new("foo.txt/..").file_name());
261    /// assert_eq!(None, SystemPath::new("/").file_name());
262    /// ```
263    #[inline]
264    #[must_use]
265    pub fn file_name(&self) -> Option<&str> {
266        self.0.file_name()
267    }
268
269    /// Extracts the stem (non-extension) portion of [`self.file_name`].
270    ///
271    /// [`self.file_name`]: SystemPath::file_name
272    ///
273    /// The stem is:
274    ///
275    /// * [`None`], if there is no file name;
276    /// * The entire file name if there is no embedded `.`;
277    /// * The entire file name if the file name begins with `.` and has no other `.`s within;
278    /// * Otherwise, the portion of the file name before the final `.`
279    ///
280    /// # Examples
281    ///
282    /// ```
283    /// use ruff_db::system::SystemPath;
284    ///
285    /// assert_eq!("foo", SystemPath::new("foo.rs").file_stem().unwrap());
286    /// assert_eq!("foo.tar", SystemPath::new("foo.tar.gz").file_stem().unwrap());
287    /// ```
288    #[inline]
289    #[must_use]
290    pub fn file_stem(&self) -> Option<&str> {
291        self.0.file_stem()
292    }
293
294    /// Returns a path that, when joined onto `base`, yields `self`.
295    ///
296    /// # Errors
297    ///
298    /// If `base` is not a prefix of `self` (i.e., [`starts_with`]
299    /// returns `false`), returns [`Err`].
300    ///
301    /// [`starts_with`]: SystemPath::starts_with
302    ///
303    /// # Examples
304    ///
305    /// ```
306    /// use ruff_db::system::{SystemPath, SystemPathBuf};
307    ///
308    /// let path = SystemPath::new("/test/haha/foo.txt");
309    ///
310    /// assert_eq!(path.strip_prefix("/"), Ok(SystemPath::new("test/haha/foo.txt")));
311    /// assert_eq!(path.strip_prefix("/test"), Ok(SystemPath::new("haha/foo.txt")));
312    /// assert_eq!(path.strip_prefix("/test/"), Ok(SystemPath::new("haha/foo.txt")));
313    /// assert_eq!(path.strip_prefix("/test/haha/foo.txt"), Ok(SystemPath::new("")));
314    /// assert_eq!(path.strip_prefix("/test/haha/foo.txt/"), Ok(SystemPath::new("")));
315    ///
316    /// assert!(path.strip_prefix("test").is_err());
317    /// assert!(path.strip_prefix("/haha").is_err());
318    ///
319    /// let prefix = SystemPathBuf::from("/test/");
320    /// assert_eq!(path.strip_prefix(prefix), Ok(SystemPath::new("haha/foo.txt")));
321    /// ```
322    #[inline]
323    pub fn strip_prefix(
324        &self,
325        base: impl AsRef<SystemPath>,
326    ) -> std::result::Result<&SystemPath, StripPrefixError> {
327        self.0.strip_prefix(base.as_ref()).map(SystemPath::new)
328    }
329
330    /// Creates an owned [`SystemPathBuf`] with `path` adjoined to `self`.
331    ///
332    /// See [`std::path::PathBuf::push`] for more details on what it means to adjoin a path.
333    ///
334    /// # Examples
335    ///
336    /// ```
337    /// use ruff_db::system::{SystemPath, SystemPathBuf};
338    ///
339    /// assert_eq!(SystemPath::new("/etc").join("passwd"), SystemPathBuf::from("/etc/passwd"));
340    /// ```
341    #[inline]
342    #[must_use]
343    pub fn join(&self, path: impl AsRef<SystemPath>) -> SystemPathBuf {
344        SystemPathBuf::from_utf8_path_buf(self.0.join(&path.as_ref().0))
345    }
346
347    /// Creates an owned [`SystemPathBuf`] like `self` but with the given extension.
348    ///
349    /// See [`std::path::PathBuf::set_extension`] for more details.
350    ///
351    /// # Examples
352    ///
353    /// ```
354    /// use ruff_db::system::{SystemPath, SystemPathBuf};
355    ///
356    /// let path = SystemPath::new("foo.rs");
357    /// assert_eq!(path.with_extension("txt"), SystemPathBuf::from("foo.txt"));
358    ///
359    /// let path = SystemPath::new("foo.tar.gz");
360    /// assert_eq!(path.with_extension(""), SystemPathBuf::from("foo.tar"));
361    /// assert_eq!(path.with_extension("xz"), SystemPathBuf::from("foo.tar.xz"));
362    /// assert_eq!(path.with_extension("").with_extension("txt"), SystemPathBuf::from("foo.txt"));
363    /// ```
364    #[inline]
365    pub fn with_extension(&self, extension: &str) -> SystemPathBuf {
366        SystemPathBuf::from_utf8_path_buf(self.0.with_extension(extension))
367    }
368
369    /// Converts the path to an owned [`SystemPathBuf`].
370    pub fn to_path_buf(&self) -> SystemPathBuf {
371        SystemPathBuf(self.0.to_path_buf())
372    }
373
374    /// Returns the path as a string slice.
375    #[inline]
376    pub fn as_str(&self) -> &str {
377        self.0.as_str()
378    }
379
380    /// Returns the std path for the file.
381    #[inline]
382    pub fn as_std_path(&self) -> &Path {
383        self.0.as_std_path()
384    }
385
386    /// Returns the [`Utf8Path`] for the file.
387    #[inline]
388    pub fn as_utf8_path(&self) -> &Utf8Path {
389        &self.0
390    }
391
392    pub fn from_std_path(path: &Path) -> Option<&SystemPath> {
393        Some(SystemPath::new(Utf8Path::from_path(path)?))
394    }
395
396    /// Makes a path absolute and normalizes it without accessing the file system.
397    ///
398    /// Adapted from [cargo](https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61)
399    ///
400    /// # Examples
401    ///
402    /// ## Posix paths
403    ///
404    /// ```
405    /// # #[cfg(unix)]
406    /// # fn main() {
407    ///   use ruff_db::system::{SystemPath, SystemPathBuf};
408    ///
409    ///   // Relative to absolute
410    ///   let absolute = SystemPath::absolute("foo/./bar", "/tmp");
411    ///   assert_eq!(absolute, SystemPathBuf::from("/tmp/foo/bar"));
412    ///
413    ///   // Path's going past the root are normalized to the root
414    ///   let absolute = SystemPath::absolute("../../../", "/tmp");
415    ///   assert_eq!(absolute, SystemPathBuf::from("/"));
416    ///
417    ///   // Absolute to absolute
418    ///   let absolute = SystemPath::absolute("/foo//test/.././bar.rs", "/tmp");
419    ///   assert_eq!(absolute, SystemPathBuf::from("/foo/bar.rs"));
420    /// # }
421    /// # #[cfg(not(unix))]
422    /// # fn main() {}
423    /// ```
424    ///
425    /// ## Windows paths
426    ///
427    /// ```
428    /// # #[cfg(windows)]
429    /// # fn main() {
430    ///   use ruff_db::system::{SystemPath, SystemPathBuf};
431    ///
432    ///   // Relative to absolute
433    ///   let absolute = SystemPath::absolute(r"foo\.\bar", r"C:\tmp");
434    ///   assert_eq!(absolute, SystemPathBuf::from(r"C:\tmp\foo\bar"));
435    ///
436    ///   // Path's going past the root are normalized to the root
437    ///   let absolute = SystemPath::absolute(r"..\..\..\", r"C:\tmp");
438    ///   assert_eq!(absolute, SystemPathBuf::from(r"C:\"));
439    ///
440    ///   // Absolute to absolute
441    ///   let absolute = SystemPath::absolute(r"C:\foo//test\..\./bar.rs", r"C:\tmp");
442    ///   assert_eq!(absolute, SystemPathBuf::from(r"C:\foo\bar.rs"));
443    /// # }
444    /// # #[cfg(not(windows))]
445    /// # fn main() {}
446    /// ```
447    pub fn absolute(path: impl AsRef<SystemPath>, cwd: impl AsRef<SystemPath>) -> SystemPathBuf {
448        fn absolute(path: &SystemPath, cwd: &SystemPath) -> SystemPathBuf {
449            let path = &path.0;
450
451            let mut components = path.components().peekable();
452            let mut ret = if let Some(
453                c @ (camino::Utf8Component::Prefix(..) | camino::Utf8Component::RootDir),
454            ) = components.peek().cloned()
455            {
456                components.next();
457                Utf8PathBuf::from(c.as_str())
458            } else {
459                cwd.0.to_path_buf()
460            };
461
462            for component in components {
463                match component {
464                    camino::Utf8Component::Prefix(..) => unreachable!(),
465                    camino::Utf8Component::RootDir => {
466                        ret.push(component);
467                    }
468                    camino::Utf8Component::CurDir => {}
469                    camino::Utf8Component::ParentDir => {
470                        ret.pop();
471                    }
472                    camino::Utf8Component::Normal(c) => {
473                        ret.push(c);
474                    }
475                }
476            }
477
478            SystemPathBuf::from_utf8_path_buf(ret)
479        }
480
481        absolute(path.as_ref(), cwd.as_ref())
482    }
483}
484
485impl ToOwned for SystemPath {
486    type Owned = SystemPathBuf;
487
488    fn to_owned(&self) -> Self::Owned {
489        self.to_path_buf()
490    }
491}
492
493/// An owned, mutable path on [`System`](`super::System`) (akin to [`String`]).
494///
495/// The path is guaranteed to be valid UTF-8.
496#[repr(transparent)]
497#[derive(Eq, PartialEq, Clone, Hash, PartialOrd, Ord)]
498#[cfg_attr(
499    feature = "serde",
500    derive(serde::Serialize, serde::Deserialize),
501    serde(transparent)
502)]
503#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
504pub struct SystemPathBuf(#[cfg_attr(feature = "schemars", schemars(with = "String"))] Utf8PathBuf);
505
506impl get_size2::GetSize for SystemPathBuf {
507    fn get_heap_size_with_tracker<T: get_size2::GetSizeTracker>(&self, tracker: T) -> (usize, T) {
508        (self.0.capacity(), tracker)
509    }
510}
511
512impl SystemPathBuf {
513    pub fn new() -> Self {
514        Self(Utf8PathBuf::new())
515    }
516
517    pub fn from_utf8_path_buf(path: Utf8PathBuf) -> Self {
518        Self(path)
519    }
520
521    pub fn from_path_buf(
522        path: std::path::PathBuf,
523    ) -> std::result::Result<Self, std::path::PathBuf> {
524        Utf8PathBuf::from_path_buf(path).map(Self)
525    }
526
527    /// Try to convert from `path` directly, falling back to the lossy string representation on
528    /// error.
529    pub fn from_path_buf_lossy(path: std::path::PathBuf) -> Self {
530        Self::from_path_buf(path)
531            .unwrap_or_else(|path| Self::from(path.to_string_lossy().to_string()))
532    }
533
534    /// Extends `self` with `path`.
535    ///
536    /// If `path` is absolute, it replaces the current path.
537    ///
538    /// On Windows:
539    ///
540    /// * if `path` has a root but no prefix (e.g., `\windows`), it
541    ///   replaces everything except for the prefix (if any) of `self`.
542    /// * if `path` has a prefix but no root, it replaces `self`.
543    ///
544    /// # Examples
545    ///
546    /// Pushing a relative path extends the existing path:
547    ///
548    /// ```
549    /// use ruff_db::system::SystemPathBuf;
550    ///
551    /// let mut path = SystemPathBuf::from("/tmp");
552    /// path.push("file.bk");
553    /// assert_eq!(path, SystemPathBuf::from("/tmp/file.bk"));
554    /// ```
555    ///
556    /// Pushing an absolute path replaces the existing path:
557    ///
558    /// ```
559    ///
560    /// use ruff_db::system::SystemPathBuf;
561    ///
562    /// let mut path = SystemPathBuf::from("/tmp");
563    /// path.push("/etc");
564    /// assert_eq!(path, SystemPathBuf::from("/etc"));
565    /// ```
566    pub fn push(&mut self, path: impl AsRef<SystemPath>) {
567        self.0.push(&path.as_ref().0);
568    }
569
570    pub fn into_utf8_path_buf(self) -> Utf8PathBuf {
571        self.0
572    }
573
574    pub fn into_std_path_buf(self) -> PathBuf {
575        self.0.into_std_path_buf()
576    }
577
578    pub fn into_string(self) -> String {
579        self.0.into_string()
580    }
581
582    #[inline]
583    pub fn as_path(&self) -> &SystemPath {
584        SystemPath::new(&self.0)
585    }
586}
587
588impl From<&SystemPath> for Box<SystemPath> {
589    fn from(path: &SystemPath) -> Self {
590        Box::from(path.to_path_buf())
591    }
592}
593
594impl From<SystemPathBuf> for Box<SystemPath> {
595    fn from(path: SystemPathBuf) -> Self {
596        let path = Box::into_raw(path.0.into_boxed_path()) as *mut SystemPath;
597        // SAFETY: SystemPath is marked as #[repr(transparent)] so the conversion from a
598        // *mut Utf8Path to a *mut SystemPath is valid.
599        unsafe { Box::from_raw(path) }
600    }
601}
602
603impl Clone for Box<SystemPath> {
604    fn clone(&self) -> Self {
605        Box::from(&**self)
606    }
607}
608
609impl get_size2::GetSize for Box<SystemPath> {
610    fn get_heap_size_with_tracker<T: get_size2::GetSizeTracker>(&self, tracker: T) -> (usize, T) {
611        (std::mem::size_of_val(&**self), tracker)
612    }
613}
614
615impl Borrow<SystemPath> for SystemPathBuf {
616    fn borrow(&self) -> &SystemPath {
617        self.as_path()
618    }
619}
620
621impl From<&str> for SystemPathBuf {
622    fn from(value: &str) -> Self {
623        SystemPathBuf::from_utf8_path_buf(Utf8PathBuf::from(value))
624    }
625}
626
627impl From<String> for SystemPathBuf {
628    fn from(value: String) -> Self {
629        SystemPathBuf::from_utf8_path_buf(Utf8PathBuf::from(value))
630    }
631}
632
633impl Default for SystemPathBuf {
634    fn default() -> Self {
635        Self::new()
636    }
637}
638
639impl AsRef<SystemPath> for SystemPathBuf {
640    #[inline]
641    fn as_ref(&self) -> &SystemPath {
642        self.as_path()
643    }
644}
645
646impl AsRef<SystemPath> for SystemPath {
647    #[inline]
648    fn as_ref(&self) -> &SystemPath {
649        self
650    }
651}
652
653impl AsRef<SystemPath> for Utf8Path {
654    #[inline]
655    fn as_ref(&self) -> &SystemPath {
656        SystemPath::new(self)
657    }
658}
659
660impl AsRef<SystemPath> for Utf8PathBuf {
661    #[inline]
662    fn as_ref(&self) -> &SystemPath {
663        SystemPath::new(self.as_path())
664    }
665}
666
667impl AsRef<SystemPath> for camino::Utf8Component<'_> {
668    #[inline]
669    fn as_ref(&self) -> &SystemPath {
670        SystemPath::new(self.as_str())
671    }
672}
673
674impl AsRef<SystemPath> for str {
675    #[inline]
676    fn as_ref(&self) -> &SystemPath {
677        SystemPath::new(self)
678    }
679}
680
681impl AsRef<SystemPath> for String {
682    #[inline]
683    fn as_ref(&self) -> &SystemPath {
684        SystemPath::new(self)
685    }
686}
687
688impl AsRef<Path> for SystemPath {
689    #[inline]
690    fn as_ref(&self) -> &Path {
691        self.0.as_std_path()
692    }
693}
694
695impl Deref for SystemPathBuf {
696    type Target = SystemPath;
697
698    #[inline]
699    fn deref(&self) -> &Self::Target {
700        self.as_path()
701    }
702}
703
704impl AsRef<Path> for SystemPathBuf {
705    #[inline]
706    fn as_ref(&self) -> &Path {
707        self.0.as_std_path()
708    }
709}
710
711impl<P: AsRef<SystemPath>> FromIterator<P> for SystemPathBuf {
712    fn from_iter<I: IntoIterator<Item = P>>(iter: I) -> Self {
713        let mut buf = SystemPathBuf::new();
714        buf.extend(iter);
715        buf
716    }
717}
718
719impl<P: AsRef<SystemPath>> Extend<P> for SystemPathBuf {
720    fn extend<I: IntoIterator<Item = P>>(&mut self, iter: I) {
721        for path in iter {
722            self.push(path);
723        }
724    }
725}
726
727impl std::fmt::Debug for SystemPath {
728    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
729        self.0.fmt(f)
730    }
731}
732
733impl std::fmt::Display for SystemPath {
734    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
735        self.0.fmt(f)
736    }
737}
738
739impl std::fmt::Debug for SystemPathBuf {
740    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
741        self.0.fmt(f)
742    }
743}
744
745impl std::fmt::Display for SystemPathBuf {
746    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
747        self.0.fmt(f)
748    }
749}
750
751#[cfg(feature = "cache")]
752impl ruff_cache::CacheKey for SystemPath {
753    fn cache_key(&self, hasher: &mut ruff_cache::CacheKeyHasher) {
754        self.0.as_str().cache_key(hasher);
755    }
756}
757
758#[cfg(feature = "cache")]
759impl ruff_cache::CacheKey for SystemPathBuf {
760    fn cache_key(&self, hasher: &mut ruff_cache::CacheKeyHasher) {
761        self.as_path().cache_key(hasher);
762    }
763}
764
765/// A slice of a virtual path on [`System`](super::System) (akin to [`str`]).
766#[repr(transparent)]
767#[derive(Eq, PartialEq, Hash, PartialOrd, Ord)]
768pub struct SystemVirtualPath(str);
769
770impl SystemVirtualPath {
771    pub const fn new(path: &str) -> &SystemVirtualPath {
772        // SAFETY: SystemVirtualPath is marked as #[repr(transparent)] so the conversion from a
773        // *const str to a *const SystemVirtualPath is valid.
774        unsafe { &*(path as *const str as *const SystemVirtualPath) }
775    }
776
777    /// Converts the path to an owned [`SystemVirtualPathBuf`].
778    pub fn to_path_buf(&self) -> SystemVirtualPathBuf {
779        SystemVirtualPathBuf(self.0.to_string())
780    }
781
782    /// Extracts the file extension, if possible.
783    ///
784    /// # Examples
785    ///
786    /// ```
787    /// use ruff_db::system::SystemVirtualPath;
788    ///
789    /// assert_eq!(None, SystemVirtualPath::new("untitled:Untitled-1").extension());
790    /// assert_eq!("ipynb", SystemVirtualPath::new("untitled:Untitled-1.ipynb").extension().unwrap());
791    /// assert_eq!("ipynb", SystemVirtualPath::new("vscode-notebook-cell:Untitled-1.ipynb").extension().unwrap());
792    /// ```
793    ///
794    /// See [`Path::extension`] for more details.
795    pub fn extension(&self) -> Option<&str> {
796        Path::new(&self.0).extension().and_then(|ext| ext.to_str())
797    }
798
799    /// Returns the path as a string slice.
800    #[inline]
801    pub fn as_str(&self) -> &str {
802        &self.0
803    }
804}
805
806/// An owned, virtual path on [`System`](`super::System`) (akin to [`String`]).
807#[derive(Eq, PartialEq, Clone, Hash, PartialOrd, Ord, get_size2::GetSize)]
808pub struct SystemVirtualPathBuf(String);
809
810impl SystemVirtualPathBuf {
811    #[inline]
812    pub const fn as_path(&self) -> &SystemVirtualPath {
813        SystemVirtualPath::new(self.0.as_str())
814    }
815}
816
817impl From<&SystemVirtualPath> for Box<SystemVirtualPath> {
818    fn from(path: &SystemVirtualPath) -> Self {
819        Box::from(path.to_path_buf())
820    }
821}
822
823impl From<SystemVirtualPathBuf> for Box<SystemVirtualPath> {
824    fn from(path: SystemVirtualPathBuf) -> Self {
825        let path = Box::into_raw(path.0.into_boxed_str()) as *mut SystemVirtualPath;
826        // SAFETY: SystemVirtualPath is marked as #[repr(transparent)] so the conversion from a
827        // *mut str to a *mut SystemVirtualPath is valid.
828        unsafe { Box::from_raw(path) }
829    }
830}
831
832impl Clone for Box<SystemVirtualPath> {
833    fn clone(&self) -> Self {
834        Box::from(&**self)
835    }
836}
837
838impl get_size2::GetSize for Box<SystemVirtualPath> {
839    fn get_heap_size_with_tracker<T: get_size2::GetSizeTracker>(&self, tracker: T) -> (usize, T) {
840        (std::mem::size_of_val(&**self), tracker)
841    }
842}
843
844impl From<String> for SystemVirtualPathBuf {
845    fn from(value: String) -> Self {
846        SystemVirtualPathBuf(value)
847    }
848}
849
850impl AsRef<SystemVirtualPath> for SystemVirtualPathBuf {
851    #[inline]
852    fn as_ref(&self) -> &SystemVirtualPath {
853        self.as_path()
854    }
855}
856
857impl AsRef<SystemVirtualPath> for SystemVirtualPath {
858    #[inline]
859    fn as_ref(&self) -> &SystemVirtualPath {
860        self
861    }
862}
863
864impl AsRef<SystemVirtualPath> for str {
865    #[inline]
866    fn as_ref(&self) -> &SystemVirtualPath {
867        SystemVirtualPath::new(self)
868    }
869}
870
871impl AsRef<SystemVirtualPath> for String {
872    #[inline]
873    fn as_ref(&self) -> &SystemVirtualPath {
874        SystemVirtualPath::new(self)
875    }
876}
877
878impl Deref for SystemVirtualPathBuf {
879    type Target = SystemVirtualPath;
880
881    fn deref(&self) -> &Self::Target {
882        self.as_path()
883    }
884}
885
886impl std::fmt::Debug for SystemVirtualPath {
887    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
888        self.0.fmt(f)
889    }
890}
891
892impl std::fmt::Display for SystemVirtualPath {
893    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
894        self.0.fmt(f)
895    }
896}
897
898impl std::fmt::Debug for SystemVirtualPathBuf {
899    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
900        self.0.fmt(f)
901    }
902}
903
904impl std::fmt::Display for SystemVirtualPathBuf {
905    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
906        self.0.fmt(f)
907    }
908}
909
910#[cfg(feature = "cache")]
911impl ruff_cache::CacheKey for SystemVirtualPath {
912    fn cache_key(&self, hasher: &mut ruff_cache::CacheKeyHasher) {
913        self.as_str().cache_key(hasher);
914    }
915}
916
917#[cfg(feature = "cache")]
918impl ruff_cache::CacheKey for SystemVirtualPathBuf {
919    fn cache_key(&self, hasher: &mut ruff_cache::CacheKeyHasher) {
920        self.as_path().cache_key(hasher);
921    }
922}
923
924impl Borrow<SystemVirtualPath> for SystemVirtualPathBuf {
925    fn borrow(&self) -> &SystemVirtualPath {
926        self.as_path()
927    }
928}
929
930/// Deduplicates identical paths and removes nested paths.
931///
932/// # Examples
933/// ```rust
934/// use ruff_db::system::{SystemPath, deduplicate_nested_paths};///
935///
936/// let paths = vec![SystemPath::new("/a/b/c"), SystemPath::new("/a/b"), SystemPath::new("/a/beta"), SystemPath::new("/a/b/c")];
937/// assert_eq!(deduplicate_nested_paths(paths).collect::<Vec<_>>(), &[SystemPath::new("/a/b"), SystemPath::new("/a/beta")]);
938/// ```
939pub fn deduplicate_nested_paths<P, I>(paths: I) -> DeduplicatedNestedPathsIter<P>
940where
941    I: IntoIterator<Item = P>,
942    P: AsRef<SystemPath>,
943{
944    DeduplicatedNestedPathsIter::new(paths)
945}
946
947pub struct DeduplicatedNestedPathsIter<P> {
948    inner: std::vec::IntoIter<P>,
949    next: Option<P>,
950}
951
952impl<P> DeduplicatedNestedPathsIter<P>
953where
954    P: AsRef<SystemPath>,
955{
956    fn new<I>(paths: I) -> Self
957    where
958        I: IntoIterator<Item = P>,
959    {
960        let mut paths = paths.into_iter().collect::<Vec<_>>();
961        // Sort the path to ensure that e.g. `/a/b/c`, comes right after `/a/b`.
962        paths.sort_unstable_by(|left, right| left.as_ref().cmp(right.as_ref()));
963
964        let mut iter = paths.into_iter();
965
966        Self {
967            next: iter.next(),
968            inner: iter,
969        }
970    }
971}
972
973impl<P> Iterator for DeduplicatedNestedPathsIter<P>
974where
975    P: AsRef<SystemPath>,
976{
977    type Item = P;
978
979    fn next(&mut self) -> Option<Self::Item> {
980        let current = self.next.take()?;
981
982        for next in self.inner.by_ref() {
983            // Skip all paths that have the same prefix as the current path
984            if !next.as_ref().starts_with(current.as_ref()) {
985                self.next = Some(next);
986                break;
987            }
988        }
989
990        Some(current)
991    }
992}