gix_fs/
stack.rs

1use crate::Stack;
2use bstr::{BStr, BString, ByteSlice};
3use std::ffi::OsStr;
4use std::path::{Component, Path, PathBuf};
5
6///
7pub mod to_normal_path_components {
8    use std::path::PathBuf;
9
10    /// The error used in [`ToNormalPathComponents::to_normal_path_components()`](super::ToNormalPathComponents::to_normal_path_components()).
11    #[derive(Debug, thiserror::Error)]
12    #[allow(missing_docs)]
13    pub enum Error {
14        #[error("Input path \"{path}\" contains relative or absolute components", path = .0.display())]
15        NotANormalComponent(PathBuf),
16        #[error("Could not convert to UTF8 or from UTF8 due to ill-formed input")]
17        IllegalUtf8,
18    }
19}
20
21/// Obtain an iterator over `OsStr`-components which are normal, none-relative and not absolute.
22pub trait ToNormalPathComponents {
23    /// Return an iterator over the normal components of a path, without the separator.
24    fn to_normal_path_components(&self) -> impl Iterator<Item = Result<&OsStr, to_normal_path_components::Error>>;
25}
26
27impl ToNormalPathComponents for &Path {
28    fn to_normal_path_components(&self) -> impl Iterator<Item = Result<&OsStr, to_normal_path_components::Error>> {
29        self.components().map(|c| component_to_os_str(c, self))
30    }
31}
32
33impl ToNormalPathComponents for PathBuf {
34    fn to_normal_path_components(&self) -> impl Iterator<Item = Result<&OsStr, to_normal_path_components::Error>> {
35        self.components().map(|c| component_to_os_str(c, self))
36    }
37}
38
39fn component_to_os_str<'a>(
40    component: Component<'a>,
41    path_with_component: &Path,
42) -> Result<&'a OsStr, to_normal_path_components::Error> {
43    match component {
44        Component::Normal(os_str) => Ok(os_str),
45        _ => Err(to_normal_path_components::Error::NotANormalComponent(
46            path_with_component.to_owned(),
47        )),
48    }
49}
50
51impl ToNormalPathComponents for &BStr {
52    fn to_normal_path_components(&self) -> impl Iterator<Item = Result<&OsStr, to_normal_path_components::Error>> {
53        self.split(|b| *b == b'/').filter_map(bytes_component_to_os_str)
54    }
55}
56
57impl ToNormalPathComponents for &str {
58    fn to_normal_path_components(&self) -> impl Iterator<Item = Result<&OsStr, to_normal_path_components::Error>> {
59        self.split('/').filter_map(|c| bytes_component_to_os_str(c.as_bytes()))
60    }
61}
62
63impl ToNormalPathComponents for &BString {
64    fn to_normal_path_components(&self) -> impl Iterator<Item = Result<&OsStr, to_normal_path_components::Error>> {
65        self.split(|b| *b == b'/').filter_map(bytes_component_to_os_str)
66    }
67}
68
69fn bytes_component_to_os_str(component: &[u8]) -> Option<Result<&OsStr, to_normal_path_components::Error>> {
70    if component.is_empty() {
71        return None;
72    }
73    gix_path::try_from_byte_slice(component.as_bstr())
74        .map_err(|_| to_normal_path_components::Error::IllegalUtf8)
75        .map(Path::as_os_str)
76        .into()
77}
78
79/// Access
80impl Stack {
81    /// Returns the top-level path of the stack.
82    pub fn root(&self) -> &Path {
83        &self.root
84    }
85
86    /// Returns the absolute path the currently set path.
87    pub fn current(&self) -> &Path {
88        &self.current
89    }
90
91    /// Returns the currently set path relative to the [`root()`][Stack::root()].
92    pub fn current_relative(&self) -> &Path {
93        &self.current_relative
94    }
95}
96
97/// A delegate for use in a [`Stack`].
98pub trait Delegate {
99    /// Called whenever we push a directory on top of the stack, and after the respective call to [`push()`](Self::push).
100    ///
101    /// It is only called if the currently acted on path is a directory in itself, which is determined by knowing
102    /// that it's not the last component of the path.
103    /// Use [`Stack::current()`] to see the directory.
104    fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()>;
105
106    /// Called after any component was pushed, with the path available at [`Stack::current()`].
107    ///
108    /// `is_last_component` is `true` if the path is completely built, which typically means it's not a directory.
109    fn push(&mut self, is_last_component: bool, stack: &Stack) -> std::io::Result<()>;
110
111    /// Called right after a directory-component was popped off the stack.
112    ///
113    /// Use it to pop information off internal data structures. Note that no equivalent call exists for popping
114    /// the file-component.
115    fn pop_directory(&mut self);
116}
117
118impl Stack {
119    /// Create a new instance with `root` being the base for all future paths we handle, assuming it to be valid which includes
120    /// symbolic links to be included in it as well.
121    pub fn new(root: PathBuf) -> Self {
122        Stack {
123            current: root.clone(),
124            current_relative: PathBuf::with_capacity(128),
125            valid_components: 0,
126            root,
127            current_is_directory: true,
128        }
129    }
130
131    /// Set the current stack to point to the `relative` path and call `push_comp()` each time a new path component is popped
132    /// along with the stacks state for inspection to perform an operation that produces some data.
133    ///
134    /// The full path to `relative` will be returned along with the data returned by `push_comp`.
135    /// Note that this only works correctly for the delegate's `push_directory()` and `pop_directory()` methods if
136    /// `relative` paths are terminal, so point to their designated file or directory.
137    /// The path is also expected to be normalized, and should not contain extra separators, and must not contain `..`
138    /// or have leading or trailing slashes (or additionally backslashes on Windows).
139    pub fn make_relative_path_current(
140        &mut self,
141        relative: impl ToNormalPathComponents,
142        delegate: &mut dyn Delegate,
143    ) -> std::io::Result<()> {
144        let mut components = relative.to_normal_path_components().peekable();
145        if self.valid_components != 0 && components.peek().is_none() {
146            return Err(std::io::Error::new(
147                std::io::ErrorKind::Other,
148                "empty inputs are not allowed",
149            ));
150        }
151        if self.valid_components == 0 {
152            delegate.push_directory(self)?;
153        }
154
155        let mut existing_components = self.current_relative.components();
156        let mut matching_components = 0;
157        while let (Some(existing_comp), Some(new_comp)) = (existing_components.next(), components.peek()) {
158            match new_comp {
159                Ok(new_comp) => {
160                    if existing_comp.as_os_str() == *new_comp {
161                        components.next();
162                        matching_components += 1;
163                    } else {
164                        break;
165                    }
166                }
167                Err(err) => return Err(std::io::Error::other(format!("{err}"))),
168            }
169        }
170
171        for _ in 0..self.valid_components - matching_components {
172            self.current.pop();
173            self.current_relative.pop();
174            if self.current_is_directory {
175                delegate.pop_directory();
176            }
177            self.current_is_directory = true;
178        }
179        self.valid_components = matching_components;
180
181        if !self.current_is_directory && components.peek().is_some() {
182            delegate.push_directory(self)?;
183        }
184
185        while let Some(comp) = components.next() {
186            let comp = comp.map_err(std::io::Error::other)?;
187            let is_last_component = components.peek().is_none();
188            self.current_is_directory = !is_last_component;
189            self.current.push(comp);
190            self.current_relative.push(comp);
191            self.valid_components += 1;
192            let res = delegate.push(is_last_component, self);
193            if self.current_is_directory {
194                delegate.push_directory(self)?;
195            }
196
197            if let Err(err) = res {
198                self.current.pop();
199                self.current_relative.pop();
200                self.valid_components -= 1;
201                return Err(err);
202            }
203        }
204        Ok(())
205    }
206}