diff options
Diffstat (limited to 'vendor/gix-config/src/file/includes/mod.rs')
-rw-r--r-- | vendor/gix-config/src/file/includes/mod.rs | 319 |
1 files changed, 319 insertions, 0 deletions
diff --git a/vendor/gix-config/src/file/includes/mod.rs b/vendor/gix-config/src/file/includes/mod.rs new file mode 100644 index 000000000..8fd92725f --- /dev/null +++ b/vendor/gix-config/src/file/includes/mod.rs @@ -0,0 +1,319 @@ +use std::{ + borrow::Cow, + path::{Path, PathBuf}, +}; + +use bstr::{BStr, BString, ByteSlice, ByteVec}; +use gix_features::threading::OwnShared; +use gix_ref::Category; + +use crate::{ + file, + file::{includes, init, Metadata, SectionId}, + path, File, +}; + +impl File<'static> { + /// Traverse all `include` and `includeIf` directives found in this instance and follow them, loading the + /// referenced files from their location and adding their content right past the value that included them. + /// + /// # Limitations + /// + /// - Note that this method is _not idempotent_ and calling it multiple times will resolve includes multiple + /// times. It's recommended use is as part of a multi-step bootstrapping which needs fine-grained control, + /// and unless that's given one should prefer one of the other ways of initialization that resolve includes + /// at the right time. + /// - included values are added after the _section_ that included them, not directly after the value. This is + /// a deviation from how git does it, as it technically adds new value right after the include path itself, + /// technically 'splitting' the section. This can only make a difference if the `include` section also has values + /// which later overwrite portions of the included file, which seems unusual as these would be related to `includes`. + /// We can fix this by 'splitting' the include section if needed so the included sections are put into the right place. + pub fn resolve_includes(&mut self, options: init::Options<'_>) -> Result<(), Error> { + if options.includes.max_depth == 0 { + return Ok(()); + } + let mut buf = Vec::new(); + resolve(self, &mut buf, options) + } +} + +pub(crate) fn resolve(config: &mut File<'static>, buf: &mut Vec<u8>, options: init::Options<'_>) -> Result<(), Error> { + resolve_includes_recursive(config, 0, buf, options) +} + +fn resolve_includes_recursive( + target_config: &mut File<'static>, + depth: u8, + buf: &mut Vec<u8>, + options: init::Options<'_>, +) -> Result<(), Error> { + if depth == options.includes.max_depth { + return if options.includes.err_on_max_depth_exceeded { + Err(Error::IncludeDepthExceeded { + max_depth: options.includes.max_depth, + }) + } else { + Ok(()) + }; + } + + let mut section_ids_and_include_paths = Vec::new(); + for (id, section) in target_config + .section_order + .iter() + .map(|id| (*id, &target_config.sections[id])) + { + let header = §ion.header; + let header_name = header.name.as_ref(); + if header_name == "include" && header.subsection_name.is_none() { + detach_include_paths(&mut section_ids_and_include_paths, section, id) + } else if header_name == "includeIf" { + if let Some(condition) = &header.subsection_name { + let target_config_path = section.meta.path.as_deref(); + if include_condition_match(condition.as_ref(), target_config_path, options.includes)? { + detach_include_paths(&mut section_ids_and_include_paths, section, id) + } + } + } + } + + append_followed_includes_recursively(section_ids_and_include_paths, target_config, depth, options, buf) +} + +fn append_followed_includes_recursively( + section_ids_and_include_paths: Vec<(SectionId, crate::Path<'_>)>, + target_config: &mut File<'static>, + depth: u8, + options: init::Options<'_>, + buf: &mut Vec<u8>, +) -> Result<(), Error> { + for (section_id, config_path) in section_ids_and_include_paths { + let meta = OwnShared::clone(&target_config.sections[§ion_id].meta); + let target_config_path = meta.path.as_deref(); + let config_path = match resolve_path(config_path, target_config_path, options.includes)? { + Some(p) => p, + None => continue, + }; + if !config_path.is_file() { + continue; + } + + buf.clear(); + std::io::copy(&mut std::fs::File::open(&config_path)?, buf)?; + let config_meta = Metadata { + path: Some(config_path), + trust: meta.trust, + level: meta.level + 1, + source: meta.source, + }; + let no_follow_options = init::Options { + includes: includes::Options::no_follow(), + ..options + }; + + let mut include_config = + File::from_bytes_owned(buf, config_meta, no_follow_options).map_err(|err| match err { + init::Error::Parse(err) => Error::Parse(err), + init::Error::Interpolate(err) => Error::Interpolate(err), + init::Error::Includes(_) => unreachable!("BUG: {:?} not possible due to no-follow options", err), + })?; + resolve_includes_recursive(&mut include_config, depth + 1, buf, options)?; + + target_config.append_or_insert(include_config, Some(section_id)); + } + Ok(()) +} + +fn detach_include_paths( + include_paths: &mut Vec<(SectionId, crate::Path<'static>)>, + section: &file::Section<'_>, + id: SectionId, +) { + include_paths.extend( + section + .body + .values("path") + .into_iter() + .map(|path| (id, crate::Path::from(Cow::Owned(path.into_owned())))), + ) +} + +fn include_condition_match( + condition: &BStr, + target_config_path: Option<&Path>, + options: Options<'_>, +) -> Result<bool, Error> { + let mut tokens = condition.splitn(2, |b| *b == b':'); + let (prefix, condition) = match (tokens.next(), tokens.next()) { + (Some(a), Some(b)) => (a, b), + _ => return Ok(false), + }; + let condition = condition.as_bstr(); + match prefix { + b"gitdir" => gitdir_matches( + condition, + target_config_path, + options, + gix_glob::wildmatch::Mode::empty(), + ), + b"gitdir/i" => gitdir_matches( + condition, + target_config_path, + options, + gix_glob::wildmatch::Mode::IGNORE_CASE, + ), + b"onbranch" => Ok(onbranch_matches(condition, options.conditional).is_some()), + _ => Ok(false), + } +} + +fn onbranch_matches( + condition: &BStr, + conditional::Context { branch_name, .. }: conditional::Context<'_>, +) -> Option<()> { + let branch_name = branch_name?; + let (_, branch_name) = branch_name + .category_and_short_name() + .filter(|(cat, _)| *cat == Category::LocalBranch)?; + + let condition = if condition.ends_with(b"/") { + let mut condition: BString = condition.into(); + condition.push_str("**"); + Cow::Owned(condition) + } else { + condition.into() + }; + + gix_glob::wildmatch( + condition.as_ref(), + branch_name, + gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL, + ) + .then_some(()) +} + +fn gitdir_matches( + condition_path: &BStr, + target_config_path: Option<&Path>, + Options { + conditional: conditional::Context { git_dir, .. }, + interpolate: context, + err_on_interpolation_failure, + err_on_missing_config_path, + .. + }: Options<'_>, + wildmatch_mode: gix_glob::wildmatch::Mode, +) -> Result<bool, Error> { + if !err_on_interpolation_failure && git_dir.is_none() { + return Ok(false); + } + let git_dir = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(git_dir.ok_or(Error::MissingGitDir)?)); + + let mut pattern_path: Cow<'_, _> = { + let path = match check_interpolation_result( + err_on_interpolation_failure, + crate::Path::from(Cow::Borrowed(condition_path)).interpolate(context), + )? { + Some(p) => p, + None => return Ok(false), + }; + gix_path::into_bstr(path).into_owned().into() + }; + // NOTE: yes, only if we do path interpolation will the slashes be forced to unix separators on windows + if pattern_path != condition_path { + pattern_path = gix_path::to_unix_separators_on_windows(pattern_path); + } + + if let Some(relative_pattern_path) = pattern_path.strip_prefix(b"./") { + if !err_on_missing_config_path && target_config_path.is_none() { + return Ok(false); + } + let parent_dir = target_config_path + .ok_or(Error::MissingConfigPath)? + .parent() + .expect("config path can never be /"); + let mut joined_path = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(parent_dir)).into_owned(); + joined_path.push(b'/'); + joined_path.extend_from_slice(relative_pattern_path); + pattern_path = joined_path.into(); + } + + // NOTE: this special handling of leading backslash is needed to do it like git does + if pattern_path.iter().next() != Some(&(std::path::MAIN_SEPARATOR as u8)) + && !gix_path::from_bstr(pattern_path.clone()).is_absolute() + { + let mut prefixed = pattern_path.into_owned(); + prefixed.insert_str(0, "**/"); + pattern_path = prefixed.into() + } + if pattern_path.ends_with(b"/") { + let mut suffixed = pattern_path.into_owned(); + suffixed.push_str("**"); + pattern_path = suffixed.into(); + } + + let match_mode = gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL | wildmatch_mode; + let is_match = gix_glob::wildmatch(pattern_path.as_bstr(), git_dir.as_bstr(), match_mode); + if is_match { + return Ok(true); + } + + let expanded_git_dir = gix_path::into_bstr(gix_path::realpath(gix_path::from_byte_slice(&git_dir))?); + Ok(gix_glob::wildmatch( + pattern_path.as_bstr(), + expanded_git_dir.as_bstr(), + match_mode, + )) +} + +fn check_interpolation_result( + disable: bool, + res: Result<Cow<'_, std::path::Path>, path::interpolate::Error>, +) -> Result<Option<Cow<'_, std::path::Path>>, path::interpolate::Error> { + if disable { + return res.map(Some); + } + match res { + Ok(good) => Ok(good.into()), + Err(err) => match err { + path::interpolate::Error::Missing { .. } | path::interpolate::Error::UserInterpolationUnsupported => { + Ok(None) + } + path::interpolate::Error::UsernameConversion(_) | path::interpolate::Error::Utf8Conversion { .. } => { + Err(err) + } + }, + } +} + +fn resolve_path( + path: crate::Path<'_>, + target_config_path: Option<&Path>, + includes::Options { + interpolate: context, + err_on_interpolation_failure, + err_on_missing_config_path, + .. + }: includes::Options<'_>, +) -> Result<Option<PathBuf>, Error> { + let path = match check_interpolation_result(err_on_interpolation_failure, path.interpolate(context))? { + Some(p) => p, + None => return Ok(None), + }; + let path: PathBuf = if path.is_relative() { + if !err_on_missing_config_path && target_config_path.is_none() { + return Ok(None); + } + target_config_path + .ok_or(Error::MissingConfigPath)? + .parent() + .expect("path is a config file which naturally lives in a directory") + .join(path) + } else { + path.into() + }; + Ok(Some(path)) +} + +mod types; +pub use types::{conditional, Error, Options}; |