gix_config/file/includes/
mod.rs1use std::{
2 borrow::Cow,
3 path::{Path, PathBuf},
4};
5
6use bstr::{BStr, BString, ByteSlice, ByteVec};
7use gix_features::threading::OwnShared;
8use gix_ref::Category;
9
10use crate::{
11 file,
12 file::{includes, init, Metadata, SectionId},
13 path, File,
14};
15
16impl File<'static> {
17 pub fn resolve_includes(&mut self, options: init::Options<'_>) -> Result<(), Error> {
37 if options.includes.max_depth == 0 {
38 return Ok(());
39 }
40 let mut buf = Vec::new();
41 resolve(self, &mut buf, options)
42 }
43}
44
45pub(crate) fn resolve(config: &mut File<'static>, buf: &mut Vec<u8>, options: init::Options<'_>) -> Result<(), Error> {
46 resolve_includes_recursive(None, config, 0, buf, options)
47}
48
49fn resolve_includes_recursive(
50 search_config: Option<&File<'static>>,
51 target_config: &mut File<'static>,
52 depth: u8,
53 buf: &mut Vec<u8>,
54 options: init::Options<'_>,
55) -> Result<(), Error> {
56 if depth == options.includes.max_depth {
57 return if options.includes.err_on_max_depth_exceeded {
58 Err(Error::IncludeDepthExceeded {
59 max_depth: options.includes.max_depth,
60 })
61 } else {
62 Ok(())
63 };
64 }
65
66 for id in target_config.section_order.clone().into_iter() {
67 let section = &target_config.sections[&id];
68 let header = §ion.header;
69 let header_name = header.name.as_ref();
70 let mut paths = None;
71 if header_name == "include" && header.subsection_name.is_none() {
72 paths = Some(gather_paths(section, id));
73 } else if header_name == "includeIf" {
74 if let Some(condition) = &header.subsection_name {
75 let target_config_path = section.meta.path.as_deref();
76 if include_condition_match(
77 condition.as_ref(),
78 target_config_path,
79 search_config.unwrap_or(target_config),
80 options.includes,
81 )? {
82 paths = Some(gather_paths(section, id));
83 }
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<'static>,
96 depth: u8,
97 options: init::Options<'_>,
98 buf: &mut Vec<u8>,
99) -> Result<(), Error> {
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).map_err(|err| Error::Io {
114 source: err,
115 path: config_path.to_owned(),
116 })?,
117 buf,
118 )
119 .map_err(Error::CopyBuffer)?;
120 let config_meta = Metadata {
121 path: Some(config_path),
122 trust: meta.trust,
123 level: meta.level + 1,
124 source: meta.source,
125 };
126 let no_follow_options = init::Options {
127 includes: includes::Options::no_follow(),
128 ..options
129 };
130
131 let mut include_config =
132 File::from_bytes_owned(buf, config_meta, no_follow_options).map_err(|err| match err {
133 init::Error::Parse(err) => Error::Parse(err),
134 init::Error::Interpolate(err) => Error::Interpolate(err),
135 init::Error::Includes(_) => unreachable!("BUG: {:?} not possible due to no-follow options", err),
136 })?;
137 resolve_includes_recursive(Some(target_config), &mut include_config, depth + 1, buf, options)?;
138
139 target_config.append_or_insert(include_config, Some(section_id));
140 }
141 Ok(())
142}
143
144fn gather_paths(section: &file::Section<'_>, id: SectionId) -> Vec<(SectionId, crate::Path<'static>)> {
145 section
146 .body
147 .values("path")
148 .into_iter()
149 .map(|path| (id, crate::Path::from(Cow::Owned(path.into_owned()))))
150 .collect()
151}
152
153fn include_condition_match(
154 condition: &BStr,
155 target_config_path: Option<&Path>,
156 search_config: &File<'static>,
157 options: Options<'_>,
158) -> Result<bool, Error> {
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 = if condition.ends_with(b"/") {
219 let mut condition: BString = condition.into();
220 condition.push_str("**");
221 Cow::Owned(condition)
222 } else {
223 condition.into()
224 };
225
226 gix_glob::wildmatch(
227 condition.as_ref(),
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) -> Result<bool, Error> {
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(Error::MissingGitDir)?));
250
251 let mut pattern_path: Cow<'_, _> = {
252 let path = match check_interpolation_result(
253 err_on_interpolation_failure,
254 crate::Path::from(Cow::Borrowed(condition_path)).interpolate(context),
255 )? {
256 Some(p) => p,
257 None => return Ok(false),
258 };
259 gix_path::into_bstr(path).into_owned().into()
260 };
261 if pattern_path != condition_path {
263 pattern_path = gix_path::to_unix_separators_on_windows(pattern_path);
264 }
265
266 if let Some(relative_pattern_path) = pattern_path.strip_prefix(b"./") {
267 if !err_on_missing_config_path && target_config_path.is_none() {
268 return Ok(false);
269 }
270 let parent_dir = target_config_path
271 .ok_or(Error::MissingConfigPath)?
272 .parent()
273 .expect("config path can never be /");
274 let mut joined_path = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(parent_dir)).into_owned();
275 joined_path.push(b'/');
276 joined_path.extend_from_slice(relative_pattern_path);
277 pattern_path = joined_path.into();
278 }
279
280 if pattern_path.iter().next() != Some(&(std::path::MAIN_SEPARATOR as u8))
282 && !gix_path::from_bstr(pattern_path.clone()).is_absolute()
283 {
284 let mut prefixed = pattern_path.into_owned();
285 prefixed.insert_str(0, "**/");
286 pattern_path = prefixed.into();
287 }
288 if pattern_path.ends_with(b"/") {
289 let mut suffixed = pattern_path.into_owned();
290 suffixed.push_str("**");
291 pattern_path = suffixed.into();
292 }
293
294 let match_mode = gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL | wildmatch_mode;
295 let is_match = gix_glob::wildmatch(pattern_path.as_bstr(), git_dir.as_bstr(), match_mode);
296 if is_match {
297 return Ok(true);
298 }
299
300 let expanded_git_dir = gix_path::into_bstr(gix_path::realpath(gix_path::from_byte_slice(&git_dir))?);
301 Ok(gix_glob::wildmatch(
302 pattern_path.as_bstr(),
303 expanded_git_dir.as_bstr(),
304 match_mode,
305 ))
306}
307
308fn check_interpolation_result(
309 disable: bool,
310 res: Result<Cow<'_, std::path::Path>, path::interpolate::Error>,
311) -> Result<Option<Cow<'_, std::path::Path>>, path::interpolate::Error> {
312 if disable {
313 return res.map(Some);
314 }
315 match res {
316 Ok(good) => Ok(good.into()),
317 Err(err) => match err {
318 path::interpolate::Error::Missing { .. } | path::interpolate::Error::UserInterpolationUnsupported => {
319 Ok(None)
320 }
321 path::interpolate::Error::UsernameConversion(_) | path::interpolate::Error::Utf8Conversion { .. } => {
322 Err(err)
323 }
324 },
325 }
326}
327
328fn resolve_path(
329 path: crate::Path<'_>,
330 target_config_path: Option<&Path>,
331 includes::Options {
332 interpolate: context,
333 err_on_interpolation_failure,
334 err_on_missing_config_path,
335 ..
336 }: includes::Options<'_>,
337) -> Result<Option<PathBuf>, Error> {
338 let path = match check_interpolation_result(err_on_interpolation_failure, path.interpolate(context))? {
339 Some(p) => p,
340 None => return Ok(None),
341 };
342 let path: PathBuf = if path.is_relative() {
343 if !err_on_missing_config_path && target_config_path.is_none() {
344 return Ok(None);
345 }
346 target_config_path
347 .ok_or(Error::MissingConfigPath)?
348 .parent()
349 .expect("path is a config file which naturally lives in a directory")
350 .join(path)
351 } else {
352 path.into()
353 };
354 Ok(Some(path))
355}
356
357mod types;
358pub use types::{conditional, Error, Options};