Skip to main content

gix_fs/
stack.rs

1use std::{
2    ffi::OsStr,
3    path::{Component, Path, PathBuf},
4};
5
6use bstr::{BStr, BString, ByteSlice};
7use gix_error::{ErrorExt, ExnMessageResult, validation};
8
9use crate::Stack;
10
11/// Obtain an iterator over `OsStr`-components which are normal, none-relative and not absolute.
12pub trait ToNormalPathComponents {
13    /// Return an iterator over the normal components of a path, without the separator.
14    fn to_normal_path_components(&self) -> impl Iterator<Item = ExnMessageResult<&OsStr>>;
15}
16
17impl ToNormalPathComponents for &Path {
18    fn to_normal_path_components(&self) -> impl Iterator<Item = ExnMessageResult<&OsStr>> {
19        self.components().map(|c| component_to_os_str(c, self.display()))
20    }
21}
22
23impl ToNormalPathComponents for PathBuf {
24    fn to_normal_path_components(&self) -> impl Iterator<Item = ExnMessageResult<&OsStr>> {
25        self.components().map(|c| component_to_os_str(c, self.display()))
26    }
27}
28
29fn component_to_os_str(
30    component: Component<'_>,
31    path_with_component: impl std::fmt::Display,
32) -> ExnMessageResult<&OsStr> {
33    match component {
34        Component::Normal(os_str) => Ok(os_str),
35        _ => Err(validation(format!(
36            "Input path \"{path_with_component}\" contains relative or absolute components"
37        ))
38        .raise()),
39    }
40}
41
42impl ToNormalPathComponents for &BStr {
43    fn to_normal_path_components(&self) -> impl Iterator<Item = ExnMessageResult<&OsStr>> {
44        self.split(|b| *b == b'/')
45            .filter_map(|c| bytes_component_to_os_str(c, self))
46    }
47}
48
49impl ToNormalPathComponents for &str {
50    fn to_normal_path_components(&self) -> impl Iterator<Item = ExnMessageResult<&OsStr>> {
51        self.split('/')
52            .filter_map(|c| bytes_component_to_os_str(c.as_bytes(), (*self).into()))
53    }
54}
55
56impl ToNormalPathComponents for &BString {
57    fn to_normal_path_components(&self) -> impl Iterator<Item = ExnMessageResult<&OsStr>> {
58        self.split(|b| *b == b'/')
59            .filter_map(|c| bytes_component_to_os_str(c, self.as_bstr()))
60    }
61}
62
63fn bytes_component_to_os_str<'a>(component: &'a [u8], path: &BStr) -> Option<ExnMessageResult<&'a OsStr>> {
64    if component.is_empty() {
65        return None;
66    }
67    let component = match gix_path::try_from_byte_slice(component.as_bstr()) {
68        Ok(c) => c,
69        Err(err) => return Some(Err(err)),
70    };
71    let component = component.components().next()?;
72    Some(component_to_os_str(component, path))
73}
74
75/// Access
76impl Stack {
77    /// Returns the top-level path of the stack.
78    pub fn root(&self) -> &Path {
79        &self.root
80    }
81
82    /// Returns the absolute path the currently set path.
83    pub fn current(&self) -> &Path {
84        &self.current
85    }
86
87    /// Returns the currently set path relative to the [`root()`][Stack::root()].
88    pub fn current_relative(&self) -> &Path {
89        &self.current_relative
90    }
91}
92
93/// A delegate for use in a [`Stack`].
94pub trait Delegate {
95    /// Called whenever we push a directory on top of the stack, and after the respective call to [`push()`](Self::push).
96    ///
97    /// It is only called if the currently acted on path is a directory in itself, which is determined by knowing
98    /// that it's not the last component of the path.
99    /// Use [`Stack::current()`] to see the directory.
100    fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()>;
101
102    /// Called after any component was pushed, and for every requested terminal component even if it was cached,
103    /// with the path available at [`Stack::current()`].
104    ///
105    /// `is_last_component` is `true` if the path is completely built, which typically means it's not a directory.
106    fn push(&mut self, is_last_component: bool, stack: &Stack) -> std::io::Result<()>;
107
108    /// Called right after a directory-component was popped off the stack.
109    ///
110    /// Use it to pop information off internal data structures. Note that no equivalent call exists for popping
111    /// the file-component.
112    fn pop_directory(&mut self);
113}
114
115impl Stack {
116    /// Create a new instance with `root` being the base for all future paths we handle, assuming it to be valid which includes
117    /// symbolic links to be included in it as well.
118    pub fn new(root: PathBuf) -> Self {
119        Stack {
120            current: root.clone(),
121            current_relative: PathBuf::with_capacity(128),
122            valid_components: 0,
123            root,
124            current_is_directory: true,
125        }
126    }
127
128    /// Set the current stack to point to the `relative` path, calling [`Delegate::push()`] for new components
129    /// and for the terminal component, even when the latter was cached.
130    ///
131    /// Only leading directories remain cached. The caller may replace the terminal entry without invalidating the
132    /// stack: using it as a leading directory later will validate it again. Shared leading directories are reused
133    /// without additional delegate calls and must not be changed by the caller or other actors.
134    /// Directory push/pop calls remain balanced even when a previously leading directory becomes a terminal entry.
135    ///
136    /// The path is also expected to be normalized, and should not contain extra separators, and must not contain `..`
137    /// or have leading or trailing slashes (or additionally backslashes on Windows).
138    pub fn make_relative_path_current(
139        &mut self,
140        relative: impl ToNormalPathComponents,
141        delegate: &mut dyn Delegate,
142    ) -> std::io::Result<()> {
143        let mut components = relative.to_normal_path_components().peekable();
144        if self.valid_components != 0 && components.peek().is_none() {
145            return Err(std::io::Error::other("empty inputs are not allowed"));
146        }
147        if self.valid_components == 0 {
148            delegate.push_directory(self)?;
149        }
150
151        let mut existing_components = self.current_relative.components();
152        let mut matching_components = 0;
153        while let (Some(existing_comp), Some(new_comp)) = (existing_components.next(), components.peek()) {
154            match new_comp {
155                Ok(new_comp) => {
156                    if existing_comp.as_os_str() == *new_comp {
157                        components.next();
158                        matching_components += 1;
159                    } else {
160                        break;
161                    }
162                }
163                Err(_) => {
164                    let err = components.next().expect("just peeked").expect_err("peeked an error");
165                    return Err(std::io::Error::other(err.into_error()));
166                }
167            }
168        }
169
170        for _ in 0..self.valid_components - matching_components {
171            self.current.pop();
172            self.current_relative.pop();
173            if self.current_is_directory {
174                delegate.pop_directory();
175            }
176            self.current_is_directory = true;
177        }
178        self.valid_components = matching_components;
179
180        if matching_components != 0 && components.peek().is_none() {
181            // The caller may replace this entry, so it must no longer be cached as a leading directory.
182            if self.current_is_directory {
183                delegate.pop_directory();
184                self.current_is_directory = false;
185            }
186            return delegate.push(true, self);
187        }
188
189        if !self.current_is_directory && components.peek().is_some() {
190            delegate.push(false, self)?;
191            // Make sure we don't consider this a directory if the `push` above fails.
192            self.current_is_directory = true;
193            if let Err(err) = delegate.push_directory(self) {
194                self.current_is_directory = false;
195                return Err(err);
196            }
197        }
198
199        while let Some(comp) = components.next() {
200            let comp = comp.map_err(|err| std::io::Error::other(err.into_error()))?;
201            let is_last_component = components.peek().is_none();
202            let parent_is_directory = self.current_is_directory;
203            self.current_is_directory = !is_last_component;
204            self.current.push(comp);
205            self.current_relative.push(comp);
206            self.valid_components += 1;
207            let res = delegate.push(is_last_component, self);
208            if let Err(err) = res {
209                self.current.pop();
210                self.current_relative.pop();
211                self.valid_components -= 1;
212                self.current_is_directory = parent_is_directory;
213                return Err(err);
214            }
215            if self.current_is_directory
216                && let Err(err) = delegate.push_directory(self)
217            {
218                self.current.pop();
219                self.current_relative.pop();
220                self.valid_components -= 1;
221                self.current_is_directory = parent_is_directory;
222                return Err(err);
223            }
224        }
225        Ok(())
226    }
227}