gix_config/file/includes/
mod.rs1use std::path::{Path, PathBuf};
2
3use bstr::{BStr, BString, ByteSlice, ByteVec};
4use gix_error::{ErrorExt, ExnResult, OptionExt, ResultExt, message, not_found, validation};
5use gix_features::threading::OwnShared;
6use gix_ref::Category;
7
8use crate::{
9 File, file,
10 file::{Metadata, SectionId, includes, init},
11};
12
13impl File {
14 pub fn resolve_includes(&mut self, options: init::Options<'_>) -> ExnResult {
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<'_>) -> ExnResult {
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) -> ExnResult {
53 if depth == options.includes.max_depth {
54 return if options.includes.err_on_max_depth_exceeded {
55 Err(validation(format!(
56 "The maximum allowed length {} of the file include chain built by following nested resolve_includes is exceeded",
57 options.includes.max_depth
58 ))
59 .raise_erased())
60 } else {
61 Ok(())
62 };
63 }
64
65 for id in target_config.section_order.clone().into_iter() {
66 let section = &target_config.sections[&id];
67 let header = §ion.header;
68 let backing = &target_config.backing;
69 let header_name = header.name.as_bstr_in(backing);
70 let mut paths = None;
71 if header_name == "include" && header.subsection_name.is_none() {
72 paths = Some(gather_paths(section, id, backing));
73 } else if header_name == "includeIf"
74 && let Some(condition) = &header.subsection_name
75 {
76 let target_config_path = section.meta.path.as_deref();
77 if include_condition_match(
78 condition.value_in(backing),
79 target_config_path,
80 search_config.unwrap_or(target_config),
81 options.includes,
82 )? {
83 paths = Some(gather_paths(section, id, backing));
84 }
85 }
86 if let Some(paths) = paths {
87 insert_includes_recursively(paths, target_config, depth, options, buf)?;
88 }
89 }
90 Ok(())
91}
92
93fn insert_includes_recursively(
94 section_ids_and_include_paths: Vec<(SectionId, crate::Path)>,
95 target_config: &mut File,
96 depth: u8,
97 options: init::Options<'_>,
98 buf: &mut Vec<u8>,
99) -> ExnResult {
100 for (section_id, config_path) in section_ids_and_include_paths {
101 let meta = OwnShared::clone(&target_config.sections[§ion_id].meta);
102 let target_config_path = meta.path.as_deref();
103 let config_path = match resolve_path(config_path, target_config_path, options.includes)? {
104 Some(p) => p,
105 None => continue,
106 };
107 if !config_path.is_file() {
108 continue;
109 }
110
111 buf.clear();
112 std::io::copy(
113 &mut std::fs::File::open(&config_path).or_raise_erased(|| {
114 message!(
115 "Could not read included configuration file at '{}'",
116 config_path.display()
117 )
118 })?,
119 buf,
120 )
121 .or_raise_erased(|| message("Failed to copy configuration file into buffer"))?;
122 let config_meta = Metadata {
123 path: Some(config_path),
124 trust: meta.trust,
125 level: meta.level + 1,
126 source: meta.source,
127 };
128 let no_follow_options = init::Options {
129 includes: includes::Options::no_follow(),
130 ..options
131 };
132
133 let mut include_config = File::from_bytes_owned(buf, config_meta, no_follow_options)
134 .or_raise_erased(|| message("Could not parse included configuration file"))?;
135 resolve_includes_recursive(Some(target_config), &mut include_config, depth + 1, buf, options)?;
136
137 target_config
138 .append_or_insert(include_config, Some(section_id))
139 .or_raise_erased(|| message("Could not append included configuration"))?;
140 }
141 Ok(())
142}
143
144fn gather_paths(section: &file::SectionData, id: SectionId, backing: &[u8]) -> Vec<(SectionId, crate::Path)> {
145 section
146 .body
147 .values_in(backing, "path")
148 .into_iter()
149 .map(|path| (id, crate::Path::from(path)))
150 .collect()
151}
152
153fn include_condition_match(
154 condition: &BStr,
155 target_config_path: Option<&Path>,
156 search_config: &File,
157 options: Options<'_>,
158) -> ExnResult<bool> {
159 let mut tokens = condition.splitn(2, |b| *b == b':');
160 let (prefix, condition) = match (tokens.next(), tokens.next()) {
161 (Some(a), Some(b)) => (a, b),
162 _ => return Ok(false),
163 };
164 let condition = condition.as_bstr();
165 match prefix {
166 b"gitdir" => gitdir_matches(
167 condition,
168 target_config_path,
169 options,
170 gix_glob::wildmatch::Mode::empty(),
171 ),
172 b"gitdir/i" => gitdir_matches(
173 condition,
174 target_config_path,
175 options,
176 gix_glob::wildmatch::Mode::IGNORE_CASE,
177 ),
178 b"onbranch" => Ok(onbranch_matches(condition, options.conditional).is_some()),
179 b"hasconfig" => {
180 let mut tokens = condition.splitn(2, |b| *b == b':');
181 let (key_glob, value_glob) = match (tokens.next(), tokens.next()) {
182 (Some(a), Some(b)) => (a, b),
183 _ => return Ok(false),
184 };
185 if key_glob.as_bstr() != "remote.*.url" {
186 return Ok(false);
187 }
188 let Some(sections) = search_config.sections_by_name("remote") else {
189 return Ok(false);
190 };
191 for remote in sections {
192 for url in remote.values("url") {
193 let glob_matches = gix_glob::wildmatch(
194 value_glob.as_bstr(),
195 url.as_ref(),
196 gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL,
197 );
198 if glob_matches {
199 return Ok(true);
200 }
201 }
202 }
203 Ok(false)
204 }
205 _ => Ok(false),
206 }
207}
208
209fn onbranch_matches(
210 condition: &BStr,
211 conditional::Context { branch_name, .. }: conditional::Context<'_>,
212) -> Option<()> {
213 let branch_name = branch_name?;
214 let (_, branch_name) = branch_name
215 .category_and_short_name()
216 .filter(|(cat, _)| *cat == Category::LocalBranch)?;
217
218 let condition: BString = if condition.ends_with(b"/") {
219 let mut condition: BString = condition.into();
220 condition.push_str("**");
221 condition
222 } else {
223 condition.into()
224 };
225
226 gix_glob::wildmatch(
227 condition.as_bstr(),
228 branch_name,
229 gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL,
230 )
231 .then_some(())
232}
233
234fn gitdir_matches(
235 condition_path: &BStr,
236 target_config_path: Option<&Path>,
237 Options {
238 conditional: conditional::Context { git_dir, .. },
239 interpolate: context,
240 err_on_interpolation_failure,
241 err_on_missing_config_path,
242 ..
243 }: Options<'_>,
244 wildmatch_mode: gix_glob::wildmatch::Mode,
245) -> ExnResult<bool> {
246 if !err_on_interpolation_failure && git_dir.is_none() {
247 return Ok(false);
248 }
249 let git_dir = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(git_dir.ok_or_raise_erased(|| {
250 not_found("The git directory must be provided to support `gitdir:` conditional includes")
251 })?));
252
253 let mut pattern_path = match check_interpolation_result(
254 err_on_interpolation_failure,
255 crate::Path::from(condition_path.to_owned()).interpolate(context),
256 )
257 .or_raise_erased(|| message("Could not interpolate conditional include path"))?
258 {
259 Some(path) => gix_path::into_bstr(path).into_owned(),
260 None => condition_path.to_owned(),
262 };
263 if pattern_path != condition_path {
265 pattern_path = gix_path::to_unix_separators_on_windows(pattern_path).into_owned();
266 }
267
268 if let Some(relative_pattern_path) = pattern_path.strip_prefix(b"./") {
269 if !err_on_missing_config_path && target_config_path.is_none() {
270 return Ok(false);
271 }
272 let parent_dir = target_config_path
273 .ok_or_raise_erased(|| {
274 not_found(
275 "Include paths from environment variables must not be relative as no config file path exists as root",
276 )
277 })?
278 .parent()
279 .expect("config path can never be /");
280 let mut joined_path = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(parent_dir)).into_owned();
281 joined_path.push(b'/');
282 joined_path.extend_from_slice(relative_pattern_path);
283 pattern_path = joined_path;
284 }
285
286 if pattern_path.iter().next() != Some(&(std::path::MAIN_SEPARATOR as u8))
288 && !gix_path::from_bstr(pattern_path.clone()).is_absolute()
289 {
290 pattern_path.insert_str(0, "**/");
291 }
292 if pattern_path.ends_with(b"/") {
293 pattern_path.push_str("**");
294 }
295
296 let match_mode = gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL | wildmatch_mode;
297 let is_match = gix_glob::wildmatch(pattern_path.as_bstr(), git_dir.as_bstr(), match_mode);
298 if is_match {
299 return Ok(true);
300 }
301
302 let expanded_git_dir = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(
303 gix_path::realpath(gix_path::from_byte_slice(&git_dir))
304 .or_raise_erased(|| message("Could not resolve the git directory to its real path"))?,
305 ));
306 Ok(gix_glob::wildmatch(
307 pattern_path.as_bstr(),
308 expanded_git_dir.as_ref(),
309 match_mode,
310 ))
311}
312
313fn check_interpolation_result(disable: bool, res: ExnResult<impl Into<PathBuf>>) -> ExnResult<Option<PathBuf>> {
314 if disable {
315 return res.map(|path| Some(path.into()));
316 }
317 match res {
318 Ok(good) => Ok(Some(good.into())),
319 Err(err) if err.is_validation() => Err(err),
320 Err(_) => Ok(None),
321 }
322}
323
324fn resolve_path(
325 path: crate::Path,
326 target_config_path: Option<&Path>,
327 includes::Options {
328 interpolate: context,
329 err_on_interpolation_failure,
330 err_on_missing_config_path,
331 ..
332 }: includes::Options<'_>,
333) -> ExnResult<Option<PathBuf>> {
334 let path = match check_interpolation_result(err_on_interpolation_failure, path.interpolate(context))
335 .or_raise_erased(|| message("Could not interpolate include path"))?
336 {
337 Some(p) => p,
338 None => return Ok(None),
339 };
340 let path: PathBuf = if path.is_relative() {
341 if !err_on_missing_config_path && target_config_path.is_none() {
342 return Ok(None);
343 }
344 target_config_path
345 .ok_or_raise_erased(|| {
346 not_found(
347 "Include paths from environment variables must not be relative as no config file path exists as root",
348 )
349 })?
350 .parent()
351 .expect("path is a config file which naturally lives in a directory")
352 .join(path)
353 } else {
354 path
355 };
356 Ok(Some(path))
357}
358
359mod types;
360pub use types::{Options, conditional};