1use std::{
2 ffi::OsStr,
3 path::{Component, Path, PathBuf},
4};
5
6use bstr::{BStr, BString, ByteSlice};
7
8use crate::Stack;
9
10pub mod to_normal_path_components {
12 use std::path::PathBuf;
13
14 #[derive(Debug, thiserror::Error)]
16 #[allow(missing_docs)]
17 pub enum Error {
18 #[error("Input path \"{path}\" contains relative or absolute components", path = .0.display())]
19 NotANormalComponent(PathBuf),
20 #[error("Could not convert to UTF8 or from UTF8 due to ill-formed input")]
21 IllegalUtf8,
22 }
23}
24
25pub trait ToNormalPathComponents {
27 fn to_normal_path_components(&self) -> impl Iterator<Item = Result<&OsStr, to_normal_path_components::Error>>;
29}
30
31impl ToNormalPathComponents for &Path {
32 fn to_normal_path_components(&self) -> impl Iterator<Item = Result<&OsStr, to_normal_path_components::Error>> {
33 self.components().map(|c| component_to_os_str(c, self))
34 }
35}
36
37impl ToNormalPathComponents for PathBuf {
38 fn to_normal_path_components(&self) -> impl Iterator<Item = Result<&OsStr, to_normal_path_components::Error>> {
39 self.components().map(|c| component_to_os_str(c, self))
40 }
41}
42
43fn component_to_os_str<'a>(
44 component: Component<'a>,
45 path_with_component: &Path,
46) -> Result<&'a OsStr, to_normal_path_components::Error> {
47 match component {
48 Component::Normal(os_str) => Ok(os_str),
49 _ => Err(to_normal_path_components::Error::NotANormalComponent(
50 path_with_component.to_owned(),
51 )),
52 }
53}
54
55impl ToNormalPathComponents for &BStr {
56 fn to_normal_path_components(&self) -> impl Iterator<Item = Result<&OsStr, to_normal_path_components::Error>> {
57 self.split(|b| *b == b'/')
58 .filter_map(|c| bytes_component_to_os_str(c, self))
59 }
60}
61
62impl ToNormalPathComponents for &str {
63 fn to_normal_path_components(&self) -> impl Iterator<Item = Result<&OsStr, to_normal_path_components::Error>> {
64 self.split('/')
65 .filter_map(|c| bytes_component_to_os_str(c.as_bytes(), (*self).into()))
66 }
67}
68
69impl ToNormalPathComponents for &BString {
70 fn to_normal_path_components(&self) -> impl Iterator<Item = Result<&OsStr, to_normal_path_components::Error>> {
71 self.split(|b| *b == b'/')
72 .filter_map(|c| bytes_component_to_os_str(c, self.as_bstr()))
73 }
74}
75
76fn bytes_component_to_os_str<'a>(
77 component: &'a [u8],
78 path: &BStr,
79) -> Option<Result<&'a OsStr, to_normal_path_components::Error>> {
80 if component.is_empty() {
81 return None;
82 }
83 let component = match gix_path::try_from_byte_slice(component.as_bstr())
84 .map_err(|_| to_normal_path_components::Error::IllegalUtf8)
85 {
86 Ok(c) => c,
87 Err(err) => return Some(Err(err)),
88 };
89 let component = component.components().next()?;
90 Some(component_to_os_str(
91 component,
92 gix_path::try_from_byte_slice(path.as_ref()).ok()?,
93 ))
94}
95
96impl Stack {
98 pub fn root(&self) -> &Path {
100 &self.root
101 }
102
103 pub fn current(&self) -> &Path {
105 &self.current
106 }
107
108 pub fn current_relative(&self) -> &Path {
110 &self.current_relative
111 }
112}
113
114pub trait Delegate {
116 fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()>;
122
123 fn push(&mut self, is_last_component: bool, stack: &Stack) -> std::io::Result<()>;
127
128 fn pop_directory(&mut self);
133}
134
135impl Stack {
136 pub fn new(root: PathBuf) -> Self {
139 Stack {
140 current: root.clone(),
141 current_relative: PathBuf::with_capacity(128),
142 valid_components: 0,
143 root,
144 current_is_directory: true,
145 }
146 }
147
148 pub fn make_relative_path_current(
157 &mut self,
158 relative: impl ToNormalPathComponents,
159 delegate: &mut dyn Delegate,
160 ) -> std::io::Result<()> {
161 let mut components = relative.to_normal_path_components().peekable();
162 if self.valid_components != 0 && components.peek().is_none() {
163 return Err(std::io::Error::other("empty inputs are not allowed"));
164 }
165 if self.valid_components == 0 {
166 delegate.push_directory(self)?;
167 }
168
169 let mut existing_components = self.current_relative.components();
170 let mut matching_components = 0;
171 while let (Some(existing_comp), Some(new_comp)) = (existing_components.next(), components.peek()) {
172 match new_comp {
173 Ok(new_comp) => {
174 if existing_comp.as_os_str() == *new_comp {
175 components.next();
176 matching_components += 1;
177 } else {
178 break;
179 }
180 }
181 Err(err) => return Err(std::io::Error::other(format!("{err}"))),
182 }
183 }
184
185 for _ in 0..self.valid_components - matching_components {
186 self.current.pop();
187 self.current_relative.pop();
188 if self.current_is_directory {
189 delegate.pop_directory();
190 }
191 self.current_is_directory = true;
192 }
193 self.valid_components = matching_components;
194
195 if !self.current_is_directory && components.peek().is_some() {
196 delegate.push_directory(self)?;
197 }
198
199 while let Some(comp) = components.next() {
200 let comp = comp.map_err(std::io::Error::other)?;
201 let is_last_component = components.peek().is_none();
202 self.current_is_directory = !is_last_component;
203 self.current.push(comp);
204 self.current_relative.push(comp);
205 self.valid_components += 1;
206 let res = delegate.push(is_last_component, self);
207 if self.current_is_directory {
208 delegate.push_directory(self)?;
209 }
210
211 if let Err(err) = res {
212 self.current.pop();
213 self.current_relative.pop();
214 self.valid_components -= 1;
215 return Err(err);
216 }
217 }
218 Ok(())
219 }
220}