gix_config/file/includes/
mod.rs1use 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 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 = §ion.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[§ion_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 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 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};