conch_runtime_pshaw/
path.rs

1//! Defines helpers and utilities for working with file system paths
2
3use std::fmt;
4use std::io;
5use std::mem;
6use std::ops::Deref;
7use std::path::{Component, Path, PathBuf};
8
9/// An error that can arise during physical path normalization.
10#[derive(Debug, thiserror::Error)]
11pub struct NormalizationError {
12    /// The error that occured.
13    #[source]
14    err: io::Error,
15    /// The path that caused the error.
16    path: PathBuf,
17}
18
19impl fmt::Display for NormalizationError {
20    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
21        write!(fmt, "{}: {}", self.err, self.path.display())
22    }
23}
24
25/// A `PathBuf` wrapper which ensures paths do not have `.` or `..` components.
26#[derive(PartialEq, Eq, Clone, Debug, Default)]
27pub struct NormalizedPath {
28    /// Inner path buffer which *always* remains normalized.
29    normalized_path: PathBuf,
30}
31
32pub(crate) fn has_dot_components(path: &Path) -> bool {
33    path.components().any(|c| match c {
34        Component::CurDir | Component::ParentDir => true,
35
36        Component::Prefix(_) | Component::RootDir | Component::Normal(_) => false,
37    })
38}
39
40impl NormalizedPath {
41    /// Creates a new, empty `NormalizedPath`.
42    pub fn new() -> Self {
43        Self {
44            normalized_path: PathBuf::new(),
45        }
46    }
47
48    /// Creates a new `NormalizedPath` instance with the provided buffer.
49    ///
50    /// If `buf` is non-empty, it will be logically normalized as needed.
51    /// See the documentation for `join_normalized_logial` for more
52    /// information on how the normalization is performed.
53    pub fn new_normalized_logical(buf: PathBuf) -> Self {
54        if has_dot_components(&buf) {
55            let mut normalized = Self::new();
56            normalized.perform_join_normalized_logical(&buf);
57            normalized
58        } else {
59            Self {
60                normalized_path: buf,
61            }
62        }
63    }
64
65    /// Creates a new `NormalizedPath` instance with the provided buffer.
66    ///
67    /// If `buf` is non-empty, it will be physically normalized as needed.
68    /// See the documentation for `join_normalized_physical` for more
69    /// information on how the normalization is performed.
70    pub fn new_normalized_physical(buf: PathBuf) -> Result<Self, NormalizationError> {
71        if has_dot_components(&buf) {
72            let mut normalized_path = Self::new();
73            normalized_path.perform_join_normalized_physical_for_dot_components(&buf)?;
74            Ok(normalized_path)
75        } else {
76            // Ensure we've resolved all possible symlinks
77            let normalized_path = buf
78                .canonicalize()
79                .map_err(|e| NormalizationError { err: e, path: buf })?;
80
81            Ok(Self { normalized_path })
82        }
83    }
84
85    /// Joins a path to the buffer, normalizing away any `.` or `..` components,
86    /// without following any symbolic links.
87    ///
88    /// For example, joining `../some/path` to `/root/dir` will yield
89    /// `/root/some/path`.
90    ///
91    /// The normal behaviors of joining `Path`s will take effect (e.g. joining
92    /// with an absolute path will replace the previous contents).
93    pub fn join_normalized_logial<P: AsRef<Path>>(&mut self, path: P) {
94        self.join_normalized_logial_(path.as_ref())
95    }
96
97    fn join_normalized_logial_(&mut self, path: &Path) {
98        // If we have no relative components to resolve then we can avoid
99        // multiple reallocations by pushing the entiere path at once.
100        if !has_dot_components(path) {
101            self.normalized_path.push(path);
102            return;
103        }
104
105        self.perform_join_normalized_logical(path);
106    }
107
108    fn perform_join_normalized_logical(&mut self, path: &Path) {
109        for component in path.components() {
110            match component {
111                c @ Component::Prefix(_) | c @ Component::RootDir | c @ Component::Normal(_) => {
112                    self.normalized_path.push(c.as_os_str())
113                }
114
115                Component::CurDir => {}
116                Component::ParentDir => {
117                    self.normalized_path.pop();
118                }
119            }
120        }
121    }
122
123    /// Joins a path to the buffer, normalizing away any `.` or `..` components
124    /// after following any symbolic links.
125    ///
126    /// For example, joining `../some/path` to `/root/dir` (where `/root/dir`
127    /// is a symlink to `/root/another/place`) will yield `/root/another/some/path`.
128    ///
129    /// The normal behaviors of joining `Path`s will take effect (e.g. joining
130    /// with an absolute path will replace the previous contents).
131    ///
132    /// # Errors
133    ///
134    /// If an error occurs while resolving symlinks, the current path buffer
135    /// will be reset to its previous state (as if the call never happened)
136    /// before the error is propagated to the caller.
137    pub fn join_normalized_physical<P: AsRef<Path>>(
138        &mut self,
139        path: P,
140    ) -> Result<(), NormalizationError> {
141        self.join_normalized_physical_(path.as_ref())
142    }
143
144    fn join_normalized_physical_(&mut self, path: &Path) -> Result<(), NormalizationError> {
145        if has_dot_components(path) {
146            self.perform_join_normalized_physical_for_dot_components(path)
147        } else {
148            // If we have no relative components to resolve then we can avoid
149            // multiple reallocations by pushing the entiere path at once.
150            self.normalized_path.push(path);
151            self.normalized_path =
152                self.normalized_path
153                    .canonicalize()
154                    .map_err(|e| NormalizationError {
155                        err: e,
156                        path: self.normalized_path.clone(),
157                    })?;
158
159            Ok(())
160        }
161    }
162
163    fn perform_join_normalized_physical_for_dot_components(
164        &mut self,
165        path: &Path,
166    ) -> Result<(), NormalizationError> {
167        let orig_path = self.normalized_path.clone();
168        self.perform_join_normalized_physical(path)
169            .map_err(|e| NormalizationError {
170                err: e,
171                path: mem::replace(&mut self.normalized_path, orig_path),
172            })
173    }
174
175    fn perform_join_normalized_physical(&mut self, path: &Path) -> io::Result<()> {
176        for component in path.components() {
177            match component {
178                c @ Component::Prefix(_) | c @ Component::RootDir | c @ Component::Normal(_) => {
179                    self.normalized_path.push(c.as_os_str())
180                }
181
182                Component::CurDir => {}
183                Component::ParentDir => {
184                    self.normalized_path = self.normalized_path.canonicalize()?;
185                    self.normalized_path.pop();
186                }
187            }
188        }
189
190        // Perform one last resolution of all potential symlinks
191        self.normalized_path = self.normalized_path.canonicalize()?;
192        Ok(())
193    }
194
195    /// Unwraps the inner `PathBuf`.
196    pub fn into_inner(self) -> PathBuf {
197        self.normalized_path
198    }
199}
200
201impl Deref for NormalizedPath {
202    type Target = PathBuf;
203
204    fn deref(&self) -> &PathBuf {
205        &self.normalized_path
206    }
207}