diff options
Diffstat (limited to 'vendor/gix/src/open')
-rw-r--r-- | vendor/gix/src/open/mod.rs | 67 | ||||
-rw-r--r-- | vendor/gix/src/open/options.rs | 180 | ||||
-rw-r--r-- | vendor/gix/src/open/repository.rs | 345 |
3 files changed, 592 insertions, 0 deletions
diff --git a/vendor/gix/src/open/mod.rs b/vendor/gix/src/open/mod.rs new file mode 100644 index 000000000..77018f5a2 --- /dev/null +++ b/vendor/gix/src/open/mod.rs @@ -0,0 +1,67 @@ +use std::path::PathBuf; + +use crate::{bstr::BString, config, permission, Permissions}; + +/// The options used in [`ThreadSafeRepository::open_opts()`][crate::ThreadSafeRepository::open_opts()]. +/// +/// ### Replacement Objects for the object database +/// +/// The environment variables `GIT_REPLACE_REF_BASE` and `GIT_NO_REPLACE_OBJECTS` are mapped to `gitoxide.objects.replaceRefBase` +/// and `gitoxide.objects.noReplace` respectively and then interpreted exactly as their environment variable counterparts. +/// +/// Use [Permissions] to control which environment variables can be read, and config-overrides to control these values programmatically. +#[derive(Clone)] +pub struct Options { + pub(crate) object_store_slots: gix_odb::store::init::Slots, + /// Define what is allowed while opening a repository. + pub permissions: Permissions, + pub(crate) git_dir_trust: Option<gix_sec::Trust>, + /// Warning: this one is copied to to config::Cache - don't change it after repo open or keep in sync. + pub(crate) filter_config_section: Option<fn(&gix_config::file::Metadata) -> bool>, + pub(crate) lossy_config: Option<bool>, + pub(crate) lenient_config: bool, + pub(crate) bail_if_untrusted: bool, + pub(crate) api_config_overrides: Vec<BString>, + pub(crate) cli_config_overrides: Vec<BString>, + pub(crate) open_path_as_is: bool, + /// Internal to pass an already obtained CWD on to where it may also be used. This avoids the CWD being queried more than once per repo. + pub(crate) current_dir: Option<PathBuf>, +} + +/// The error returned by [`crate::open()`]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("Failed to load the git configuration")] + Config(#[from] config::Error), + #[error("\"{path}\" does not appear to be a git repository")] + NotARepository { + source: gix_discover::is_git::Error, + path: PathBuf, + }, + #[error(transparent)] + Io(#[from] std::io::Error), + #[error("The git directory at '{}' is considered unsafe as it's not owned by the current user.", .path.display())] + UnsafeGitDir { path: PathBuf }, + #[error(transparent)] + EnvironmentAccessDenied(#[from] permission::env_var::resource::Error), +} + +mod options; + +mod repository; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn size_of_options() { + let actual = std::mem::size_of::<Options>(); + let limit = 160; + assert!( + actual <= limit, + "{actual} <= {limit}: size shouldn't change without us knowing (on windows, it's bigger)" + ); + } +} diff --git a/vendor/gix/src/open/options.rs b/vendor/gix/src/open/options.rs new file mode 100644 index 000000000..fb648e3c2 --- /dev/null +++ b/vendor/gix/src/open/options.rs @@ -0,0 +1,180 @@ +use std::path::PathBuf; + +use super::{Error, Options}; +use crate::{bstr::BString, config, Permissions, ThreadSafeRepository}; + +impl Default for Options { + fn default() -> Self { + Options { + object_store_slots: Default::default(), + permissions: Default::default(), + git_dir_trust: None, + filter_config_section: None, + lossy_config: None, + lenient_config: true, + bail_if_untrusted: false, + open_path_as_is: false, + api_config_overrides: Vec::new(), + cli_config_overrides: Vec::new(), + current_dir: None, + } + } +} + +/// Instantiation +impl Options { + /// Options configured to prevent accessing anything else than the repository configuration file, prohibiting + /// accessing the environment or spreading beyond the git repository location. + pub fn isolated() -> Self { + Options::default().permissions(Permissions::isolated()) + } +} + +/// Generic modification +impl Options { + /// An adapter to allow calling any builder method on this instance despite only having a mutable reference. + pub fn modify(&mut self, f: impl FnOnce(Self) -> Self) { + *self = f(std::mem::take(self)); + } +} + +/// Builder methods +impl Options { + /// Apply the given configuration `values` like `init.defaultBranch=special` or `core.bool-implicit-true` in memory to as early + /// as the configuration is initialized to allow affecting the repository instantiation phase, both on disk or when opening. + /// The configuration is marked with [source API][gix_config::Source::Api]. + pub fn config_overrides(mut self, values: impl IntoIterator<Item = impl Into<BString>>) -> Self { + self.api_config_overrides = values.into_iter().map(Into::into).collect(); + self + } + + /// Set configuration values of the form `core.abbrev=5` or `remote.origin.url = foo` or `core.bool-implicit-true` for application + /// as CLI overrides to the repository configuration, marked with [source CLI][gix_config::Source::Cli]. + /// These are equivalent to CLI overrides passed with `-c` in `git`, for example. + pub fn cli_overrides(mut self, values: impl IntoIterator<Item = impl Into<BString>>) -> Self { + self.cli_config_overrides = values.into_iter().map(Into::into).collect(); + self + } + + /// Set the amount of slots to use for the object database. It's a value that doesn't need changes on the client, typically, + /// but should be controlled on the server. + pub fn object_store_slots(mut self, slots: gix_odb::store::init::Slots) -> Self { + self.object_store_slots = slots; + self + } + + // TODO: tests + /// Set the given permissions, which are typically derived by a `Trust` level. + pub fn permissions(mut self, permissions: Permissions) -> Self { + self.permissions = permissions; + self + } + + /// If `true`, default `false`, we will not modify the incoming path to open to assure it is a `.git` directory. + /// + /// If `false`, we will try to open the input directory as is, even though it doesn't appear to be a `git` repository + /// due to the lack of `.git` suffix or because its basename is not `.git` as in `worktree/.git`. + pub fn open_path_as_is(mut self, enable: bool) -> Self { + self.open_path_as_is = enable; + self + } + + /// Set the trust level of the `.git` directory we are about to open. + /// + /// This can be set manually to force trust even though otherwise it might + /// not be fully trusted, leading to limitations in how configuration files + /// are interpreted. + /// + /// If not called explicitly, it will be determined by looking at its + /// ownership via [`gix_sec::Trust::from_path_ownership()`]. + /// + /// # Security Warning + /// + /// Use with extreme care and only if it's absolutely known that the repository + /// is always controlled by the desired user. Using this capability _only_ saves + /// a permission check and only so if the [`open()`][Self::open()] method is used, + /// as opposed to discovery. + pub fn with(mut self, trust: gix_sec::Trust) -> Self { + self.git_dir_trust = trust.into(); + self + } + + /// If true, default false, and if the repository's trust level is not `Full` + /// (see [`with()`][Self::with()] for more), then the open operation will fail. + /// + /// Use this to mimic `git`s way of handling untrusted repositories. Note that `gitoxide` solves + /// this by not using configuration from untrusted sources and by generally being secured against + /// doctored input files which at worst could cause out-of-memory at the time of writing. + pub fn bail_if_untrusted(mut self, toggle: bool) -> Self { + self.bail_if_untrusted = toggle; + self + } + + /// Set the filter which determines if a configuration section can be used to read values from, + /// hence it returns true if it is eligible. + /// + /// The default filter selects sections whose trust level is [`full`][gix_sec::Trust::Full] or + /// whose source is not [`repository-local`][gix_config::source::Kind::Repository]. + pub fn filter_config_section(mut self, filter: fn(&gix_config::file::Metadata) -> bool) -> Self { + self.filter_config_section = Some(filter); + self + } + + /// By default, in release mode configuration will be read without retaining non-essential information like + /// comments or whitespace to optimize lookup performance. + /// + /// Some application might want to toggle this to false in they want to display or edit configuration losslessly + /// with all whitespace and comments included. + pub fn lossy_config(mut self, toggle: bool) -> Self { + self.lossy_config = toggle.into(); + self + } + + /// If set, default is false, invalid configuration values will cause an error even if these can safely be defaulted. + /// + /// This is recommended for all applications that prefer correctness over usability. + /// `git` itself defaults to strict configuration mode, flagging incorrect configuration immediately. + pub fn strict_config(mut self, toggle: bool) -> Self { + self.lenient_config = !toggle; + self + } + + /// Open a repository at `path` with the options set so far. + #[allow(clippy::result_large_err)] + pub fn open(self, path: impl Into<PathBuf>) -> Result<ThreadSafeRepository, Error> { + ThreadSafeRepository::open_opts(path, self) + } +} + +impl gix_sec::trust::DefaultForLevel for Options { + fn default_for_level(level: gix_sec::Trust) -> Self { + match level { + gix_sec::Trust::Full => Options { + object_store_slots: Default::default(), + permissions: Permissions::default_for_level(level), + git_dir_trust: gix_sec::Trust::Full.into(), + filter_config_section: Some(config::section::is_trusted), + lossy_config: None, + bail_if_untrusted: false, + lenient_config: true, + open_path_as_is: false, + api_config_overrides: Vec::new(), + cli_config_overrides: Vec::new(), + current_dir: None, + }, + gix_sec::Trust::Reduced => Options { + object_store_slots: gix_odb::store::init::Slots::Given(32), // limit resource usage + permissions: Permissions::default_for_level(level), + git_dir_trust: gix_sec::Trust::Reduced.into(), + filter_config_section: Some(config::section::is_trusted), + bail_if_untrusted: false, + lenient_config: true, + open_path_as_is: false, + lossy_config: None, + api_config_overrides: Vec::new(), + cli_config_overrides: Vec::new(), + current_dir: None, + }, + } + } +} diff --git a/vendor/gix/src/open/repository.rs b/vendor/gix/src/open/repository.rs new file mode 100644 index 000000000..85dd91da7 --- /dev/null +++ b/vendor/gix/src/open/repository.rs @@ -0,0 +1,345 @@ +#![allow(clippy::result_large_err)] +use std::{borrow::Cow, path::PathBuf}; + +use gix_features::threading::OwnShared; + +use super::{Error, Options}; +use crate::{ + config, + config::{ + cache::{interpolate_context, util::ApplyLeniency}, + tree::{gitoxide, Core, Key, Safe}, + }, + permission, Permissions, ThreadSafeRepository, +}; + +#[derive(Default, Clone)] +pub(crate) struct EnvironmentOverrides { + /// An override of the worktree typically from the environment, and overrides even worktree dirs set as parameter. + /// + /// This emulates the way git handles this override. + worktree_dir: Option<PathBuf>, + /// An override for the .git directory, typically from the environment. + /// + /// If set, the passed in `git_dir` parameter will be ignored in favor of this one. + git_dir: Option<PathBuf>, +} + +impl EnvironmentOverrides { + fn from_env() -> Result<Self, permission::env_var::resource::Error> { + let mut worktree_dir = None; + if let Some(path) = std::env::var_os(Core::WORKTREE.the_environment_override()) { + worktree_dir = PathBuf::from(path).into(); + } + let mut git_dir = None; + if let Some(path) = std::env::var_os("GIT_DIR") { + git_dir = PathBuf::from(path).into(); + } + Ok(EnvironmentOverrides { worktree_dir, git_dir }) + } +} + +impl ThreadSafeRepository { + /// Open a git repository at the given `path`, possibly expanding it to `path/.git` if `path` is a work tree dir. + pub fn open(path: impl Into<PathBuf>) -> Result<Self, Error> { + Self::open_opts(path, Options::default()) + } + + /// Open a git repository at the given `path`, possibly expanding it to `path/.git` if `path` is a work tree dir, and use + /// `options` for fine-grained control. + /// + /// Note that you should use [`crate::discover()`] if security should be adjusted by ownership. + pub fn open_opts(path: impl Into<PathBuf>, mut options: Options) -> Result<Self, Error> { + let (path, kind) = { + let path = path.into(); + let looks_like_git_dir = + path.ends_with(gix_discover::DOT_GIT_DIR) || path.extension() == Some(std::ffi::OsStr::new("git")); + let candidate = if !options.open_path_as_is && !looks_like_git_dir { + Cow::Owned(path.join(gix_discover::DOT_GIT_DIR)) + } else { + Cow::Borrowed(&path) + }; + match gix_discover::is_git(candidate.as_ref()) { + Ok(kind) => (candidate.into_owned(), kind), + Err(err) => { + if options.open_path_as_is || matches!(candidate, Cow::Borrowed(_)) { + return Err(Error::NotARepository { + source: err, + path: candidate.into_owned(), + }); + } + match gix_discover::is_git(&path) { + Ok(kind) => (path, kind), + Err(err) => return Err(Error::NotARepository { source: err, path }), + } + } + } + }; + let cwd = std::env::current_dir()?; + let (git_dir, worktree_dir) = gix_discover::repository::Path::from_dot_git_dir(path, kind, &cwd) + .expect("we have sanitized path with is_git()") + .into_repository_and_work_tree_directories(); + if options.git_dir_trust.is_none() { + options.git_dir_trust = gix_sec::Trust::from_path_ownership(&git_dir)?.into(); + } + options.current_dir = Some(cwd); + ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, options) + } + + /// Try to open a git repository in `fallback_directory` (can be worktree or `.git` directory) only if there is no override + /// from of the `gitdir` using git environment variables. + /// + /// Use the `trust_map` to apply options depending in the trust level for `directory` or the directory it's overridden with. + /// The `.git` directory whether given or computed is used for trust checks. + /// + /// Note that this will read various `GIT_*` environment variables to check for overrides, and is probably most useful when implementing + /// custom hooks. + // TODO: tests, with hooks, GIT_QUARANTINE for ref-log and transaction control (needs gix-sec support to remove write access in gix-ref) + // TODO: The following vars should end up as overrides of the respective configuration values (see gix-config). + // GIT_PROXY_SSL_CERT, GIT_PROXY_SSL_KEY, GIT_PROXY_SSL_CERT_PASSWORD_PROTECTED. + // GIT_PROXY_SSL_CAINFO, GIT_SSL_CIPHER_LIST, GIT_HTTP_MAX_REQUESTS, GIT_CURL_FTP_NO_EPSV, + pub fn open_with_environment_overrides( + fallback_directory: impl Into<PathBuf>, + trust_map: gix_sec::trust::Mapping<Options>, + ) -> Result<Self, Error> { + let overrides = EnvironmentOverrides::from_env()?; + let (path, path_kind): (PathBuf, _) = match overrides.git_dir { + Some(git_dir) => gix_discover::is_git(&git_dir) + .map_err(|err| Error::NotARepository { + source: err, + path: git_dir.clone(), + }) + .map(|kind| (git_dir, kind))?, + None => { + let fallback_directory = fallback_directory.into(); + gix_discover::is_git(&fallback_directory) + .map_err(|err| Error::NotARepository { + source: err, + path: fallback_directory.clone(), + }) + .map(|kind| (fallback_directory, kind))? + } + }; + + let cwd = std::env::current_dir()?; + let (git_dir, worktree_dir) = gix_discover::repository::Path::from_dot_git_dir(path, path_kind, &cwd) + .expect("we have sanitized path with is_git()") + .into_repository_and_work_tree_directories(); + let worktree_dir = worktree_dir.or(overrides.worktree_dir); + + let git_dir_trust = gix_sec::Trust::from_path_ownership(&git_dir)?; + let mut options = trust_map.into_value_by_level(git_dir_trust); + options.current_dir = Some(cwd); + ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, options) + } + + pub(crate) fn open_from_paths( + git_dir: PathBuf, + mut worktree_dir: Option<PathBuf>, + options: Options, + ) -> Result<Self, Error> { + let Options { + git_dir_trust, + object_store_slots, + filter_config_section, + lossy_config, + lenient_config, + bail_if_untrusted, + open_path_as_is: _, + permissions: Permissions { ref env, config }, + ref api_config_overrides, + ref cli_config_overrides, + ref current_dir, + } = options; + let current_dir = current_dir.as_deref().expect("BUG: current_dir must be set by caller"); + let git_dir_trust = git_dir_trust.expect("trust must be been determined by now"); + + // TODO: assure we handle the worktree-dir properly as we can have config per worktree with an extension. + // This would be something read in later as have to first check for extensions. Also this means + // that each worktree, even if accessible through this instance, has to come in its own Repository instance + // as it may have its own configuration. That's fine actually. + let common_dir = gix_discover::path::from_plain_file(git_dir.join("commondir")) + .transpose()? + .map(|cd| git_dir.join(cd)); + let common_dir_ref = common_dir.as_deref().unwrap_or(&git_dir); + + let repo_config = config::cache::StageOne::new( + common_dir_ref, + git_dir.as_ref(), + git_dir_trust, + lossy_config, + lenient_config, + )?; + let mut refs = { + let reflog = repo_config.reflog.unwrap_or(gix_ref::store::WriteReflog::Disable); + let object_hash = repo_config.object_hash; + match &common_dir { + Some(common_dir) => crate::RefStore::for_linked_worktree(&git_dir, common_dir, reflog, object_hash), + None => crate::RefStore::at(&git_dir, reflog, object_hash), + } + }; + let head = refs.find("HEAD").ok(); + let git_install_dir = crate::path::install_dir().ok(); + let home = std::env::var_os("HOME") + .map(PathBuf::from) + .and_then(|home| env.home.check_opt(home)); + + let mut filter_config_section = filter_config_section.unwrap_or(config::section::is_trusted); + let config = config::Cache::from_stage_one( + repo_config, + common_dir_ref, + head.as_ref().and_then(|head| head.target.try_name()), + filter_config_section, + git_install_dir.as_deref(), + home.as_deref(), + env.clone(), + config, + lenient_config, + api_config_overrides, + cli_config_overrides, + )?; + + if bail_if_untrusted && git_dir_trust != gix_sec::Trust::Full { + check_safe_directories(&git_dir, git_install_dir.as_deref(), home.as_deref(), &config)?; + } + + // core.worktree might be used to overwrite the worktree directory + if !config.is_bare { + if let Some(wt) = config + .resolved + .path_filter("core", None, Core::WORKTREE.name, &mut filter_config_section) + { + let wt_path = wt + .interpolate(interpolate_context(git_install_dir.as_deref(), home.as_deref())) + .map_err(config::Error::PathInterpolation)?; + worktree_dir = { + gix_path::normalize(git_dir.join(wt_path), current_dir) + .and_then(|wt| wt.as_ref().is_dir().then(|| wt.into_owned())) + } + } + } + + match worktree_dir { + None if !config.is_bare => { + worktree_dir = Some(git_dir.parent().expect("parent is always available").to_owned()); + } + Some(_) => { + // note that we might be bare even with a worktree directory - work trees don't have to be + // the parent of a non-bare repository. + } + None => {} + } + + refs.write_reflog = config::cache::util::reflog_or_default(config.reflog, worktree_dir.is_some()); + let replacements = replacement_objects_refs_prefix(&config.resolved, lenient_config, filter_config_section)? + .and_then(|prefix| { + let platform = refs.iter().ok()?; + let iter = platform.prefixed(&prefix).ok()?; + let prefix = prefix.to_str()?; + let replacements = iter + .filter_map(Result::ok) + .filter_map(|r: gix_ref::Reference| { + let target = r.target.try_id()?.to_owned(); + let source = + gix_hash::ObjectId::from_hex(r.name.as_bstr().strip_prefix(prefix.as_bytes())?).ok()?; + Some((source, target)) + }) + .collect::<Vec<_>>(); + Some(replacements) + }) + .unwrap_or_default(); + + Ok(ThreadSafeRepository { + objects: OwnShared::new(gix_odb::Store::at_opts( + common_dir_ref.join("objects"), + replacements, + gix_odb::store::init::Options { + slots: object_store_slots, + object_hash: config.object_hash, + use_multi_pack_index: config.use_multi_pack_index, + current_dir: current_dir.to_owned().into(), + }, + )?), + common_dir, + refs, + work_tree: worktree_dir, + config, + // used when spawning new repositories off this one when following worktrees + linked_worktree_options: options, + index: gix_features::fs::MutableSnapshot::new().into(), + }) + } +} + +// TODO: tests +fn replacement_objects_refs_prefix( + config: &gix_config::File<'static>, + lenient: bool, + mut filter_config_section: fn(&gix_config::file::Metadata) -> bool, +) -> Result<Option<PathBuf>, Error> { + let is_disabled = config + .boolean_filter_by_key("gitoxide.objects.noReplace", &mut filter_config_section) + .map(|b| gitoxide::Objects::NO_REPLACE.enrich_error(b)) + .transpose() + .with_leniency(lenient) + .map_err(config::Error::ConfigBoolean)? + .unwrap_or_default(); + + if is_disabled { + return Ok(None); + } + + let ref_base = gix_path::from_bstr({ + let key = "gitoxide.objects.replaceRefBase"; + debug_assert_eq!(gitoxide::Objects::REPLACE_REF_BASE.logical_name(), key); + config + .string_filter_by_key(key, &mut filter_config_section) + .unwrap_or_else(|| Cow::Borrowed("refs/replace/".into())) + }) + .into_owned(); + Ok(ref_base.into()) +} + +fn check_safe_directories( + git_dir: &std::path::Path, + git_install_dir: Option<&std::path::Path>, + home: Option<&std::path::Path>, + config: &config::Cache, +) -> Result<(), Error> { + let mut is_safe = false; + let git_dir = match gix_path::realpath(git_dir) { + Ok(p) => p, + Err(_) => git_dir.to_owned(), + }; + for safe_dir in config + .resolved + .strings_filter("safe", None, Safe::DIRECTORY.name, &mut Safe::directory_filter) + .unwrap_or_default() + { + if safe_dir.as_ref() == "*" { + is_safe = true; + continue; + } + if safe_dir.is_empty() { + is_safe = false; + continue; + } + if !is_safe { + let safe_dir = match gix_config::Path::from(std::borrow::Cow::Borrowed(safe_dir.as_ref())) + .interpolate(interpolate_context(git_install_dir, home)) + { + Ok(path) => path, + Err(_) => gix_path::from_bstr(safe_dir), + }; + if safe_dir == git_dir { + is_safe = true; + continue; + } + } + } + if is_safe { + Ok(()) + } else { + Err(Error::UnsafeGitDir { path: git_dir }) + } +} |