Skip to main content

gix_config/file/includes/
mod.rs

1use std::path::{Path, PathBuf};
2
3use bstr::{BStr, BString, ByteSlice, ByteVec};
4use gix_features::threading::OwnShared;
5use gix_ref::Category;
6
7use crate::{
8    File, file,
9    file::{Metadata, SectionId, includes, init},
10    path,
11};
12
13impl File {
14    /// Traverse all `include` and `includeIf` directives found in this instance and follow them, loading the
15    /// referenced files from their location and adding their content right past the value that included them.
16    ///
17    /// # Limitations
18    ///
19    /// - Note that this method is _not idempotent_ and calling it multiple times will resolve includes multiple
20    ///   times. It's recommended use is as part of a multi-step bootstrapping which needs fine-grained control,
21    ///   and unless that's given one should prefer one of the other ways of initialization that resolve includes
22    ///   at the right time.
23    ///
24    /// # Deviation
25    ///
26    /// - included values are added after the _section_ that included them, not directly after the value. This is
27    ///   a deviation from how git does it, as it technically adds new value right after the include path itself,
28    ///   technically 'splitting' the section. This can only make a difference if the `include` section also has values
29    ///   which later overwrite portions of the included file, which seems unusual as these would be related to `includes`.
30    ///   We can fix this by 'splitting' the include section if needed so the included sections are put into the right place.
31    /// - `hasconfig:remote.*.url` will not prevent itself to include files with `[remote "name"]\nurl = x` values, but it also
32    ///   won't match them, i.e. one cannot include something that will cause the condition to match or to always be true.
33    pub fn resolve_includes(&mut self, options: init::Options<'_>) -> Result<(), Error> {
34        if options.includes.max_depth == 0 {
35            return Ok(());
36        }
37        let mut buf = Vec::new();
38        resolve(self, &mut buf, options)
39    }
40}
41
42pub(crate) fn resolve(config: &mut File, buf: &mut Vec<u8>, options: init::Options<'_>) -> Result<(), Error> {
43    resolve_includes_recursive(None, config, 0, buf, options)
44}
45
46fn resolve_includes_recursive(
47    search_config: Option<&File>,
48    target_config: &mut File,
49    depth: u8,
50    buf: &mut Vec<u8>,
51    options: init::Options<'_>,
52) -> Result<(), Error> {
53    if depth == options.includes.max_depth {
54        return if options.includes.err_on_max_depth_exceeded {
55            Err(Error::IncludeDepthExceeded {
56                max_depth: options.includes.max_depth,
57            })
58        } else {
59            Ok(())
60        };
61    }
62
63    for id in target_config.section_order.clone().into_iter() {
64        let section = &target_config.sections[&id];
65        let header = &section.header;
66        let backing = &target_config.backing;
67        let header_name = header.name.as_bstr_in(backing);
68        let mut paths = None;
69        if header_name == "include" && header.subsection_name.is_none() {
70            paths = Some(gather_paths(section, id, backing));
71        } else if header_name == "includeIf" {
72            if let Some(condition) = &header.subsection_name {
73                let target_config_path = section.meta.path.as_deref();
74                if include_condition_match(
75                    condition.value_in(backing),
76                    target_config_path,
77                    search_config.unwrap_or(target_config),
78                    options.includes,
79                )? {
80                    paths = Some(gather_paths(section, id, backing));
81                }
82            }
83        }
84        if let Some(paths) = paths {
85            insert_includes_recursively(paths, target_config, depth, options, buf)?;
86        }
87    }
88    Ok(())
89}
90
91fn insert_includes_recursively(
92    section_ids_and_include_paths: Vec<(SectionId, crate::Path)>,
93    target_config: &mut File,
94    depth: u8,
95    options: init::Options<'_>,
96    buf: &mut Vec<u8>,
97) -> Result<(), Error> {
98    for (section_id, config_path) in section_ids_and_include_paths {
99        let meta = OwnShared::clone(&target_config.sections[&section_id].meta);
100        let target_config_path = meta.path.as_deref();
101        let config_path = match resolve_path(config_path, target_config_path, options.includes)? {
102            Some(p) => p,
103            None => continue,
104        };
105        if !config_path.is_file() {
106            continue;
107        }
108
109        buf.clear();
110        std::io::copy(
111            &mut std::fs::File::open(&config_path).map_err(|err| Error::Io {
112                source: err,
113                path: config_path.to_owned(),
114            })?,
115            buf,
116        )
117        .map_err(Error::CopyBuffer)?;
118        let config_meta = Metadata {
119            path: Some(config_path),
120            trust: meta.trust,
121            level: meta.level + 1,
122            source: meta.source,
123        };
124        let no_follow_options = init::Options {
125            includes: includes::Options::no_follow(),
126            ..options
127        };
128
129        let mut include_config =
130            File::from_bytes_owned(buf, config_meta, no_follow_options).map_err(|err| match err {
131                init::Error::Parse(err) => Error::Parse(err),
132                init::Error::Interpolate(err) => Error::Interpolate(err),
133                init::Error::Span(err) => Error::Span(err),
134                init::Error::Includes(_) => unreachable!("BUG: {:?} not possible due to no-follow options", err),
135            })?;
136        resolve_includes_recursive(Some(target_config), &mut include_config, depth + 1, buf, options)?;
137
138        target_config.append_or_insert(include_config, Some(section_id))?;
139    }
140    Ok(())
141}
142
143fn gather_paths(section: &file::SectionData, id: SectionId, backing: &[u8]) -> Vec<(SectionId, crate::Path)> {
144    section
145        .body
146        .values_in(backing, "path")
147        .into_iter()
148        .map(|path| (id, crate::Path::from(path)))
149        .collect()
150}
151
152fn include_condition_match(
153    condition: &BStr,
154    target_config_path: Option<&Path>,
155    search_config: &File,
156    options: Options<'_>,
157) -> Result<bool, Error> {
158    let mut tokens = condition.splitn(2, |b| *b == b':');
159    let (prefix, condition) = match (tokens.next(), tokens.next()) {
160        (Some(a), Some(b)) => (a, b),
161        _ => return Ok(false),
162    };
163    let condition = condition.as_bstr();
164    match prefix {
165        b"gitdir" => gitdir_matches(
166            condition,
167            target_config_path,
168            options,
169            gix_glob::wildmatch::Mode::empty(),
170        ),
171        b"gitdir/i" => gitdir_matches(
172            condition,
173            target_config_path,
174            options,
175            gix_glob::wildmatch::Mode::IGNORE_CASE,
176        ),
177        b"onbranch" => Ok(onbranch_matches(condition, options.conditional).is_some()),
178        b"hasconfig" => {
179            let mut tokens = condition.splitn(2, |b| *b == b':');
180            let (key_glob, value_glob) = match (tokens.next(), tokens.next()) {
181                (Some(a), Some(b)) => (a, b),
182                _ => return Ok(false),
183            };
184            if key_glob.as_bstr() != "remote.*.url" {
185                return Ok(false);
186            }
187            let Some(sections) = search_config.sections_by_name("remote") else {
188                return Ok(false);
189            };
190            for remote in sections {
191                for url in remote.values("url") {
192                    let glob_matches = gix_glob::wildmatch(
193                        value_glob.as_bstr(),
194                        url.as_ref(),
195                        gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL,
196                    );
197                    if glob_matches {
198                        return Ok(true);
199                    }
200                }
201            }
202            Ok(false)
203        }
204        _ => Ok(false),
205    }
206}
207
208fn onbranch_matches(
209    condition: &BStr,
210    conditional::Context { branch_name, .. }: conditional::Context<'_>,
211) -> Option<()> {
212    let branch_name = branch_name?;
213    let (_, branch_name) = branch_name
214        .category_and_short_name()
215        .filter(|(cat, _)| *cat == Category::LocalBranch)?;
216
217    let condition: BString = if condition.ends_with(b"/") {
218        let mut condition: BString = condition.into();
219        condition.push_str("**");
220        condition
221    } else {
222        condition.into()
223    };
224
225    gix_glob::wildmatch(
226        condition.as_bstr(),
227        branch_name,
228        gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL,
229    )
230    .then_some(())
231}
232
233fn gitdir_matches(
234    condition_path: &BStr,
235    target_config_path: Option<&Path>,
236    Options {
237        conditional: conditional::Context { git_dir, .. },
238        interpolate: context,
239        err_on_interpolation_failure,
240        err_on_missing_config_path,
241        ..
242    }: Options<'_>,
243    wildmatch_mode: gix_glob::wildmatch::Mode,
244) -> Result<bool, Error> {
245    if !err_on_interpolation_failure && git_dir.is_none() {
246        return Ok(false);
247    }
248    let git_dir = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(git_dir.ok_or(Error::MissingGitDir)?));
249
250    let mut pattern_path: BString = {
251        let path = match check_interpolation_result(
252            err_on_interpolation_failure,
253            crate::Path::from(condition_path.to_owned()).interpolate(context),
254        )? {
255            Some(p) => p,
256            None => return Ok(false),
257        };
258        gix_path::into_bstr(path).into_owned()
259    };
260    // NOTE: yes, only if we do path interpolation will the slashes be forced to unix separators on windows
261    if pattern_path != condition_path {
262        pattern_path = gix_path::to_unix_separators_on_windows(pattern_path).into_owned();
263    }
264
265    if let Some(relative_pattern_path) = pattern_path.strip_prefix(b"./") {
266        if !err_on_missing_config_path && target_config_path.is_none() {
267            return Ok(false);
268        }
269        let parent_dir = target_config_path
270            .ok_or(Error::MissingConfigPath)?
271            .parent()
272            .expect("config path can never be /");
273        let mut joined_path = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(parent_dir)).into_owned();
274        joined_path.push(b'/');
275        joined_path.extend_from_slice(relative_pattern_path);
276        pattern_path = joined_path;
277    }
278
279    // NOTE: this special handling of leading backslash is needed to do it like git does
280    if pattern_path.iter().next() != Some(&(std::path::MAIN_SEPARATOR as u8))
281        && !gix_path::from_bstr(pattern_path.clone()).is_absolute()
282    {
283        pattern_path.insert_str(0, "**/");
284    }
285    if pattern_path.ends_with(b"/") {
286        pattern_path.push_str("**");
287    }
288
289    let match_mode = gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL | wildmatch_mode;
290    let is_match = gix_glob::wildmatch(pattern_path.as_bstr(), git_dir.as_bstr(), match_mode);
291    if is_match {
292        return Ok(true);
293    }
294
295    let expanded_git_dir = gix_path::into_bstr(gix_path::realpath(gix_path::from_byte_slice(&git_dir))?);
296    Ok(gix_glob::wildmatch(
297        pattern_path.as_bstr(),
298        expanded_git_dir.as_bstr(),
299        match_mode,
300    ))
301}
302
303fn check_interpolation_result(
304    disable: bool,
305    res: Result<impl Into<PathBuf>, path::interpolate::Error>,
306) -> Result<Option<PathBuf>, path::interpolate::Error> {
307    if disable {
308        return res.map(|path| Some(path.into()));
309    }
310    match res {
311        Ok(good) => Ok(Some(good.into())),
312        Err(err) => match err {
313            path::interpolate::Error::Missing { .. } | path::interpolate::Error::UserInterpolationUnsupported => {
314                Ok(None)
315            }
316            path::interpolate::Error::UsernameConversion(_) | path::interpolate::Error::Utf8Conversion { .. } => {
317                Err(err)
318            }
319        },
320    }
321}
322
323fn resolve_path(
324    path: crate::Path,
325    target_config_path: Option<&Path>,
326    includes::Options {
327        interpolate: context,
328        err_on_interpolation_failure,
329        err_on_missing_config_path,
330        ..
331    }: includes::Options<'_>,
332) -> Result<Option<PathBuf>, Error> {
333    let path = match check_interpolation_result(err_on_interpolation_failure, path.interpolate(context))? {
334        Some(p) => p,
335        None => return Ok(None),
336    };
337    let path: PathBuf = if path.is_relative() {
338        if !err_on_missing_config_path && target_config_path.is_none() {
339            return Ok(None);
340        }
341        target_config_path
342            .ok_or(Error::MissingConfigPath)?
343            .parent()
344            .expect("path is a config file which naturally lives in a directory")
345            .join(path)
346    } else {
347        path
348    };
349    Ok(Some(path))
350}
351
352mod types;
353pub use types::{Error, Options, conditional};