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());
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    let Some(last_component) = stack.current_relative().components().next_back() else {
138        return Ok(());
139    };
140    let last_component = gix_path::try_into_bstr(std::borrow::Cow::Borrowed(last_component.as_os_str().as_ref()))
141        .map_err(|_err| {
142            std::io::Error::other(format!(
143                "Path component {last_component:?} of path \"{}\" contained invalid UTF-8 and could not be validated",
144                stack.current_relative().display()
145            ))
146        })?;
147
148    if let Err(err) = gix_validate::path::component(
149        last_component.as_ref(),
150        mode.and_then(|m| {
151            (m == gix_index::entry::Mode::SYMLINK).then_some(gix_validate::path::component::Mode::Symlink)
152        }),
153        opts,
154    ) {
155        return Err(std::io::Error::other(err));
156    }
157    Ok(())
158}
159
160#[cfg(feature = "attributes")]
161fn create_leading_directory(
162    is_last_component: bool,
163    stack: &gix_fs::Stack,
164    mode: Option<gix_index::entry::Mode>,
165    mkdir_calls: &mut usize,
166    unlink_on_collision: bool,
167    #[cfg_attr(not(windows), allow(unused_variables))] check_terminal_symlinks: bool,
168) -> std::io::Result<()> {
169    if is_last_component && !crate::stack::mode_is_dir(mode).unwrap_or(false) {
170        #[cfg(not(windows))]
171        {
172            return Ok(());
173        }
174        #[cfg(windows)]
175        {
176            // Forced checkout delegates terminal symlink detection and removal to the caller immediately before
177            // replacement, avoiding a redundant check here. Callers combining these flags must uphold that no-follow
178            // contract; this stack only protects leading components in that mode.
179            if unlink_on_collision || !check_terminal_symlinks {
180                return Ok(());
181            }
182            return match stack.current().symlink_metadata() {
183                Ok(meta) if meta.file_type().is_symlink() => Err(std::io::ErrorKind::AlreadyExists.into()),
184                Ok(_) => Ok(()),
185                Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
186                Err(err) => Err(err),
187            };
188        }
189    }
190    *mkdir_calls += 1;
191    match std::fs::create_dir(stack.current()) {
192        Ok(()) => Ok(()),
193        Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
194            let meta = stack.current().symlink_metadata()?;
195            if meta.is_dir() {
196                Ok(())
197            } else if unlink_on_collision {
198                if meta.file_type().is_symlink() {
199                    gix_fs::symlink::remove(stack.current())?;
200                } else {
201                    std::fs::remove_file(stack.current())?;
202                }
203                *mkdir_calls += 1;
204                std::fs::create_dir(stack.current())
205            } else {
206                Err(err)
207            }
208        }
209        Err(err) => Err(err),
210    }
211}