Skip to main content

gix_worktree/stack/
delegate.rs

1use crate::{PathIdMapping, stack::State};
2
3/// Various aggregate numbers related to the stack delegate itself.
4#[derive(Default, Clone, Copy, Debug)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub struct Statistics {
7    /// The amount of `std::fs::create_dir` calls.
8    ///
9    /// This only happens if we are in the respective mode to create leading directories efficiently.
10    pub num_mkdir_calls: usize,
11    /// Amount of calls to push a path element.
12    pub push_element: usize,
13    /// Amount of calls to push a directory.
14    pub push_directory: usize,
15    /// Amount of calls to pop a directory.
16    pub pop_directory: usize,
17}
18
19pub(crate) struct StackDelegate<'a, 'find> {
20    pub state: &'a mut State,
21    pub buf: &'a mut Vec<u8>,
22    #[cfg_attr(not(feature = "attributes"), allow(dead_code))]
23    pub mode: Option<gix_index::entry::Mode>,
24    pub id_mappings: &'a Vec<PathIdMapping>,
25    pub objects: &'find dyn gix_object::Find,
26    pub case: gix_glob::pattern::Case,
27    pub reject_temrinal_symlinks: bool,
28    pub statistics: &'a mut super::Statistics,
29}
30
31impl gix_fs::stack::Delegate for StackDelegate<'_, '_> {
32    fn push_directory(&mut self, stack: &gix_fs::Stack) -> std::io::Result<()> {
33        self.statistics.delegate.push_directory += 1;
34        let rela_dir_bstr = gix_path::into_bstr(stack.current_relative()).map_err(std::io::Error::other)?;
35        let rela_dir = gix_path::to_unix_separators_on_windows(rela_dir_bstr);
36        match &mut self.state {
37            #[cfg(feature = "attributes")]
38            State::CreateDirectoryAndAttributesStack { attributes, .. } | State::AttributesStack(attributes) => {
39                attributes.push_directory(
40                    stack.root(),
41                    stack.current(),
42                    &rela_dir,
43                    self.buf,
44                    self.id_mappings,
45                    self.objects,
46                    &mut self.statistics.attributes,
47                )?;
48            }
49            #[cfg(feature = "attributes")]
50            State::AttributesAndIgnoreStack { ignore, attributes } => {
51                attributes.push_directory(
52                    stack.root(),
53                    stack.current(),
54                    &rela_dir,
55                    self.buf,
56                    self.id_mappings,
57                    self.objects,
58                    &mut self.statistics.attributes,
59                )?;
60                ignore.push_directory(
61                    stack.root(),
62                    stack.current(),
63                    &rela_dir,
64                    self.buf,
65                    self.id_mappings,
66                    self.objects,
67                    self.case,
68                    &mut self.statistics.ignore,
69                )?;
70            }
71            State::IgnoreStack(ignore) => ignore.push_directory(
72                stack.root(),
73                stack.current(),
74                &rela_dir,
75                self.buf,
76                self.id_mappings,
77                self.objects,
78                self.case,
79                &mut self.statistics.ignore,
80            )?,
81        }
82        Ok(())
83    }
84
85    #[cfg_attr(not(feature = "attributes"), allow(unused_variables))]
86    fn push(&mut self, is_last_component: bool, stack: &gix_fs::Stack) -> std::io::Result<()> {
87        self.statistics.delegate.push_element += 1;
88        match &mut self.state {
89            #[cfg(feature = "attributes")]
90            State::CreateDirectoryAndAttributesStack {
91                unlink_on_collision,
92                validate,
93                attributes: _,
94            } => {
95                validate_last_component(stack, self.mode, *validate)?;
96                create_leading_directory(
97                    is_last_component,
98                    stack,
99                    self.mode,
100                    &mut self.statistics.delegate.num_mkdir_calls,
101                    *unlink_on_collision,
102                    self.reject_temrinal_symlinks,
103                )?;
104            }
105            #[cfg(feature = "attributes")]
106            State::AttributesAndIgnoreStack { .. } | State::AttributesStack(_) => {}
107            State::IgnoreStack(_) => {}
108        }
109        Ok(())
110    }
111
112    fn pop_directory(&mut self) {
113        self.statistics.delegate.pop_directory += 1;
114        match &mut self.state {
115            #[cfg(feature = "attributes")]
116            State::CreateDirectoryAndAttributesStack { attributes, .. } | State::AttributesStack(attributes) => {
117                attributes.pop_directory();
118            }
119            #[cfg(feature = "attributes")]
120            State::AttributesAndIgnoreStack { attributes, ignore } => {
121                attributes.pop_directory();
122                ignore.pop_directory();
123            }
124            State::IgnoreStack(ignore) => {
125                ignore.pop_directory();
126            }
127        }
128    }
129}
130
131#[cfg(feature = "attributes")]
132fn validate_last_component(
133    stack: &gix_fs::Stack,
134    mode: Option<gix_index::entry::Mode>,
135    opts: gix_validate::path::component::Options,
136) -> std::io::Result<()> {
137    use gix_error::ResultExt;
138
139    let Some(last_component) = stack.current_relative().components().next_back() else {
140        return Ok(());
141    };
142    let last_component = gix_path::into_bstr(std::borrow::Cow::Borrowed(last_component.as_os_str().as_ref()))
143        .or_raise(|| {
144            gix_error::message!(
145                "Path component {last_component:?} of path \"{}\" contained invalid UTF-8 and could not be validated",
146                stack.current_relative().display()
147            )
148        })
149        .map_err(std::io::Error::other)?;
150
151    if let Err(err) = gix_validate::path::component(
152        last_component.as_ref(),
153        mode.and_then(|m| {
154            (m == gix_index::entry::Mode::SYMLINK).then_some(gix_validate::path::component::Mode::Symlink)
155        }),
156        opts,
157    ) {
158        return Err(std::io::Error::other(err));
159    }
160    Ok(())
161}
162
163#[cfg(feature = "attributes")]
164fn create_leading_directory(
165    is_last_component: bool,
166    stack: &gix_fs::Stack,
167    mode: Option<gix_index::entry::Mode>,
168    mkdir_calls: &mut usize,
169    unlink_on_collision: bool,
170    #[cfg_attr(not(windows), allow(unused_variables))] check_terminal_symlinks: bool,
171) -> std::io::Result<()> {
172    if is_last_component && !crate::stack::mode_is_dir(mode).unwrap_or(false) {
173        #[cfg(not(windows))]
174        {
175            return Ok(());
176        }
177        #[cfg(windows)]
178        {
179            // Forced checkout delegates terminal symlink detection and removal to the caller immediately before
180            // replacement, avoiding a redundant check here. Callers combining these flags must uphold that no-follow
181            // contract; this stack only protects leading components in that mode.
182            if unlink_on_collision || !check_terminal_symlinks {
183                return Ok(());
184            }
185            return match stack.current().symlink_metadata() {
186                Ok(meta) if meta.file_type().is_symlink() => Err(std::io::ErrorKind::AlreadyExists.into()),
187                Ok(_) => Ok(()),
188                Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
189                Err(err) => Err(err),
190            };
191        }
192    }
193    *mkdir_calls += 1;
194    match std::fs::create_dir(stack.current()) {
195        Ok(()) => Ok(()),
196        Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
197            let meta = stack.current().symlink_metadata()?;
198            if meta.is_dir() {
199                Ok(())
200            } else if unlink_on_collision {
201                if meta.file_type().is_symlink() {
202                    gix_fs::symlink::remove(stack.current())?;
203                } else {
204                    std::fs::remove_file(stack.current())?;
205                }
206                *mkdir_calls += 1;
207                std::fs::create_dir(stack.current())
208            } else {
209                Err(err)
210            }
211        }
212        Err(err) => Err(err),
213    }
214}