From f3d5a69bbe0ad14502ce617dc580cc2aa481bb0a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 14 May 2024 09:43:50 +0200 Subject: [PATCH 01/50] mark safety-related core-flags as planned --- src/plumbing/progress.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plumbing/progress.rs b/src/plumbing/progress.rs index 28215273d99..09b85a66e7c 100644 --- a/src/plumbing/progress.rs +++ b/src/plumbing/progress.rs @@ -96,11 +96,11 @@ static GIT_CONFIG: &[Record] = &[ }, Record { config: "core.protectHFS", - usage: Planned("relevant for checkout on MacOS") + usage: Planned("relevant for checkout on MacOS, and possibly on networked drives") }, Record { config: "core.protectNTFS", - usage: NotPlanned("lack of demand") + usage: Planned("relevant for checkout on Windows, and possibly networked drives") }, Record { config: "core.sparseCheckout", From 0d78db2440c3866bfa972c8773aa7d8e7b245f2e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 14 May 2024 09:43:19 +0200 Subject: [PATCH 02/50] add validation for path components and tree-names ---- Note that this commit also streamlines obtaininig a relative path for a directory, which previously could panic. --- gix-fs/src/stack.rs | 40 ++- gix-fs/tests/stack/mod.rs | 359 ++++++++++++++++++++- gix-worktree/src/stack/delegate.rs | 28 +- gix-worktree/src/stack/state/attributes.rs | 3 +- 4 files changed, 379 insertions(+), 51 deletions(-) diff --git a/gix-fs/src/stack.rs b/gix-fs/src/stack.rs index 5d3dfeccd34..25b4cde7f31 100644 --- a/gix-fs/src/stack.rs +++ b/gix-fs/src/stack.rs @@ -1,4 +1,4 @@ -use std::path::{Path, PathBuf}; +use std::path::{Component, Path, PathBuf}; use crate::Stack; @@ -22,20 +22,22 @@ impl Stack { /// A delegate for use in a [`Stack`]. pub trait Delegate { - /// Called whenever we push a directory on top of the stack, after the fact. + /// Called whenever we push a directory on top of the stack, and after the respective call to [`push()`](Self::push). /// - /// It is also called if the currently acted on path is a directory in itself. - /// Use `stack.current()` to see the directory. + /// It is only called if the currently acted on path is a directory in itself, which is determined by knowing + /// that it's not the last component fo the path. + /// Use [`Stack::current()`] to see the directory. fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()>; - /// Called after any component was pushed, with the path available at `stack.current()`. + /// Called after any component was pushed, with the path available at [`Stack::current()`]. /// - /// `is_last_component` is true if the path is completely built. + /// `is_last_component` is `true` if the path is completely built, which typically means it's not a directory. fn push(&mut self, is_last_component: bool, stack: &Stack) -> std::io::Result<()>; /// Called right after a directory-component was popped off the stack. /// - /// Use it to pop information off internal data structures. + /// Use it to pop information off internal data structures. Note that no equivalent call exists for popping + /// the file-component. fn pop_directory(&mut self); } @@ -58,12 +60,17 @@ impl Stack { /// The full path to `relative` will be returned along with the data returned by `push_comp`. /// Note that this only works correctly for the delegate's `push_directory()` and `pop_directory()` methods if /// `relative` paths are terminal, so point to their designated file or directory. + /// The path is also expected to be normalized, and should not contain extra separators, and must not contain `..` + /// or have leading or trailing slashes (or additionally backslashes on Windows). pub fn make_relative_path_current(&mut self, relative: &Path, delegate: &mut dyn Delegate) -> std::io::Result<()> { - debug_assert!( - relative.is_relative(), - "only index paths are handled correctly here, must be relative" - ); - + if relative.as_os_str().is_empty() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "empty inputs are not allowed", + )); + } + // TODO: prevent leading or trailing slashes, on Windows also backslashes. + // prevent leading backslashes on Windows as they are strange if self.valid_components == 0 { delegate.push_directory(self)?; } @@ -95,6 +102,15 @@ impl Stack { } while let Some(comp) = components.next() { + if !matches!(comp, Component::Normal(_)) { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "Input path \"{}\" contains relative or absolute components", + relative.display() + ), + )); + } let is_last_component = components.peek().is_none(); self.current_is_directory = !is_last_component; self.current.push(comp); diff --git a/gix-fs/tests/stack/mod.rs b/gix-fs/tests/stack/mod.rs index 5e122cdb007..61f3d0b2459 100644 --- a/gix-fs/tests/stack/mod.rs +++ b/gix-fs/tests/stack/mod.rs @@ -26,6 +26,238 @@ impl gix_fs::stack::Delegate for Record { } } +fn p(s: &str) -> &Path { + s.as_ref() +} + +/// Just to learn the specialities of `Path::join()`, which boils down to `Path::push(component)`. +#[test] +#[cfg(windows)] +fn path_join_handling() { + let absolute = p("/absolute"); + assert!( + absolute.is_relative(), + "on Windows, absolute linux paths are considered relative" + ); + let bs_absolute = p("\\absolute"); + assert!( + absolute.is_relative(), + "on Windows, strange single-backslash paths are relative" + ); + assert_eq!( + p("relative").join(absolute), + absolute, + "relative + absolute = absolute - however, they kind of act like they are absolute in conjunction with relative base paths" + ); + assert_eq!( + p("relative").join(bs_absolute), + bs_absolute, + "relative + absolute = absolute - backslashes aren't special here, and it just acts like it's absolute" + ); + + assert_eq!( + p("c:").join("relative"), + p("c:relative"), + "absolute + relative = strange joined result with missing slash - but that shouldn't usually happen" + ); + assert_eq!( + p("c:\\").join("relative"), + p("c:\\relative"), + "absolute + relative = joined result" + ); + + assert_eq!( + p("\\\\?\\base").join(absolute), + p("\\\\?\\base\\absolute"), + "absolute1 + absolute2 = joined result with backslash" + ); + assert_eq!( + p("\\\\?\\base").join(bs_absolute), + p("\\\\?\\base\\absolute"), + "absolute1 + absolute2 = joined result" + ); + + assert_eq!( + p("/").join("C:"), + p("C:"), + "unix-absolute + win-absolute = win-absolute" + ); + assert_eq!( + p("/").join("C:/"), + p("C:\\"), + "unix-absolute + win-absolute = win-result, strangely enough it changed the trailing slash to backslash, so better not have trailing slashes" + ); + assert_eq!( + p("/").join("C:\\"), + p("C:\\"), + "unix-absolute + win-absolute = win-result" + ); + assert_eq!( + p("relative").join("C:"), + p("C:"), + "relative + win-absolute = win-result" + ); + + assert_eq!( + p("/").join("\\\\localhost"), + p("\\localhost"), + "unix-absolute + win-absolute-unc = win-absolute-unc" + ); + assert_eq!( + p("relative").join("\\\\localhost"), + p("\\\\localhost"), + "relative + win-absolute-unc = win-absolute-unc" + ); +} + +/// Just to learn the specialities of `Path::join()`, which boils down to `Path::push(component)`. +#[test] +#[cfg(not(windows))] +fn path_join_handling() { + assert_eq!( + p("relative").join("/absolute"), + p("/absolute"), + "relative + absolute = absolute" + ); + + assert_eq!( + p("/").join("relative"), + p("/relative"), + "absolute + relative = joined result" + ); + + assert_eq!( + p("/").join("/absolute"), + p("/absolute"), + "absolute1 + absolute2 = absolute2" + ); + + assert_eq!(p("/").join("C:"), p("/C:"), "absolute + win-absolute = joined result"); + assert_eq!(p("/").join("C:/"), p("/C:/"), "absolute + win-absolute = joined result"); + assert_eq!( + p("/").join("C:\\"), + p("/C:\\"), + "absolute + win-absolute = joined result" + ); + assert_eq!( + p("relative").join("C:"), + p("relative/C:"), + "relative + win-absolute = joined result" + ); + + assert_eq!( + p("/").join("\\localhost"), + p("/\\localhost"), + "absolute + win-absolute-unc = joined result" + ); + assert_eq!( + p("relative").join("\\localhost"), + p("relative/\\localhost"), + "relative + win-absolute-unc = joined result" + ); +} + +#[test] +fn relative_components_are_invalid() { + let root = PathBuf::from("."); + let mut s = Stack::new(root.clone()); + + let mut r = Record::default(); + let err = s.make_relative_path_current("a/..".as_ref(), &mut r).unwrap_err(); + assert_eq!( + err.to_string(), + format!( + "Input path {input:?} contains relative or absolute components", + input = "a/.." + ) + ); + + s.make_relative_path_current("a/./b".as_ref(), &mut r) + .expect("dot is ignored"); + assert_eq!( + r, + Record { + push_dir: 2, + dirs: vec![".".into(), "./a".into()], + push: 2, + }, + "The `a` directory is pushed, and the leaf, for a total of 2 pushes" + ); + assert_eq!( + s.current().to_string_lossy(), + if cfg!(windows) { ".\\a\\b" } else { "./a/b" }, + "dot is silently ignored" + ); +} + +#[test] +fn absolute_paths_are_invalid() -> crate::Result { + let root = PathBuf::from("."); + let mut s = Stack::new(root.clone()); + + let mut r = Record::default(); + let err = s.make_relative_path_current("/".as_ref(), &mut r).unwrap_err(); + assert_eq!( + err.to_string(), + "Input path \"/\" contains relative or absolute components", + "a leading slash is always considered absolute" + ); + s.make_relative_path_current("a/".as_ref(), &mut r)?; + assert_eq!( + s.current(), + p("./a/"), + "trailing slashes aren't a problem at this stage, as they cannot cause a 'breakout'" + ); + s.make_relative_path_current("b\\".as_ref(), &mut r)?; + assert_eq!( + s.current(), + p("./b\\"), + "trailing back-slashes are fine both on Windows and unix - on Unix it's part fo the filename" + ); + + #[cfg(windows)] + { + let err = s.make_relative_path_current("\\".as_ref(), &mut r).unwrap_err(); + assert_eq!( + err.to_string(), + "Input path \"\\\" contains relative or absolute components", + "on windows, backslashes are considered absolute and replace the base if it is relative, \ + hence they are forbidden." + ); + + let err = s.make_relative_path_current("c:".as_ref(), &mut r).unwrap_err(); + assert_eq!( + err.to_string(), + "Input path \"c:\" contains relative or absolute components", + "on windows, drive-letters are also absolute" + ); + + s.make_relative_path_current("ึ:".as_ref(), &mut r)?; + assert_eq!( + s.current().to_string_lossy(), + ".\\ึ:", + "on windows, any unicode character will do as virtual drive-letter actually with `subst`, \ + but we just turn it into a presumably invalid path which is fine, i.e. we get a joined path" + ); + let err = s + .make_relative_path_current(r#"\\localhost\hello"#.as_ref(), &mut r) + .unwrap_err(); + assert_eq!( + err.to_string(), + r#"Input path "\\localhost\hello" contains relative or absolute components"#, + "there is UNC paths as well" + ); + + let err = s.make_relative_path_current(r#"\\?\C:"#.as_ref(), &mut r).unwrap_err(); + assert_eq!( + err.to_string(), + r#"Input path "\\?\C:" contains relative or absolute components"#, + "there is UNC paths as well, sometimes they look different" + ); + } + Ok(()) +} + #[test] fn delegate_calls_are_consistent() -> crate::Result { let root = PathBuf::from("."); @@ -43,7 +275,8 @@ fn delegate_calls_are_consistent() -> crate::Result { push_dir: 2, dirs: dirs.clone(), push: 2, - } + }, + "it pushes the root-directory first, then the intermediate one" ); s.make_relative_path_current("a/b2".as_ref(), &mut r)?; @@ -53,7 +286,8 @@ fn delegate_calls_are_consistent() -> crate::Result { push_dir: 2, dirs: dirs.clone(), push: 3, - } + }, + "dirs remain the same as b2 is a leaf/file, hence the new `push`" ); s.make_relative_path_current("c/d/e".as_ref(), &mut r)?; @@ -65,7 +299,8 @@ fn delegate_calls_are_consistent() -> crate::Result { push_dir: 4, dirs: dirs.clone(), push: 6, - } + }, + "each directory is pushed individually, after popping 'a' which isn't included anymore" ); dirs.push(root.join("c").join("d").join("x")); @@ -76,10 +311,11 @@ fn delegate_calls_are_consistent() -> crate::Result { push_dir: 5, dirs: dirs.clone(), push: 8, - } + }, + "a new path component is added, hence `push_dir + 1`, but two components are added in total" ); - dirs.drain(dirs.len() - 3..).count(); + dirs.drain(1..).count(); s.make_relative_path_current("f".as_ref(), &mut r)?; assert_eq!(s.current_relative(), Path::new("f")); assert_eq!( @@ -88,7 +324,8 @@ fn delegate_calls_are_consistent() -> crate::Result { push_dir: 5, dirs: dirs.clone(), push: 9, - } + }, + "Now we only keep the root, as `f` is a leaf, hence `push + 1`" ); dirs.push(root.join("x")); @@ -99,7 +336,8 @@ fn delegate_calls_are_consistent() -> crate::Result { push_dir: 6, dirs: dirs.clone(), push: 11, - } + }, + "a new directory is pushed, or two new components total, hence `push + 2`" ); dirs.push(root.join("x").join("z")); @@ -110,7 +348,8 @@ fn delegate_calls_are_consistent() -> crate::Result { push_dir: 7, dirs: dirs.clone(), push: 12, - } + }, + "and another sub-directory is added" ); dirs.push(root.join("x").join("z").join("a")); @@ -122,10 +361,11 @@ fn delegate_calls_are_consistent() -> crate::Result { push_dir: 9, dirs: dirs.clone(), push: 14, - } + }, + "and more subdirectories, two at once this time." ); - dirs.drain(dirs.len() - 2..).count(); + dirs.drain(1 /*root*/ + 1 /*x*/ + 1 /*x/z*/ ..).count(); s.make_relative_path_current("x/z".as_ref(), &mut r)?; assert_eq!( r, @@ -133,7 +373,8 @@ fn delegate_calls_are_consistent() -> crate::Result { push_dir: 9, dirs: dirs.clone(), push: 14, - } + }, + "this only pops components, and as x/z/a/ was previously a directory, x/z is still a directory" ); assert_eq!( dirs.last(), @@ -141,16 +382,104 @@ fn delegate_calls_are_consistent() -> crate::Result { "the stack is state so keeps thinking it's a directory which is consistent. Git does it differently though." ); - s.make_relative_path_current("".as_ref(), &mut r)?; + let err = s.make_relative_path_current("".as_ref(), &mut r).unwrap_err(); + assert_eq!( + err.to_string(), + "empty inputs are not allowed", + "this is to protect us from double-counting the root path next time a component is pushed, \ + and besides that really shouldn't happen" + ); + + s.make_relative_path_current("leaf".as_ref(), &mut r)?; + dirs.drain(1..).count(); assert_eq!( r, Record { push_dir: 9, - dirs: vec![".".into()], - push: 14, + dirs: dirs.clone(), + push: 15, }, - "empty-paths reset the tree effectively" + "reset as much as possible, with just a leaf-component and the root directory" ); + s.make_relative_path_current("a//b".as_ref(), &mut r)?; + dirs.push(root.join("a")); + assert_eq!( + r, + Record { + push_dir: 10, + dirs: dirs.clone(), + push: 17, + }, + "double-slashes are automatically cleaned, even though they shouldn't happen, it's not forbidden" + ); + + #[cfg(not(windows))] + { + s.make_relative_path_current("\\/b".as_ref(), &mut r)?; + dirs.pop(); + dirs.push(root.join("\\")); + assert_eq!( + r, + Record { + push_dir: 11, + dirs: dirs.clone(), + push: 19, + }, + "a backslash is a normal character outside of windows, so it's fine to have it as component" + ); + + s.make_relative_path_current("\\".as_ref(), &mut r)?; + assert_eq!( + r, + Record { + push_dir: 11, + dirs: dirs.clone(), + push: 19, + }, + ); + assert_eq!( + s.current().to_string_lossy(), + "./\\", + "a backslash can also be a valid leaf component - here we only popped the 'b', leaving the \\ 'directory'" + ); + + s.make_relative_path_current("\\\\".as_ref(), &mut r)?; + dirs.pop(); + assert_eq!( + r, + Record { + push_dir: 11, + dirs: dirs.clone(), + push: 20, + }, + ); + assert_eq!( + s.current().to_string_lossy(), + "./\\\\", + "the backslash can also be an ordinary leaf, without the need for it to be a directory" + ); + } + + #[cfg(windows)] + { + s.make_relative_path_current("c\\/d".as_ref(), &mut r)?; + dirs.pop(); + dirs.push(root.join("c")); + assert_eq!( + r, + Record { + push_dir: 11, + dirs: dirs.clone(), + push: 19, + }, + ); + assert_eq!( + s.current().to_string_lossy(), + ".\\c\\d", + "the backslash is a path-separator, and so is the `/`, which is turned into backslash" + ); + } + Ok(()) } diff --git a/gix-worktree/src/stack/delegate.rs b/gix-worktree/src/stack/delegate.rs index 1234346c5de..c06c2dccf60 100644 --- a/gix-worktree/src/stack/delegate.rs +++ b/gix-worktree/src/stack/delegate.rs @@ -1,5 +1,3 @@ -use bstr::{BStr, ByteSlice}; - use crate::{stack::State, PathIdMapping}; /// Various aggregate numbers related to the stack delegate itself. @@ -32,29 +30,15 @@ pub(crate) struct StackDelegate<'a, 'find> { impl<'a, 'find> gix_fs::stack::Delegate for StackDelegate<'a, 'find> { fn push_directory(&mut self, stack: &gix_fs::Stack) -> std::io::Result<()> { self.statistics.delegate.push_directory += 1; - let dir_bstr = gix_path::into_bstr(stack.current()); - let rela_dir_cow = gix_path::to_unix_separators_on_windows( - gix_glob::search::pattern::strip_base_handle_recompute_basename_pos( - gix_path::into_bstr(stack.root()).as_ref(), - dir_bstr.as_ref(), - None, - self.case, - ) - .expect("dir in root") - .0, - ); - let rela_dir: &BStr = if rela_dir_cow.starts_with(b"/") { - rela_dir_cow[1..].as_bstr() - } else { - rela_dir_cow.as_ref() - }; + let rela_dir_bstr = gix_path::into_bstr(stack.current_relative()); + let rela_dir = gix_path::to_unix_separators_on_windows(rela_dir_bstr); match &mut self.state { #[cfg(feature = "attributes")] State::CreateDirectoryAndAttributesStack { attributes, .. } | State::AttributesStack(attributes) => { attributes.push_directory( stack.root(), stack.current(), - rela_dir, + &rela_dir, self.buf, self.id_mappings, self.objects, @@ -66,7 +50,7 @@ impl<'a, 'find> gix_fs::stack::Delegate for StackDelegate<'a, 'find> { attributes.push_directory( stack.root(), stack.current(), - rela_dir, + &rela_dir, self.buf, self.id_mappings, self.objects, @@ -75,7 +59,7 @@ impl<'a, 'find> gix_fs::stack::Delegate for StackDelegate<'a, 'find> { ignore.push_directory( stack.root(), stack.current(), - rela_dir, + &rela_dir, self.buf, self.id_mappings, self.objects, @@ -86,7 +70,7 @@ impl<'a, 'find> gix_fs::stack::Delegate for StackDelegate<'a, 'find> { State::IgnoreStack(ignore) => ignore.push_directory( stack.root(), stack.current(), - rela_dir, + &rela_dir, self.buf, self.id_mappings, self.objects, diff --git a/gix-worktree/src/stack/state/attributes.rs b/gix-worktree/src/stack/state/attributes.rs index 04ad8b5c712..1071e4a9f27 100644 --- a/gix-worktree/src/stack/state/attributes.rs +++ b/gix-worktree/src/stack/state/attributes.rs @@ -98,8 +98,7 @@ impl Attributes { objects: &dyn gix_object::Find, stats: &mut Statistics, ) -> std::io::Result<()> { - let attr_path_relative = - gix_path::to_unix_separators_on_windows(gix_path::join_bstr_unix_pathsep(rela_dir, ".gitattributes")); + let attr_path_relative = gix_path::join_bstr_unix_pathsep(rela_dir, ".gitattributes"); let attr_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(attr_path_relative.as_ref())); // Git does not follow symbolic links as per documentation. let no_follow_symlinks = false; From eff4c00fc76b7bc8c8ac6a6ec4c5bd34889cc436 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 15 May 2024 15:42:05 +0200 Subject: [PATCH 03/50] feat: add validation for path components That way it's easier to assure that forbidden names are never used as part of path components. --- gix-validate/src/lib.rs | 4 + gix-validate/src/path.rs | 246 ++++++++++++++++++++++++++++++ gix-validate/tests/path/mod.rs | 265 +++++++++++++++++++++++++++++++++ gix-validate/tests/validate.rs | 1 + 4 files changed, 516 insertions(+) create mode 100644 gix-validate/src/path.rs create mode 100644 gix-validate/tests/path/mod.rs diff --git a/gix-validate/src/lib.rs b/gix-validate/src/lib.rs index f0493960c73..0143187a851 100644 --- a/gix-validate/src/lib.rs +++ b/gix-validate/src/lib.rs @@ -13,3 +13,7 @@ pub mod tag; /// #[allow(clippy::empty_docs)] pub mod submodule; + +/// +#[allow(clippy::empty_docs)] +pub mod path; diff --git a/gix-validate/src/path.rs b/gix-validate/src/path.rs new file mode 100644 index 00000000000..5a305c06a0b --- /dev/null +++ b/gix-validate/src/path.rs @@ -0,0 +1,246 @@ +use bstr::{BStr, ByteSlice}; + +/// +#[allow(clippy::empty_docs)] +pub mod component { + /// The error returned by [`component()`](super::component()). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("A path component must not be empty")] + Empty, + #[error("Path separators like / or \\ are not allowed")] + PathSeparator, + #[error("Window path prefixes are not allowed")] + WindowsPathPrefix, + #[error("The .git name may never be used")] + DotGitDir, + #[error("The .gitmodules file must not be a symlink")] + SymlinkedGitModules, + } + + /// Further specify what to check for in [`component()`](super::component()) + /// + /// Note that the `Default` implementation maximizes safety by enabling all protections. + #[derive(Debug, Copy, Clone)] + pub struct Options { + /// This flag should be turned on when on Windows, but can be turned on when on other platforms + /// as well to prevent path components that can cause trouble on Windows. + pub protect_windows: bool, + /// If `true`, protections for the MacOS HFS+ filesystem will be active, checking for + /// special directories that we should never write while ignoring codepoints just like HFS+ would. + /// + /// This field is equivalent to `core.protectHFS`. + pub protect_hfs: bool, + /// If `true`, protections for Windows NTFS specific features will be active. This adds special handling + /// for `8.3` filenames and alternate data streams, both of which could be used to mask th etrue name of + /// what would be created on disk. + /// + /// This field is equivalent to `core.protectNTFS`. + pub protect_ntfs: bool, + } + + impl Default for Options { + fn default() -> Self { + Options { + protect_windows: true, + protect_hfs: true, + protect_ntfs: true, + } + } + } + + /// The mode of the component, if it's the leaf of a path. + #[derive(Debug, Copy, Clone, PartialEq, Eq)] + pub enum Mode { + /// The item is a symbolic link. + Symlink, + } +} + +/// Assure the given `input` resembles a valid name for a tree or blob, and in that sense, a path component. +/// `mode` indicates the kind of `input` and it should be `Some` if `input` is the last component in the underlying +/// path. Currently, this is only used to determine if `.gitmodules` is a symlink. +/// +/// `input` must not make it possible to exit the repository, or to specify absolute paths. +pub fn component( + input: &BStr, + mode: Option, + component::Options { + protect_windows, + protect_hfs, + protect_ntfs, + }: component::Options, +) -> Result<&BStr, component::Error> { + if input.is_empty() { + return Err(component::Error::Empty); + } + if protect_windows { + if input.find_byteset(b"/\\").is_some() { + return Err(component::Error::PathSeparator); + } + if input.chars().skip(1).next() == Some(':') { + return Err(component::Error::WindowsPathPrefix); + } + } else if input.find_byte(b'/').is_some() { + return Err(component::Error::PathSeparator); + } + if protect_hfs { + if is_dot_hfs(input, "git") { + return Err(component::Error::DotGitDir); + } + if is_symlink(mode) && is_dot_hfs(input, "gitmodules") { + return Err(component::Error::SymlinkedGitModules); + } + } + + if protect_ntfs { + if is_dot_git_ntfs(input) { + return Err(component::Error::DotGitDir); + } + if is_symlink(mode) && is_dot_ntfs(input, "gitmodules", "gi7eba") { + return Err(component::Error::SymlinkedGitModules); + } + } + + if !(protect_hfs | protect_ntfs) { + if input.eq_ignore_ascii_case(b".git") { + return Err(component::Error::DotGitDir); + } + if is_symlink(mode) && input.eq_ignore_ascii_case(b".gitmodules") { + return Err(component::Error::SymlinkedGitModules); + } + } + Ok(input) +} + +fn is_symlink(mode: Option) -> bool { + mode.map_or(false, |m| m == component::Mode::Symlink) +} + +fn is_dot_hfs(input: &BStr, search_case_insensitive: &str) -> bool { + let mut input = input.chars().filter(|c| match *c as u32 { + 0x200c | /* ZERO WIDTH NON-JOINER */ + 0x200d | /* ZERO WIDTH JOINER */ + 0x200e | /* LEFT-TO-RIGHT MARK */ + 0x200f | /* RIGHT-TO-LEFT MARK */ + 0x202a | /* LEFT-TO-RIGHT EMBEDDING */ + 0x202b | /* RIGHT-TO-LEFT EMBEDDING */ + 0x202c | /* POP DIRECTIONAL FORMATTING */ + 0x202d | /* LEFT-TO-RIGHT OVERRIDE */ + 0x202e | /* RIGHT-TO-LEFT OVERRIDE */ + 0x206a | /* INHIBIT SYMMETRIC SWAPPING */ + 0x206b | /* ACTIVATE SYMMETRIC SWAPPING */ + 0x206c | /* INHIBIT ARABIC FORM SHAPING */ + 0x206d | /* ACTIVATE ARABIC FORM SHAPING */ + 0x206e | /* NATIONAL DIGIT SHAPES */ + 0x206f | /* NOMINAL DIGIT SHAPES */ + 0xfeff => false, /* ZERO WIDTH NO-BREAK SPACE */ + _ => true + }); + if input.next() != Some('.') { + return false; + } + + let mut comp = search_case_insensitive.chars(); + loop { + match (comp.next(), input.next()) { + (Some(a), Some(b)) => { + if !a.eq_ignore_ascii_case(&b) { + return false; + } + } + (None, None) => return true, + _ => return false, + } + } +} + +fn is_dot_git_ntfs(input: &BStr) -> bool { + if input + .get(..4) + .map_or(false, |input| input.eq_ignore_ascii_case(b".git")) + { + return is_done_ntfs(input.get(4..)); + } + if input + .get(..5) + .map_or(false, |input| input.eq_ignore_ascii_case(b"git~1")) + { + return is_done_ntfs(input.get(5..)); + } + false +} + +fn is_dot_ntfs(input: &BStr, search_case_insensitive: &str, ntfs_shortname_prefix: &str) -> bool { + if input.get(0) == Some(&b'.') { + let end_pos = 1 + search_case_insensitive.len(); + if input.get(1..end_pos).map_or(false, |input| { + input.eq_ignore_ascii_case(search_case_insensitive.as_bytes()) + }) { + is_done_ntfs(input.get(end_pos..)) + } else { + false + } + } else { + let search_case_insensitive: &[u8] = search_case_insensitive.as_bytes(); + if search_case_insensitive + .get(..6) + .zip(input.get(..6)) + .map_or(false, |(ntfs_prefix, first_6_of_input)| { + first_6_of_input.eq_ignore_ascii_case(ntfs_prefix) + && input.get(6) == Some(&b'~') + && input.get(7).map_or(false, |num| num >= &b'1' && num <= &b'4') + }) + { + return is_done_ntfs(input.get(8..)); + } + + let ntfs_shortname_prefix: &[u8] = ntfs_shortname_prefix.as_bytes(); + let mut saw_tilde = false; + let mut pos = 0; + while pos < 8 { + let Some(b) = input.get(pos).copied() else { + return false; + }; + if saw_tilde { + if b < b'0' || b > b'9' { + return false; + } + } else if b == b'~' { + saw_tilde = true; + pos += 1; + let Some(b) = input.get(pos).copied() else { + return false; + }; + if b < b'1' || b > b'9' { + return false; + } + } else if pos >= 6 { + return false; + } else if b & 0x80 == 0x80 { + return false; + } else if ntfs_shortname_prefix + .get(pos) + .map_or(true, |ob| !b.eq_ignore_ascii_case(ob)) + { + return false; + } + pos += 1; + } + is_done_ntfs(input.get(pos..)) + } +} + +fn is_done_ntfs(input: Option<&[u8]>) -> bool { + let Some(input) = input else { return true }; + for b in input.bytes() { + if b == b':' { + return true; + } + if b != b' ' && b != b'.' { + return false; + } + } + true +} diff --git a/gix-validate/tests/path/mod.rs b/gix-validate/tests/path/mod.rs new file mode 100644 index 00000000000..1436ca1a375 --- /dev/null +++ b/gix-validate/tests/path/mod.rs @@ -0,0 +1,265 @@ +mod component { + use gix_validate::path::component; + + const NO_OPTS: component::Options = component::Options { + protect_windows: false, + protect_hfs: false, + protect_ntfs: false, + }; + const ALL_OPTS: component::Options = component::Options { + protect_windows: true, + protect_hfs: true, + protect_ntfs: true, + }; + + mod valid { + use crate::path::component::{ALL_OPTS, NO_OPTS}; + use bstr::ByteSlice; + use gix_validate::path::component; + use gix_validate::path::component::Mode::Symlink; + macro_rules! mktest { + ($name:ident, $input:expr) => { + mktest!($name, $input, ALL_OPTS); + }; + ($name:ident, $input:expr, $opts:expr) => { + #[test] + fn $name() { + assert!(gix_validate::path::component($input.as_bstr(), None, $opts).is_ok()) + } + }; + ($name:ident, $input:expr, $mode:expr, $opts:expr) => { + #[test] + fn $name() { + assert!(gix_validate::path::component($input.as_bstr(), Some($mode), $opts).is_ok()) + } + }; + } + + const UNIX_OPTS: component::Options = component::Options { + protect_windows: false, + protect_hfs: true, + protect_ntfs: true, + }; + + mktest!(ascii, b"ascii-only_and-that"); + mktest!(unicode, "๐Ÿ˜๐Ÿ‘๐Ÿ‘Œ".as_bytes()); + mktest!(backslashes_on_unix, b"\\", UNIX_OPTS); + mktest!(drive_letters_on_unix, b"c:", UNIX_OPTS); + mktest!(virtual_drive_letters_on_unix, "ึ:".as_bytes(), UNIX_OPTS); + mktest!(unc_path_on_unix, b"\\\\?\\pictures", UNIX_OPTS); + mktest!(not_dot_git_longer, b".gitu", NO_OPTS); + mktest!(not_dot_git_longer_all, b".gitu"); + mktest!(not_dot_gitmodules_shorter, b".gitmodule", Symlink, NO_OPTS); + mktest!(not_dot_gitmodules_shorter_all, b".gitmodule", Symlink, ALL_OPTS); + mktest!(not_dot_gitmodules_longer, b".gitmodulesa", Symlink, NO_OPTS); + mktest!(not_dot_gitmodules_longer_all, b".gitmodulesa", Symlink, ALL_OPTS); + mktest!(dot_gitmodules_as_file, b".gitmodules", UNIX_OPTS); + mktest!(not_dot_git_shorter, b".gi", NO_OPTS); + mktest!(not_dot_git_shorter_ntfs_8_3, b"gi~1"); + mktest!(not_dot_git_longer_ntfs_8_3, b"gitu~1"); + mktest!(not_dot_git_shorter_ntfs_8_3_disabled, b"git~1", NO_OPTS); + mktest!(not_dot_git_longer_hfs, ".g\u{200c}itu".as_bytes()); + mktest!(not_dot_git_shorter_hfs, ".g\u{200c}i".as_bytes()); + mktest!( + not_dot_gitmodules_shorter_hfs, + ".gitm\u{200c}odule".as_bytes(), + Symlink, + UNIX_OPTS + ); + mktest!(dot_gitmodules_as_file_hfs, ".g\u{200c}itmodules".as_bytes(), UNIX_OPTS); + mktest!(dot_gitmodules_ntfs_8_3_disabled, b"gItMOD~1", Symlink, NO_OPTS); + mktest!( + not_dot_gitmodules_longer_hfs, + "\u{200c}.gitmodulesa".as_bytes(), + Symlink, + UNIX_OPTS + ); + } + + mod invalid { + use crate::path::component::{ALL_OPTS, NO_OPTS}; + use bstr::ByteSlice; + use gix_validate::path::component::Error; + use gix_validate::path::component::Mode::Symlink; + + macro_rules! mktest { + ($name:ident, $input:expr, $expected:pat) => { + mktest!($name, $input, $expected, ALL_OPTS); + }; + ($name:ident, $input:expr, $expected:pat, $opts:expr) => { + #[test] + fn $name() { + match gix_validate::path::component($input.as_bstr(), None, $opts) { + Err($expected) => {} + got => panic!("Wanted {}, got {:?}", stringify!($expected), got), + } + } + }; + ($name:ident, $input:expr, $expected:pat, $mode:expr, $opts:expr) => { + #[test] + fn $name() { + match gix_validate::path::component($input.as_bstr(), Some($mode), $opts) { + Err($expected) => {} + got => panic!("Wanted {}, got {:?}", stringify!($expected), got), + } + } + }; + } + + mktest!(empty, b"", Error::Empty); + mktest!(dot_git_lower, b".git", Error::DotGitDir, NO_OPTS); + mktest!(dot_git_lower_hfs, ".g\u{200c}it".as_bytes(), Error::DotGitDir); + mktest!(dot_git_lower_hfs_simple, ".Git".as_bytes(), Error::DotGitDir); + mktest!(dot_git_upper, b".GIT", Error::DotGitDir, NO_OPTS); + mktest!(dot_git_upper_hfs, ".GIT\u{200e}".as_bytes(), Error::DotGitDir); + mktest!(dot_git_upper_ntfs_8_3, b"GIT~1", Error::DotGitDir); + mktest!(dot_git_mixed, b".gIt", Error::DotGitDir, NO_OPTS); + mktest!(dot_git_mixed_ntfs_8_3, b"gIt~1", Error::DotGitDir); + mktest!( + dot_gitmodules_mixed, + b".gItmodules", + Error::SymlinkedGitModules, + Symlink, + NO_OPTS + ); + mktest!(dot_git_mixed_hfs, "\u{206e}.gIt".as_bytes(), Error::DotGitDir); + mktest!( + dot_git_ntfs_8_3_numbers_only, + b"~1000000", + Error::SymlinkedGitModules, + Symlink, + ALL_OPTS + ); + mktest!( + dot_git_ntfs_8_3_numbers_only_too, + b"~9999999", + Error::SymlinkedGitModules, + Symlink, + ALL_OPTS + ); + mktest!( + dot_gitmodules_mixed_hfs, + "\u{206e}.gItmodules".as_bytes(), + Error::SymlinkedGitModules, + Symlink, + ALL_OPTS + ); + mktest!( + dot_gitmodules_mixed_ntfs_8_3, + b"gItMOD~1", + Error::SymlinkedGitModules, + Symlink, + ALL_OPTS + ); + mktest!( + dot_gitmodules_mixed_ntfs_stream, + b".giTmodUles:$DATA", + Error::SymlinkedGitModules, + Symlink, + ALL_OPTS + ); + mktest!(path_separator_slash_between, b"a/b", Error::PathSeparator); + mktest!(path_separator_slash_leading, b"/a", Error::PathSeparator); + mktest!(path_separator_slash_trailing, b"a/", Error::PathSeparator); + mktest!(path_separator_slash_only, b"/", Error::PathSeparator); + mktest!(slashes_on_windows, b"/", Error::PathSeparator, ALL_OPTS); + mktest!(backslashes_on_windows, b"\\", Error::PathSeparator, ALL_OPTS); + mktest!(path_separator_backslash_between, b"a\\b", Error::PathSeparator); + mktest!(path_separator_backslash_leading, b"\\a", Error::PathSeparator); + mktest!(path_separator_backslash_trailing, b"a\\", Error::PathSeparator); + mktest!(drive_letters, b"c:", Error::WindowsPathPrefix, ALL_OPTS); + mktest!( + virtual_drive_letters, + "ึ:".as_bytes(), + Error::WindowsPathPrefix, + ALL_OPTS + ); + mktest!(unc_path, b"\\\\?\\pictures", Error::PathSeparator, ALL_OPTS); + + #[test] + fn ntfs_gitmodules() { + for invalid in [ + ".gitmodules", + ".Gitmodules", + ".gitmoduleS", + ".gitmodules ", + ".gitmodules.", + ".gitmodules ", + ".gitmodules. ", + ".gitmodules .", + ".gitmodules..", + ".gitmodules ", + ".gitmodules. ", + ".gitmodules . ", + ".gitmodules .", + ".Gitmodules ", + ".Gitmodules.", + ".Gitmodules ", + ".Gitmodules. ", + ".Gitmodules .", + ".Gitmodules..", + ".Gitmodules ", + ".Gitmodules. ", + ".Gitmodules . ", + ".Gitmodules .", + "GITMOD~1", + "gitmod~1", + "GITMOD~2", + "giTmod~3", + "GITMOD~4", + "GITMOD~1 ", + "gitMod~2.", + "GITMOD~3 ", + "gitmod~4. ", + "GITMoD~1 .", + "gitmod~2 ", + "GITMOD~3. ", + "gitmoD~4 . ", + "GI7EBA~1", + "gi7eba~9", + "GI7EB~10", + "GI7EB~11", + "GI7EB~99", + "GI7EB~10", + "GI7E~100", + "GI7E~101", + "GI7E~999", + ".gitmodules:$DATA", + "gitmod~4 . :$DATA", + ] { + match gix_validate::path::component(invalid.into(), Some(Symlink), ALL_OPTS) { + Ok(_) => { + unreachable!("{invalid:?} should not validate successfully") + } + Err(err) => { + assert!(matches!(err, Error::SymlinkedGitModules)) + } + } + } + + for valid in [ + ".gitmodules x", + ".gitmodules .x", + " .gitmodules", + "..gitmodules", + "gitmodules", + ".gitmodule", + ".gitmodules x ", + ".gitmodules .x", + "GI7EBA~", + "GI7EBA~0", + "GI7EBA~~1", + "GI7EBA~X", + "Gx7EBA~1", + "GI7EBX~1", + "GI7EB~1", + "GI7EB~01", + "GI7EB~1X", + ".gitmodules,:$DATA", + ] { + gix_validate::path::component(valid.into(), Some(Symlink), ALL_OPTS) + .unwrap_or_else(|_| panic!("{valid:?} should have been valid")); + } + } + } +} diff --git a/gix-validate/tests/validate.rs b/gix-validate/tests/validate.rs index db45c4aac56..d1951c3d159 100644 --- a/gix-validate/tests/validate.rs +++ b/gix-validate/tests/validate.rs @@ -1,3 +1,4 @@ +mod path; mod reference; mod submodule; mod tag; From 874cfd6dd7e371f178ec5f63368220b272608805 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 16 May 2024 18:33:12 +0200 Subject: [PATCH 04/50] fix!: validate all components pushed onto the stack when creating leading paths. This way, everyone using the stack with the purpose of altering the working tree will run additional checks to prevent callers from sneaking in forbidden paths. Note that these checks don't run otherwise, so one has to be careful to not forget to run these checks whenever needed. --- Cargo.lock | 1 + gix-fs/src/stack.rs | 4 +- gix-fs/tests/stack/mod.rs | 14 +++++++ gix-worktree/Cargo.toml | 7 ++-- gix-worktree/src/lib.rs | 3 ++ gix-worktree/src/stack/delegate.rs | 41 +++++++++++++++---- gix-worktree/src/stack/mod.rs | 16 +++++--- gix-worktree/src/stack/state/mod.rs | 7 +++- .../tests/worktree/stack/attributes.rs | 1 + .../tests/worktree/stack/create_directory.rs | 21 +++++++++- 10 files changed, 94 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2a396884f76..ab1a3612362 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2791,6 +2791,7 @@ dependencies = [ "gix-index 0.32.1", "gix-object 0.42.1", "gix-path 0.10.7", + "gix-validate 0.8.4", "serde", ] diff --git a/gix-fs/src/stack.rs b/gix-fs/src/stack.rs index 25b4cde7f31..9af6201728a 100644 --- a/gix-fs/src/stack.rs +++ b/gix-fs/src/stack.rs @@ -63,14 +63,12 @@ impl Stack { /// The path is also expected to be normalized, and should not contain extra separators, and must not contain `..` /// or have leading or trailing slashes (or additionally backslashes on Windows). pub fn make_relative_path_current(&mut self, relative: &Path, delegate: &mut dyn Delegate) -> std::io::Result<()> { - if relative.as_os_str().is_empty() { + if self.valid_components != 0 && relative.as_os_str().is_empty() { return Err(std::io::Error::new( std::io::ErrorKind::Other, "empty inputs are not allowed", )); } - // TODO: prevent leading or trailing slashes, on Windows also backslashes. - // prevent leading backslashes on Windows as they are strange if self.valid_components == 0 { delegate.push_directory(self)?; } diff --git a/gix-fs/tests/stack/mod.rs b/gix-fs/tests/stack/mod.rs index 61f3d0b2459..9edd40a2c18 100644 --- a/gix-fs/tests/stack/mod.rs +++ b/gix-fs/tests/stack/mod.rs @@ -157,6 +157,20 @@ fn path_join_handling() { ); } +#[test] +fn empty_paths_are_noop_if_no_path_was_pushed_before() { + let root = PathBuf::from("."); + let mut s = Stack::new(root.clone()); + + let mut r = Record::default(); + s.make_relative_path_current("".as_ref(), &mut r).unwrap(); + assert_eq!( + s.current_relative().to_string_lossy(), + "", + "it's fine to push an empty path to get a value for the stack root, once" + ); +} + #[test] fn relative_components_are_invalid() { let root = PathBuf::from("."); diff --git a/gix-worktree/Cargo.toml b/gix-worktree/Cargo.toml index a01fbb40f27..cf691531416 100644 --- a/gix-worktree/Cargo.toml +++ b/gix-worktree/Cargo.toml @@ -16,9 +16,9 @@ doctest = false [features] default = ["attributes"] ## Instantiate stacks that can access `.gitattributes` information. -attributes = ["dep:gix-attributes"] +attributes = ["dep:gix-attributes", "dep:gix-validate"] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde = [ "dep:serde", "bstr/serde", "gix-index/serde", "gix-hash/serde", "gix-object/serde", "gix-attributes?/serde", "gix-ignore/serde" ] +serde = ["dep:serde", "bstr/serde", "gix-index/serde", "gix-hash/serde", "gix-object/serde", "gix-attributes?/serde", "gix-ignore/serde"] [dependencies] gix-index = { version = "^0.32.1", path = "../gix-index" } @@ -28,10 +28,11 @@ gix-object = { version = "^0.42.0", path = "../gix-object" } gix-glob = { version = "^0.16.2", path = "../gix-glob" } gix-path = { version = "^0.10.7", path = "../gix-path" } gix-attributes = { version = "^0.22.2", path = "../gix-attributes", optional = true } +gix-validate = { version = "^0.8.4", path = "../gix-validate", optional = true } gix-ignore = { version = "^0.11.2", path = "../gix-ignore" } gix-features = { version = "^0.38.1", path = "../gix-features" } -serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} +serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } bstr = { version = "1.3.0", default-features = false } document-features = { version = "0.2.0", optional = true } diff --git a/gix-worktree/src/lib.rs b/gix-worktree/src/lib.rs index 7238538e73d..a68bea99826 100644 --- a/gix-worktree/src/lib.rs +++ b/gix-worktree/src/lib.rs @@ -19,6 +19,9 @@ pub use gix_glob as glob; pub use gix_ignore as ignore; /// Provides types needed for using [`Stack::at_path()`] and [`Stack::at_entry()`]. pub use gix_object as object; +/// Provides types needed for using [`stack::State::for_checkout()`]. +#[cfg(feature = "attributes")] +pub use gix_validate as validate; /// A cache for efficiently executing operations on directories and files which are encountered in sorted order. /// That way, these operations can be re-used for subsequent invocations in the same directory. diff --git a/gix-worktree/src/stack/delegate.rs b/gix-worktree/src/stack/delegate.rs index c06c2dccf60..98e8a8c9e74 100644 --- a/gix-worktree/src/stack/delegate.rs +++ b/gix-worktree/src/stack/delegate.rs @@ -88,14 +88,18 @@ impl<'a, 'find> gix_fs::stack::Delegate for StackDelegate<'a, 'find> { #[cfg(feature = "attributes")] State::CreateDirectoryAndAttributesStack { unlink_on_collision, + validate, attributes: _, - } => create_leading_directory( - is_last_component, - stack, - self.is_dir, - &mut self.statistics.delegate.num_mkdir_calls, - *unlink_on_collision, - )?, + } => { + validate_last_component(stack, *validate)?; + create_leading_directory( + is_last_component, + stack, + self.is_dir, + &mut self.statistics.delegate.num_mkdir_calls, + *unlink_on_collision, + )? + } #[cfg(feature = "attributes")] State::AttributesAndIgnoreStack { .. } | State::AttributesStack(_) => {} State::IgnoreStack(_) => {} @@ -122,6 +126,29 @@ impl<'a, 'find> gix_fs::stack::Delegate for StackDelegate<'a, 'find> { } } +#[cfg(feature = "attributes")] +fn validate_last_component(stack: &gix_fs::Stack, opts: gix_validate::path::component::Options) -> std::io::Result<()> { + // TODO: add mode-information + let Some(last_component) = stack.current_relative().components().rev().next() else { + return Ok(()); + }; + let last_component = + gix_path::try_into_bstr(std::borrow::Cow::Borrowed(last_component.as_os_str().as_ref())).map_err(|_err| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "Path component {last_component:?} of path \"{}\" contained invalid UTF-8 and could not be validated", + stack.current_relative().display() + ), + ) + })?; + + if let Err(err) = gix_validate::path::component(last_component.as_ref(), None, opts) { + return Err(std::io::Error::new(std::io::ErrorKind::Other, err)); + } + Ok(()) +} + #[cfg(feature = "attributes")] fn create_leading_directory( is_last_component: bool, diff --git a/gix-worktree/src/stack/mod.rs b/gix-worktree/src/stack/mod.rs index 4629a7a08ed..991d1ce1b51 100644 --- a/gix-worktree/src/stack/mod.rs +++ b/gix-worktree/src/stack/mod.rs @@ -28,6 +28,8 @@ pub enum State { CreateDirectoryAndAttributesStack { /// If there is a symlink or a file in our path, try to unlink it before creating the directory. unlink_on_collision: bool, + /// Options to control how newly created path components should be validated. + validate: gix_validate::path::component::Options, /// State to handle attribute information attributes: state::Attributes, }, @@ -135,10 +137,6 @@ impl Stack { /// All effects are similar to [`at_path()`][Self::at_path()]. /// /// If `relative` ends with `/` and `is_dir` is `None`, it is automatically assumed to be a directory. - /// - /// ### Panics - /// - /// on illformed UTF8 in `relative` pub fn at_entry<'r>( &mut self, relative: impl Into<&'r BStr>, @@ -146,7 +144,15 @@ impl Stack { objects: &dyn gix_object::Find, ) -> std::io::Result> { let relative = relative.into(); - let relative_path = gix_path::from_bstr(relative); + let relative_path = gix_path::try_from_bstr(relative).map_err(|_err| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "The path \"{}\" contained invalid UTF-8 and could not be turned into a path", + relative + ), + ) + })?; self.at_path( relative_path, diff --git a/gix-worktree/src/stack/state/mod.rs b/gix-worktree/src/stack/state/mod.rs index 04afb046368..52d74daac61 100644 --- a/gix-worktree/src/stack/state/mod.rs +++ b/gix-worktree/src/stack/state/mod.rs @@ -60,9 +60,14 @@ pub mod ignore; impl State { /// Configure a state to be suitable for checking out files, which only needs access to attribute files read from the index. #[cfg(feature = "attributes")] - pub fn for_checkout(unlink_on_collision: bool, attributes: Attributes) -> Self { + pub fn for_checkout( + unlink_on_collision: bool, + validate: gix_validate::path::component::Options, + attributes: Attributes, + ) -> Self { State::CreateDirectoryAndAttributesStack { unlink_on_collision, + validate, attributes, } } diff --git a/gix-worktree/tests/worktree/stack/attributes.rs b/gix-worktree/tests/worktree/stack/attributes.rs index ae5d7a6b542..5234eccfe42 100644 --- a/gix-worktree/tests/worktree/stack/attributes.rs +++ b/gix-worktree/tests/worktree/stack/attributes.rs @@ -17,6 +17,7 @@ fn baseline() -> crate::Result { let mut collection = gix_attributes::search::MetadataCollection::default(); let state = gix_worktree::stack::State::for_checkout( false, + Default::default(), state::Attributes::new( gix_attributes::Search::new_globals([base.join("user.attributes")], &mut buf, &mut collection)?, Some(git_dir.join("info").join("attributes")), diff --git a/gix-worktree/tests/worktree/stack/create_directory.rs b/gix-worktree/tests/worktree/stack/create_directory.rs index 19130d6559f..b6b83b80549 100644 --- a/gix-worktree/tests/worktree/stack/create_directory.rs +++ b/gix-worktree/tests/worktree/stack/create_directory.rs @@ -8,7 +8,7 @@ fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() -> crate let dir = tempdir()?; let mut cache = Stack::new( dir.path().join("non-existing-root"), - stack::State::for_checkout(false, Default::default()), + stack::State::for_checkout(false, Default::default(), Default::default()), Default::default(), Vec::new(), Default::default(), @@ -54,6 +54,23 @@ fn existing_directories_are_fine() -> crate::Result { Ok(()) } +#[test] +fn validation_to_each_component() -> crate::Result { + let (mut cache, tmp) = new_cache(); + + let err = cache + .at_path("valid/.gIt", Some(false), &gix_object::find::Never) + .unwrap_err(); + assert_eq!( + cache.statistics().delegate.num_mkdir_calls, + 1, + "the valid directory was created" + ); + assert!(tmp.path().join("valid").is_dir(), "it was actually created"); + assert_eq!(err.to_string(), "The .git name may never be used"); + Ok(()) +} + #[test] fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> crate::Result { let (mut cache, tmp) = new_cache(); @@ -110,7 +127,7 @@ fn new_cache() -> (Stack, TempDir) { let dir = tempdir().unwrap(); let cache = Stack::new( dir.path(), - stack::State::for_checkout(false, Default::default()), + stack::State::for_checkout(false, Default::default(), Default::default()), Default::default(), Vec::new(), Default::default(), From 595fe877455824ee1f079976b61d4a5bad74383d Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 17 May 2024 09:44:53 +0200 Subject: [PATCH 05/50] feat!: `Stack::at_path()` replaces `is_dir` parameter with `mode`. That way, detailed information about the path-to-be is available not only for evaluating attributes or excludes, but also for validating path components (in this case, relevant for `.gitmodules`). --- gix-worktree/src/stack/delegate.rs | 26 +++++++++++----- gix-worktree/src/stack/mod.rs | 30 ++++++++++++------- .../tests/worktree/stack/create_directory.rs | 27 +++++++++-------- gix-worktree/tests/worktree/stack/ignore.rs | 14 +++++++-- 4 files changed, 64 insertions(+), 33 deletions(-) diff --git a/gix-worktree/src/stack/delegate.rs b/gix-worktree/src/stack/delegate.rs index 98e8a8c9e74..d20bc372b83 100644 --- a/gix-worktree/src/stack/delegate.rs +++ b/gix-worktree/src/stack/delegate.rs @@ -1,3 +1,4 @@ +use crate::stack::mode_is_dir; use crate::{stack::State, PathIdMapping}; /// Various aggregate numbers related to the stack delegate itself. @@ -20,7 +21,7 @@ pub(crate) struct StackDelegate<'a, 'find> { pub state: &'a mut State, pub buf: &'a mut Vec, #[cfg_attr(not(feature = "attributes"), allow(dead_code))] - pub is_dir: bool, + pub mode: Option, pub id_mappings: &'a Vec, pub objects: &'find dyn gix_object::Find, pub case: gix_glob::pattern::Case, @@ -91,11 +92,11 @@ impl<'a, 'find> gix_fs::stack::Delegate for StackDelegate<'a, 'find> { validate, attributes: _, } => { - validate_last_component(stack, *validate)?; + validate_last_component(stack, self.mode, *validate)?; create_leading_directory( is_last_component, stack, - self.is_dir, + self.mode, &mut self.statistics.delegate.num_mkdir_calls, *unlink_on_collision, )? @@ -127,8 +128,11 @@ impl<'a, 'find> gix_fs::stack::Delegate for StackDelegate<'a, 'find> { } #[cfg(feature = "attributes")] -fn validate_last_component(stack: &gix_fs::Stack, opts: gix_validate::path::component::Options) -> std::io::Result<()> { - // TODO: add mode-information +fn validate_last_component( + stack: &gix_fs::Stack, + mode: Option, + opts: gix_validate::path::component::Options, +) -> std::io::Result<()> { let Some(last_component) = stack.current_relative().components().rev().next() else { return Ok(()); }; @@ -143,7 +147,13 @@ fn validate_last_component(stack: &gix_fs::Stack, opts: gix_validate::path::comp ) })?; - if let Err(err) = gix_validate::path::component(last_component.as_ref(), None, opts) { + if let Err(err) = gix_validate::path::component( + last_component.as_ref(), + mode.and_then(|m| { + (m == gix_index::entry::Mode::SYMLINK).then_some(gix_validate::path::component::Mode::Symlink) + }), + opts, + ) { return Err(std::io::Error::new(std::io::ErrorKind::Other, err)); } Ok(()) @@ -153,11 +163,11 @@ fn validate_last_component(stack: &gix_fs::Stack, opts: gix_validate::path::comp fn create_leading_directory( is_last_component: bool, stack: &gix_fs::Stack, - is_dir: bool, + mode: Option, mkdir_calls: &mut usize, unlink_on_collision: bool, ) -> std::io::Result<()> { - if is_last_component && !is_dir { + if is_last_component && !mode_is_dir(mode).unwrap_or(false) { return Ok(()); } *mkdir_calls += 1; diff --git a/gix-worktree/src/stack/mod.rs b/gix-worktree/src/stack/mod.rs index 991d1ce1b51..2a436148f18 100644 --- a/gix-worktree/src/stack/mod.rs +++ b/gix-worktree/src/stack/mod.rs @@ -105,22 +105,23 @@ impl Stack { impl Stack { /// Append the `relative` path to the root directory of the cache and efficiently create leading directories, while assuring that no /// symlinks are in that path. - /// Unless `is_dir` is known with `Some(โ€ฆ)`, then `relative` points to a directory itself in which case the entire resulting - /// path is created as directory. If it's not known it is assumed to be a file. + /// Unless `mode` is known with `Some(gix_index::entry::Mode::DIR|COMMIT)`, + /// then `relative` points to a directory itself in which case the entire resulting path is created as directory. + /// If it's not known it is assumed to be a file. /// `objects` maybe used to lookup objects from an [id mapping][crate::stack::State::id_mappings_from_index()], with mappnigs /// /// Provide access to cached information for that `relative` path via the returned platform. pub fn at_path( &mut self, relative: impl AsRef, - is_dir: Option, + mode: Option, objects: &dyn gix_object::Find, ) -> std::io::Result> { self.statistics.platforms += 1; let mut delegate = StackDelegate { state: &mut self.state, buf: &mut self.buf, - is_dir: is_dir.unwrap_or(false), + mode, id_mappings: &self.id_mappings, objects, case: self.case, @@ -128,19 +129,22 @@ impl Stack { }; self.stack .make_relative_path_current(relative.as_ref(), &mut delegate)?; - Ok(Platform { parent: self, is_dir }) + Ok(Platform { + parent: self, + is_dir: mode_is_dir(mode), + }) } - /// Obtain a platform for lookups from a repo-`relative` path, typically obtained from an index entry. `is_dir` should reflect - /// whether it's a directory or not, or left at `None` if unknown. + /// Obtain a platform for lookups from a repo-`relative` path, typically obtained from an index entry. `mode` should reflect + /// the kind of item set here, or left at `None` if unknown. /// `objects` maybe used to lookup objects from an [id mapping][crate::stack::State::id_mappings_from_index()]. /// All effects are similar to [`at_path()`][Self::at_path()]. /// - /// If `relative` ends with `/` and `is_dir` is `None`, it is automatically assumed to be a directory. + /// If `relative` ends with `/` and `mode` is `None`, it is automatically assumed set to be a directory. pub fn at_entry<'r>( &mut self, relative: impl Into<&'r BStr>, - is_dir: Option, + mode: Option, objects: &dyn gix_object::Find, ) -> std::io::Result> { let relative = relative.into(); @@ -156,12 +160,18 @@ impl Stack { self.at_path( relative_path, - is_dir.or_else(|| relative.ends_with_str("/").then_some(true)), + mode.or_else(|| relative.ends_with_str("/").then_some(gix_index::entry::Mode::DIR)), objects, ) } } +fn mode_is_dir(mode: Option) -> Option { + mode.map(|m| + // This applies to directories and commits (submodules are directories on disk) + m.is_sparse() || m.is_submodule()) +} + /// Mutation impl Stack { /// Reset the statistics after returning them. diff --git a/gix-worktree/tests/worktree/stack/create_directory.rs b/gix-worktree/tests/worktree/stack/create_directory.rs index b6b83b80549..65b4b04fd7d 100644 --- a/gix-worktree/tests/worktree/stack/create_directory.rs +++ b/gix-worktree/tests/worktree/stack/create_directory.rs @@ -3,6 +3,9 @@ use std::path::Path; use gix_testtools::tempfile::{tempdir, TempDir}; use gix_worktree::{stack, Stack}; +const IS_FILE: Option = Some(gix_index::entry::Mode::FILE); +const IS_DIR: Option = Some(gix_index::entry::Mode::DIR); + #[test] fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() -> crate::Result { let dir = tempdir()?; @@ -15,7 +18,7 @@ fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() -> crate ); assert_eq!(cache.statistics().delegate.num_mkdir_calls, 0); - let path = cache.at_path("hello", Some(false), &gix_object::find::Never)?.path(); + let path = cache.at_path("hello", IS_FILE, &gix_object::find::Never)?.path(); assert!(!path.parent().unwrap().exists(), "prefix itself is never created"); assert_eq!(cache.statistics().delegate.num_mkdir_calls, 0); Ok(()) @@ -25,15 +28,15 @@ fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() -> crate fn directory_paths_are_created_in_full() { let (mut cache, _tmp) = new_cache(); - for (name, is_dir) in &[ - ("dir", Some(true)), - ("submodule", Some(true)), - ("file", Some(false)), - ("exe", Some(false)), + for (name, mode) in [ + ("dir", IS_DIR), + ("submodule", IS_DIR), + ("file", IS_FILE), + ("exe", IS_FILE), ("link", None), ] { let path = cache - .at_path(Path::new("dir").join(name), *is_dir, &gix_object::find::Never) + .at_path(Path::new("dir").join(name), mode, &gix_object::find::Never) .unwrap() .path(); assert!(path.parent().unwrap().is_dir(), "dir exists"); @@ -47,7 +50,7 @@ fn existing_directories_are_fine() -> crate::Result { let (mut cache, tmp) = new_cache(); std::fs::create_dir(tmp.path().join("dir"))?; - let path = cache.at_path("dir/file", Some(false), &gix_object::find::Never)?.path(); + let path = cache.at_path("dir/file", IS_FILE, &gix_object::find::Never)?.path(); assert!(path.parent().unwrap().is_dir(), "directory is still present"); assert!(!path.exists(), "it won't create the file"); assert_eq!(cache.statistics().delegate.num_mkdir_calls, 1); @@ -59,7 +62,7 @@ fn validation_to_each_component() -> crate::Result { let (mut cache, tmp) = new_cache(); let err = cache - .at_path("valid/.gIt", Some(false), &gix_object::find::Never) + .at_path("valid/.gIt", IS_FILE, &gix_object::find::Never) .unwrap_err(); assert_eq!( cache.statistics().delegate.num_mkdir_calls, @@ -89,7 +92,7 @@ fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> crate::R let relative_path = format!("{dirname}/file"); assert_eq!( cache - .at_path(&relative_path, Some(false), &gix_object::find::Never) + .at_path(&relative_path, IS_FILE, &gix_object::find::Never) .unwrap_err() .kind(), std::io::ErrorKind::AlreadyExists @@ -109,9 +112,7 @@ fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> crate::R *unlink_on_collision = true; } let relative_path = format!("{dirname}/file"); - let path = cache - .at_path(&relative_path, Some(false), &gix_object::find::Never)? - .path(); + let path = cache.at_path(&relative_path, IS_FILE, &gix_object::find::Never)?.path(); assert!(path.parent().unwrap().is_dir(), "directory was forcefully created"); assert!(!path.exists()); } diff --git a/gix-worktree/tests/worktree/stack/ignore.rs b/gix-worktree/tests/worktree/stack/ignore.rs index 6e578f352d8..7ea4fbcebe5 100644 --- a/gix-worktree/tests/worktree/stack/ignore.rs +++ b/gix-worktree/tests/worktree/stack/ignore.rs @@ -1,5 +1,7 @@ use bstr::{BStr, ByteSlice}; +use gix_index::entry::Mode; use gix_worktree::{stack::state::ignore::Source, Stack}; +use std::fs::Metadata; use crate::{hex_to_id, worktree::stack::probe_case}; @@ -62,7 +64,7 @@ fn exclude_by_dir_is_handled_just_like_git() { for (relative_entry, source_and_line) in expectations { let (source, line, expected_pattern) = source_and_line.expect("every value is matched"); let relative_path = gix_path::from_byte_slice(relative_entry); - let is_dir = dir.join(relative_path).metadata().ok().map(|m| m.is_dir()); + let is_dir = dir.join(relative_path).metadata().ok().map(metadata_to_mode); let platform = cache.at_entry(relative_entry, is_dir, &FindError).unwrap(); let match_ = platform.matching_exclude_pattern().expect("match all values"); @@ -87,6 +89,14 @@ fn exclude_by_dir_is_handled_just_like_git() { } } +fn metadata_to_mode(meta: Metadata) -> Mode { + if meta.is_dir() { + gix_index::entry::Mode::DIR + } else { + gix_index::entry::Mode::FILE + } +} + #[test] fn check_against_baseline() -> crate::Result { let dir = gix_testtools::scripted_fixture_read_only_standalone("make_ignore_and_attributes_setup.sh")?; @@ -127,7 +137,7 @@ fn check_against_baseline() -> crate::Result { }; for (relative_entry, source_and_line) in expectations { let relative_path = gix_path::from_byte_slice(relative_entry); - let is_dir = worktree_dir.join(relative_path).metadata().ok().map(|m| m.is_dir()); + let is_dir = worktree_dir.join(relative_path).metadata().ok().map(metadata_to_mode); let platform = cache.at_entry(relative_entry, is_dir, &odb)?; From 956469944d14bc0e5b16e3f95d407bb8b2282903 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 17 May 2024 10:24:03 +0200 Subject: [PATCH 06/50] feat: add `From for gix_index::entry::Mode`. --- gix-index/src/entry/mode.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gix-index/src/entry/mode.rs b/gix-index/src/entry/mode.rs index fca861d2b18..dc3a9a6de94 100644 --- a/gix-index/src/entry/mode.rs +++ b/gix-index/src/entry/mode.rs @@ -67,6 +67,12 @@ impl Mode { } } +impl From for Mode { + fn from(value: gix_object::tree::EntryMode) -> Self { + Self::from_bits_truncate(value.0 as u32) + } +} + /// A change of a [`Mode`]. pub enum Change { /// The type of mode changed, like symlink => file. From 1ca6a3ce22887c7eb42ec3e0a19f6e1202715745 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 17 May 2024 09:37:20 +0200 Subject: [PATCH 07/50] adapt to changes in `gix-worktree` --- gitoxide-core/src/lib.rs | 8 ++++++ .../src/repository/attributes/query.rs | 15 ++++++----- .../attributes/validate_baseline.rs | 2 +- gitoxide-core/src/repository/exclude.rs | 16 +++++------ gitoxide-core/src/repository/index/entries.rs | 3 ++- .../src/repository/revision/resolve.rs | 3 +-- gix-archive/tests/archive.rs | 2 +- gix-diff/src/blob/platform.rs | 16 +++++------ gix-diff/tests/blob/pipeline.rs | 16 +++++------ gix-dir/src/walk/classify.rs | 24 ++++++++++++++--- gix-filter/tests/pipeline/convert_to_git.rs | 6 ++--- .../tests/pipeline/convert_to_worktree.rs | 6 ++--- gix-status/src/index_as_worktree/function.rs | 14 ++++++++-- .../src/index_as_worktree_with_renames/mod.rs | 12 +++++++-- gix-worktree-state/src/checkout/entry.rs | 3 +-- gix-worktree-state/src/checkout/function.rs | 6 ++++- gix-worktree-state/src/checkout/mod.rs | 2 ++ gix-worktree-stream/tests/stream.rs | 4 +-- gix/src/attribute_stack.rs | 27 ++++++++----------- gix/src/config/cache/access.rs | 1 + gix/src/filter.rs | 6 ++--- gix/src/pathspec.rs | 20 ++++++++++++-- gix/src/repository/dirwalk.rs | 10 ++++++- gix/src/repository/worktree.rs | 2 +- gix/src/submodule/mod.rs | 10 ++++++- 25 files changed, 156 insertions(+), 78 deletions(-) diff --git a/gitoxide-core/src/lib.rs b/gitoxide-core/src/lib.rs index 2cf788a994a..9ff7ab73760 100644 --- a/gitoxide-core/src/lib.rs +++ b/gitoxide-core/src/lib.rs @@ -84,3 +84,11 @@ pub use discover::discover; #[cfg(all(feature = "async-client", feature = "blocking-client"))] compile_error!("Cannot set both 'blocking-client' and 'async-client' features as they are mutually exclusive"); + +fn is_dir_to_mode(is_dir: bool) -> gix::index::entry::Mode { + if is_dir { + gix::index::entry::Mode::DIR + } else { + gix::index::entry::Mode::FILE + } +} diff --git a/gitoxide-core/src/repository/attributes/query.rs b/gitoxide-core/src/repository/attributes/query.rs index b3443cdec95..9081eb9ed78 100644 --- a/gitoxide-core/src/repository/attributes/query.rs +++ b/gitoxide-core/src/repository/attributes/query.rs @@ -14,6 +14,7 @@ pub(crate) mod function { use gix::bstr::BStr; use crate::{ + is_dir_to_mode, repository::{ attributes::query::{attributes_cache, Options}, PathsOrPatterns, @@ -38,12 +39,12 @@ pub(crate) mod function { match input { PathsOrPatterns::Paths(paths) => { for path in paths { - let is_dir = gix::path::from_bstr(Cow::Borrowed(path.as_ref())) + let mode = gix::path::from_bstr(Cow::Borrowed(path.as_ref())) .metadata() .ok() - .map(|m| m.is_dir()); + .map(|m| is_dir_to_mode(m.is_dir())); - let entry = cache.at_entry(path.as_slice(), is_dir)?; + let entry = cache.at_entry(path.as_slice(), mode)?; if !entry.matching_attributes(&mut matches) { continue; } @@ -61,9 +62,9 @@ pub(crate) mod function { )?; let mut pathspec_matched_entry = false; if let Some(it) = pathspec.index_entries_with_paths(&index) { - for (path, _entry) in it { + for (path, entry) in it { pathspec_matched_entry = true; - let entry = cache.at_entry(path, Some(false))?; + let entry = cache.at_entry(path, entry.mode.into())?; if !entry.matching_attributes(&mut matches) { continue; } @@ -87,10 +88,10 @@ pub(crate) mod function { let path = pattern.path(); let entry = cache.at_entry( path, - Some( + Some(is_dir_to_mode( workdir.map_or(false, |wd| wd.join(gix::path::from_bstr(path)).is_dir()) || pattern.signature.contains(gix::pathspec::MagicSignature::MUST_BE_DIR), - ), + )), )?; if !entry.matching_attributes(&mut matches) { continue; diff --git a/gitoxide-core/src/repository/attributes/validate_baseline.rs b/gitoxide-core/src/repository/attributes/validate_baseline.rs index a5571d45e86..77eeb258a49 100644 --- a/gitoxide-core/src/repository/attributes/validate_baseline.rs +++ b/gitoxide-core/src/repository/attributes/validate_baseline.rs @@ -192,7 +192,7 @@ pub(crate) mod function { ); for (rela_path, baseline) in rx_base { - let entry = cache.at_entry(rela_path.as_str(), Some(false))?; + let entry = cache.at_entry(rela_path.as_str(), None)?; match baseline { Baseline::Attribute { assignments: expected } => { entry.matching_attributes(&mut matches); diff --git a/gitoxide-core/src/repository/exclude.rs b/gitoxide-core/src/repository/exclude.rs index ac837fe0303..a0cd212d08e 100644 --- a/gitoxide-core/src/repository/exclude.rs +++ b/gitoxide-core/src/repository/exclude.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, io}; use anyhow::bail; use gix::bstr::BStr; -use crate::{repository::PathsOrPatterns, OutputFormat}; +use crate::{is_dir_to_mode, repository::PathsOrPatterns, OutputFormat}; pub mod query { use std::ffi::OsString; @@ -44,11 +44,11 @@ pub fn query( match input { PathsOrPatterns::Paths(paths) => { for path in paths { - let is_dir = gix::path::from_bstr(Cow::Borrowed(path.as_ref())) + let mode = gix::path::from_bstr(Cow::Borrowed(path.as_ref())) .metadata() .ok() - .map(|m| m.is_dir()); - let entry = cache.at_entry(path.as_slice(), is_dir)?; + .map(|m| is_dir_to_mode(m.is_dir())); + let entry = cache.at_entry(path.as_slice(), mode)?; let match_ = entry .matching_exclude_pattern() .and_then(|m| (show_ignore_patterns || !m.pattern.is_negative()).then_some(m)); @@ -66,9 +66,9 @@ pub fn query( )?; if let Some(it) = pathspec.index_entries_with_paths(&index) { - for (path, _entry) in it { + for (path, entry) in it { pathspec_matched_something = true; - let entry = cache.at_entry(path, Some(false))?; + let entry = cache.at_entry(path, entry.mode.into())?; let match_ = entry .matching_exclude_pattern() .and_then(|m| (show_ignore_patterns || !m.pattern.is_negative()).then_some(m)); @@ -92,10 +92,10 @@ pub fn query( let path = pattern.path(); let entry = cache.at_entry( path, - Some( + Some(is_dir_to_mode( workdir.map_or(false, |wd| wd.join(gix::path::from_bstr(path)).is_dir()) || pattern.signature.contains(gix::pathspec::MagicSignature::MUST_BE_DIR), - ), + )), )?; let match_ = entry .matching_exclude_pattern() diff --git a/gitoxide-core/src/repository/index/entries.rs b/gitoxide-core/src/repository/index/entries.rs index 3f17d55b3c3..4485bad5c3e 100644 --- a/gitoxide-core/src/repository/index/entries.rs +++ b/gitoxide-core/src/repository/index/entries.rs @@ -31,6 +31,7 @@ pub(crate) mod function { }; use crate::{ + is_dir_to_mode, repository::index::entries::{Attributes, Options}, OutputFormat, }; @@ -174,7 +175,7 @@ pub(crate) mod function { } // The user doesn't want attributes, so we set the cache position on demand only None => cache - .at_entry(rela_path, Some(is_dir)) + .at_entry(rela_path, Some(is_dir_to_mode(is_dir))) .ok() .map(|platform| platform.matching_attributes(out)) .unwrap_or_default(), diff --git a/gitoxide-core/src/repository/revision/resolve.rs b/gitoxide-core/src/repository/revision/resolve.rs index b4494e9e046..6e8a2b1ad65 100644 --- a/gitoxide-core/src/repository/revision/resolve.rs +++ b/gitoxide-core/src/repository/revision/resolve.rs @@ -127,11 +127,10 @@ pub(crate) mod function { } gix::object::Kind::Blob if cache.is_some() && spec.path_and_mode().is_some() => { let (path, mode) = spec.path_and_mode().expect("is present"); - let is_dir = Some(mode.is_tree()); match cache.expect("is some") { (BlobFormat::Git, _) => unreachable!("no need for a cache when querying object db"), (BlobFormat::Worktree, cache) => { - let platform = cache.attr_stack.at_entry(path, is_dir, &repo.objects)?; + let platform = cache.attr_stack.at_entry(path, Some(mode.into()), &repo.objects)?; let object = id.object()?; let mut converted = cache.filter.worktree_filter.convert_to_worktree( &object.data, diff --git a/gix-archive/tests/archive.rs b/gix-archive/tests/archive.rs index c9a4e8bb281..a1b9920bb61 100644 --- a/gix-archive/tests/archive.rs +++ b/gix-archive/tests/archive.rs @@ -233,7 +233,7 @@ mod from_tree { noop_pipeline(), move |rela_path, mode, attrs| { cache - .at_entry(rela_path, mode.is_tree().into(), &odb) + .at_entry(rela_path, Some(mode.into()), &odb) .map(|entry| entry.matching_attributes(attrs)) .map(|_| ()) }, diff --git a/gix-diff/src/blob/platform.rs b/gix-diff/src/blob/platform.rs index 091c5a9cf3c..41b4cc928ea 100644 --- a/gix-diff/src/blob/platform.rs +++ b/gix-diff/src/blob/platform.rs @@ -583,14 +583,14 @@ impl Platform { if self.diff_cache.contains_key(storage) { return Ok(()); } - let entry = self - .attr_stack - .at_entry(rela_path, Some(false), objects) - .map_err(|err| set_resource::Error::Attributes { - source: err, - kind, - rela_path: rela_path.to_owned(), - })?; + let entry = + self.attr_stack + .at_entry(rela_path, None, objects) + .map_err(|err| set_resource::Error::Attributes { + source: err, + kind, + rela_path: rela_path.to_owned(), + })?; let mut buf = Vec::new(); let out = self.filter.convert_to_diffable( &id, diff --git a/gix-diff/tests/blob/pipeline.rs b/gix-diff/tests/blob/pipeline.rs index fb1ef355715..f4cfeba5d1b 100644 --- a/gix-diff/tests/blob/pipeline.rs +++ b/gix-diff/tests/blob/pipeline.rs @@ -507,7 +507,7 @@ pub(crate) mod convert_to_diffable { assert_eq!(out.data, Some(pipeline::Data::Binary { size: 11 })); assert_eq!(buf.len(), 0, "buffers are cleared even if we read them"); - let platform = attributes.at_entry("c", Some(false), &gix_object::find::Never)?; + let platform = attributes.at_entry("c", None, &gix_object::find::Never)?; let id = db.insert("b"); let out = filter.convert_to_diffable( @@ -589,7 +589,7 @@ pub(crate) mod convert_to_diffable { let mut db = ObjectDb::default(); let null = gix_hash::Kind::Sha1.null(); let mut buf = Vec::new(); - let platform = attributes.at_entry("a", Some(false), &gix_object::find::Never)?; + let platform = attributes.at_entry("a", None, &gix_object::find::Never)?; let worktree_modes = [ pipeline::Mode::ToWorktreeAndBinaryToText, pipeline::Mode::ToGitUnlessBinaryToTextIsPresent, @@ -672,7 +672,7 @@ pub(crate) mod convert_to_diffable { "no filter was applied in this mode, also when using the ODB" ); - let platform = attributes.at_entry("missing", Some(false), &gix_object::find::Never)?; + let platform = attributes.at_entry("missing", None, &gix_object::find::Never)?; for mode in all_modes { buf.push(1); let out = filter.convert_to_diffable( @@ -731,7 +731,7 @@ pub(crate) mod convert_to_diffable { ); } - let platform = attributes.at_entry("b", Some(false), &gix_object::find::Never)?; + let platform = attributes.at_entry("b", None, &gix_object::find::Never)?; for mode in all_modes { buf.push(1); let out = filter.convert_to_diffable( @@ -781,7 +781,7 @@ pub(crate) mod convert_to_diffable { assert_eq!(buf.len(), 0, "it's always cleared before any potential use"); } - let platform = attributes.at_entry("c", Some(false), &gix_object::find::Never)?; + let platform = attributes.at_entry("c", None, &gix_object::find::Never)?; for mode in worktree_modes { let out = filter.convert_to_diffable( &null, @@ -827,7 +827,7 @@ pub(crate) mod convert_to_diffable { ); } - let platform = attributes.at_entry("unset", Some(false), &gix_object::find::Never)?; + let platform = attributes.at_entry("unset", None, &gix_object::find::Never)?; for mode in all_modes { let out = filter.convert_to_diffable( &null, @@ -879,7 +879,7 @@ pub(crate) mod convert_to_diffable { assert_eq!(buf.len(), 0); } - let platform = attributes.at_entry("d", Some(false), &gix_object::find::Never)?; + let platform = attributes.at_entry("d", None, &gix_object::find::Never)?; let id = db.insert("d-in-db"); for mode in worktree_modes { let out = filter.convert_to_diffable( @@ -923,7 +923,7 @@ pub(crate) mod convert_to_diffable { ); } - let platform = attributes.at_entry("e-no-attr", Some(false), &gix_object::find::Never)?; + let platform = attributes.at_entry("e-no-attr", None, &gix_object::find::Never)?; let out = filter.convert_to_diffable( &null, EntryKind::Blob, diff --git a/gix-dir/src/walk/classify.rs b/gix-dir/src/walk/classify.rs index a57319535ba..6d5940f8bdb 100644 --- a/gix-dir/src/walk/classify.rs +++ b/gix-dir/src/walk/classify.rs @@ -161,7 +161,17 @@ pub fn path( .as_mut() .map_or(Ok(None), |stack| { stack - .at_entry(rela_path.as_bstr(), disk_kind.map(|ft| ft.is_dir()), ctx.objects) + .at_entry( + rela_path.as_bstr(), + disk_kind.map(|ft| { + if ft.is_dir() { + gix_index::entry::Mode::DIR + } else { + gix_index::entry::Mode::FILE + } + }), + ctx.objects, + ) .map(|platform| platform.excluded_kind()) }) .map_err(Error::ExcludesAccess)? @@ -203,9 +213,9 @@ pub fn path( && ctx.excludes.is_some() && kind.map_or(false, |ft| ft == entry::Kind::Symlink) { - path.metadata().ok().map(|md| md.is_dir()).or(Some(false)) + path.metadata().ok().map(|md| is_dir_to_mode(md.is_dir())) } else { - kind.map(|ft| ft.is_dir()) + kind.map(|ft| is_dir_to_mode(ft.is_dir())) }; let mut maybe_upgrade_to_repository = |current_kind, find_harder: bool| { @@ -408,3 +418,11 @@ fn is_eq(lhs: &BStr, rhs: impl AsRef, ignore_case: bool) -> bool { lhs == rhs.as_ref() } } + +fn is_dir_to_mode(is_dir: bool) -> gix_index::entry::Mode { + if is_dir { + gix_index::entry::Mode::DIR + } else { + gix_index::entry::Mode::FILE + } +} diff --git a/gix-filter/tests/pipeline/convert_to_git.rs b/gix-filter/tests/pipeline/convert_to_git.rs index 79d79993238..5cff5c888e3 100644 --- a/gix-filter/tests/pipeline/convert_to_git.rs +++ b/gix-filter/tests/pipeline/convert_to_git.rs @@ -53,7 +53,7 @@ fn all_stages_mean_streaming_is_impossible() -> gix_testtools::Result { Path::new("any.txt"), &mut |path, attrs| { cache - .at_entry(path, Some(false), &gix_object::find::Never) + .at_entry(path, None, &gix_object::find::Never) .expect("cannot fail") .matching_attributes(attrs); }, @@ -82,7 +82,7 @@ fn only_driver_means_streaming_is_possible() -> gix_testtools::Result { Path::new("subdir/doesnot/matter/any.txt"), &mut |path, attrs| { cache - .at_entry(path, Some(false), &gix_object::find::Never) + .at_entry(path, None, &gix_object::find::Never) .expect("cannot fail") .matching_attributes(attrs); }, @@ -112,7 +112,7 @@ fn no_filter_means_reader_is_returned_unchanged() -> gix_testtools::Result { Path::new("other.txt"), &mut |path, attrs| { cache - .at_entry(path, Some(false), &gix_object::find::Never) + .at_entry(path, None, &gix_object::find::Never) .expect("cannot fail") .matching_attributes(attrs); }, diff --git a/gix-filter/tests/pipeline/convert_to_worktree.rs b/gix-filter/tests/pipeline/convert_to_worktree.rs index 2449c50304a..be757e01ab8 100644 --- a/gix-filter/tests/pipeline/convert_to_worktree.rs +++ b/gix-filter/tests/pipeline/convert_to_worktree.rs @@ -21,7 +21,7 @@ fn all_stages() -> gix_testtools::Result { "any.txt".into(), &mut |path, attrs| { cache - .at_entry(path, Some(false), &gix_object::find::Never) + .at_entry(path, None, &gix_object::find::Never) .expect("cannot fail") .matching_attributes(attrs); }, @@ -54,7 +54,7 @@ fn all_stages_no_filter() -> gix_testtools::Result { "other.txt".into(), &mut |path, attrs| { cache - .at_entry(path, Some(false), &gix_object::find::Never) + .at_entry(path, None, &gix_object::find::Never) .expect("cannot fail") .matching_attributes(attrs); }, @@ -86,7 +86,7 @@ fn no_filter() -> gix_testtools::Result { "other.txt".into(), &mut |path, attrs| { cache - .at_entry(path, Some(false), &gix_object::find::Never) + .at_entry(path, None, &gix_object::find::Never) .expect("cannot fail") .matching_attributes(attrs); }, diff --git a/gix-status/src/index_as_worktree/function.rs b/gix-status/src/index_as_worktree/function.rs index dbe7a838ed2..29ad4de69b4 100644 --- a/gix-status/src/index_as_worktree/function.rs +++ b/gix-status/src/index_as_worktree/function.rs @@ -276,7 +276,15 @@ impl<'index> State<'_, 'index> { &mut |relative_path, case, is_dir, out| { self.attr_stack .set_case(case) - .at_entry(relative_path, Some(is_dir), objects) + .at_entry( + relative_path, + Some(if is_dir { + gix_index::entry::Mode::DIR + } else { + gix_index::entry::Mode::FILE + }), + objects, + ) .map_or(false, |platform| platform.matching_attributes(out)) }, ) @@ -541,7 +549,9 @@ where } } else { self.buf.clear(); - let platform = self.attr_stack.at_entry(self.rela_path, Some(false), &self.objects)?; + let platform = self + .attr_stack + .at_entry(self.rela_path, Some(self.entry.mode), &self.objects)?; let file = std::fs::File::open(self.path)?; let out = self .filter diff --git a/gix-status/src/index_as_worktree_with_renames/mod.rs b/gix-status/src/index_as_worktree_with_renames/mod.rs index 0932e7d4f4d..0c9a8c44463 100644 --- a/gix-status/src/index_as_worktree_with_renames/mod.rs +++ b/gix-status/src/index_as_worktree_with_renames/mod.rs @@ -99,7 +99,15 @@ pub(super) mod function { .expect("can only be called if attributes are used in patterns"); stack .set_case(case) - .at_entry(relative_path, Some(is_dir), &objects) + .at_entry( + relative_path, + Some(if is_dir { + gix_index::entry::Mode::DIR + } else { + gix_index::entry::Mode::FILE + }), + &objects, + ) .map_or(false, |platform| platform.matching_attributes(out)) }, excludes: excludes.as_mut(), @@ -494,7 +502,7 @@ pub(super) mod function { Ok(match kind { Kind::File => { let platform = attrs - .at_entry(rela_path, Some(false), objects) + .at_entry(rela_path, None, objects) .map_err(Error::SetAttributeContext)?; let rela_path = gix_path::from_bstr(rela_path); let file_path = worktree_root.join(rela_path.as_ref()); diff --git a/gix-worktree-state/src/checkout/entry.rs b/gix-worktree-state/src/checkout/entry.rs index 77db18daa1e..b08563c60c1 100644 --- a/gix-worktree-state/src/checkout/entry.rs +++ b/gix-worktree-state/src/checkout/entry.rs @@ -80,8 +80,7 @@ where let dest_relative = gix_path::try_from_bstr(entry_path).map_err(|_| crate::checkout::Error::IllformedUtf8 { path: entry_path.to_owned(), })?; - let is_dir = Some(entry.mode == gix_index::entry::Mode::COMMIT || entry.mode == gix_index::entry::Mode::DIR); - let path_cache = path_cache.at_path(dest_relative, is_dir, &*objects)?; + let path_cache = path_cache.at_path(dest_relative, Some(entry.mode), &*objects)?; let dest = path_cache.path(); let object_size = match entry.mode { diff --git a/gix-worktree-state/src/checkout/function.rs b/gix-worktree-state/src/checkout/function.rs index 9046af47110..6342f34cd59 100644 --- a/gix-worktree-state/src/checkout/function.rs +++ b/gix-worktree-state/src/checkout/function.rs @@ -64,7 +64,11 @@ where path_cache: Stack::from_state_and_ignore_case( dir, options.fs.ignore_case, - stack::State::for_checkout(options.overwrite_existing, std::mem::take(&mut options.attributes)), + stack::State::for_checkout( + options.overwrite_existing, + options.validate, + std::mem::take(&mut options.attributes), + ), index, paths, ), diff --git a/gix-worktree-state/src/checkout/mod.rs b/gix-worktree-state/src/checkout/mod.rs index 0206e7e3408..57a0d3b5f2a 100644 --- a/gix-worktree-state/src/checkout/mod.rs +++ b/gix-worktree-state/src/checkout/mod.rs @@ -41,6 +41,8 @@ pub struct Outcome { pub struct Options { /// capabilities of the file system pub fs: gix_fs::Capabilities, + /// Options to configure how to validate path components. + pub validate: gix_worktree::validate::path::component::Options, /// If set, don't use more than this amount of threads. /// Otherwise, usually use as many threads as there are logical cores. /// A value of 0 is interpreted as no-limit diff --git a/gix-worktree-stream/tests/stream.rs b/gix-worktree-stream/tests/stream.rs index 3c2964e1918..56a211d5600 100644 --- a/gix-worktree-stream/tests/stream.rs +++ b/gix-worktree-stream/tests/stream.rs @@ -67,7 +67,7 @@ mod from_tree { mutating_pipeline(true), move |rela_path, mode, attrs| { cache - .at_entry(rela_path, mode.is_tree().into(), &odb) + .at_entry(rela_path, Some(mode.into()), &odb) .map(|entry| entry.matching_attributes(attrs)) .map(|_| ()) }, @@ -225,7 +225,7 @@ mod from_tree { mutating_pipeline(false), move |rela_path, mode, attrs| { cache - .at_entry(rela_path, mode.is_tree().into(), &odb) + .at_entry(rela_path, Some(mode.into()), &odb) .map(|entry| entry.matching_attributes(attrs)) .map(|_| ()) }, diff --git a/gix/src/attribute_stack.rs b/gix/src/attribute_stack.rs index e2b9ecc5ce6..582a41dc2ff 100644 --- a/gix/src/attribute_stack.rs +++ b/gix/src/attribute_stack.rs @@ -33,34 +33,29 @@ impl DerefMut for AttributeStack<'_> { /// Platform retrieval impl<'repo> AttributeStack<'repo> { - /// Append the `relative` path to the root directory of the cache and efficiently create leading directories, while assuring that no - /// symlinks are in that path. - /// Unless `is_dir` is known with `Some(โ€ฆ)`, then `relative` points to a directory itself in which case the entire resulting - /// path is created as directory. If it's not known it is assumed to be a file. + /// Append the `relative` path to the root directory of the cache and load all attribute or ignore files on the way as needed. + /// Use `mode` to specify what kind of item lives at `relative` - directories may match against rules specifically . + /// If `mode` is `None`, the item at `relative` is assumed to be a file. /// - /// Provide access to cached information for that `relative` path via the returned platform. + /// The returned platform may be used to access the actual attribute or ignore information. #[doc(alias = "is_path_ignored", alias = "git2")] pub fn at_path( &mut self, relative: impl AsRef, - is_dir: Option, + mode: Option, ) -> std::io::Result> { - self.inner.at_path(relative, is_dir, &self.repo.objects) + self.inner.at_path(relative, mode, &self.repo.objects) } - /// Obtain a platform for lookups from a repo-`relative` path, typically obtained from an index entry. `is_dir` should reflect - /// whether it's a directory or not, or left at `None` if unknown. + /// Obtain a platform for attribute or ignore lookups from a repo-`relative` path, typically obtained from an index entry. + /// `mode` should reflect whether it's a directory or not, or left at `None` if unknown. /// - /// If `relative` ends with `/` and `is_dir` is `None`, it is automatically assumed to be a directory. - /// - /// ### Panics - /// - /// - on illformed UTF8 in `relative` + /// If `relative` ends with `/` and `mode` is `None`, it is automatically assumed to be a directory. pub fn at_entry<'r>( &mut self, relative: impl Into<&'r BStr>, - is_dir: Option, + mode: Option, ) -> std::io::Result> { - self.inner.at_entry(relative, is_dir, &self.repo.objects) + self.inner.at_entry(relative, mode, &self.repo.objects) } } diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index f35699aa09d..1bb3e0d7e93 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -310,6 +310,7 @@ impl Cache { }; Ok(gix_worktree_state::checkout::Options { filter_process_delay, + validate: Default::default(), // TODO: derive these from configuration filters, attributes: self .assemble_attribute_globals(git_dir, attributes_source, self.attributes)? diff --git a/gix/src/filter.rs b/gix/src/filter.rs index c856fe521da..19df012818e 100644 --- a/gix/src/filter.rs +++ b/gix/src/filter.rs @@ -135,7 +135,7 @@ impl<'repo> Pipeline<'repo> { impl<'repo> Pipeline<'repo> { /// Convert a `src` stream (to be found at `rela_path`, a repo-relative path) to a representation suitable for storage in `git` /// by using all attributes at `rela_path` and configuration of the repository to know exactly which filters apply. - /// `index` is used in particularly rare cases where the CRLF filter in auto-mode tries to determine whether or not to apply itself, + /// `index` is used in particularly rare cases where the CRLF filter in auto-mode tries to determine whether to apply itself, /// and it should match the state used when [instantiating this instance][Self::new()]. /// Note that the return-type implements [`std::io::Read`]. pub fn convert_to_git( @@ -147,7 +147,7 @@ impl<'repo> Pipeline<'repo> { where R: std::io::Read, { - let entry = self.cache.at_path(rela_path, Some(false), &self.repo.objects)?; + let entry = self.cache.at_path(rela_path, None, &self.repo.objects)?; Ok(self.inner.convert_to_git( src, rela_path, @@ -179,7 +179,7 @@ impl<'repo> Pipeline<'repo> { can_delay: gix_filter::driver::apply::Delay, ) -> Result, pipeline::convert_to_worktree::Error> { - let entry = self.cache.at_entry(rela_path, Some(false), &self.repo.objects)?; + let entry = self.cache.at_entry(rela_path, None, &self.repo.objects)?; Ok(self.inner.convert_to_worktree( src, rela_path, diff --git a/gix/src/pathspec.rs b/gix/src/pathspec.rs index f501be621f8..e2242609268 100644 --- a/gix/src/pathspec.rs +++ b/gix/src/pathspec.rs @@ -137,7 +137,15 @@ impl<'repo> Pathspec<'repo> { let stack = self.stack.as_mut().expect("initialized in advance"); stack .set_case(case) - .at_entry(relative_path, Some(is_dir), &self.repo.objects) + .at_entry( + relative_path, + Some(if is_dir { + gix_index::entry::Mode::DIR + } else { + gix_index::entry::Mode::FILE + }), + &self.repo.objects, + ) .map_or(false, |platform| platform.matching_attributes(out)) }, ) @@ -193,7 +201,15 @@ impl PathspecDetached { let stack = self.stack.as_mut().expect("initialized in advance"); stack .set_case(case) - .at_entry(relative_path, Some(is_dir), &self.odb) + .at_entry( + relative_path, + Some(if is_dir { + gix_index::entry::Mode::DIR + } else { + gix_index::entry::Mode::FILE + }), + &self.odb, + ) .map_or(false, |platform| platform.matching_attributes(out)) }, ) diff --git a/gix/src/repository/dirwalk.rs b/gix/src/repository/dirwalk.rs index c2600227a33..762c69def60 100644 --- a/gix/src/repository/dirwalk.rs +++ b/gix/src/repository/dirwalk.rs @@ -64,7 +64,15 @@ impl Repository { .expect("can only be called if attributes are used in patterns"); stack .set_case(case) - .at_entry(relative_path, Some(is_dir), &self.objects) + .at_entry( + relative_path, + Some(if is_dir { + gix_index::entry::Mode::DIR + } else { + gix_index::entry::Mode::FILE + }), + &self.objects, + ) .map_or(false, |platform| platform.matching_attributes(out)) }, excludes: Some(&mut excludes.inner), diff --git a/gix/src/repository/worktree.rs b/gix/src/repository/worktree.rs index 529243896ea..f24673b296e 100644 --- a/gix/src/repository/worktree.rs +++ b/gix/src/repository/worktree.rs @@ -86,7 +86,7 @@ impl crate::Repository { objects.clone(), pipeline, move |path, mode, attrs| -> std::io::Result<()> { - let entry = cache.at_entry(path, Some(mode.is_tree()), &objects)?; + let entry = cache.at_entry(path, Some(mode.into()), &objects)?; entry.matching_attributes(attrs); Ok(()) }, diff --git a/gix/src/submodule/mod.rs b/gix/src/submodule/mod.rs index 0e000eb8033..a530ba6b036 100644 --- a/gix/src/submodule/mod.rs +++ b/gix/src/submodule/mod.rs @@ -154,7 +154,15 @@ impl<'repo> Submodule<'repo> { &mut |relative_path, case, is_dir, out| { attributes .set_case(case) - .at_entry(relative_path, Some(is_dir), &self.state.repo.objects) + .at_entry( + relative_path, + Some(if is_dir { + gix_index::entry::Mode::DIR + } else { + gix_index::entry::Mode::FILE + }), + &self.state.repo.objects, + ) .map_or(false, |platform| platform.matching_attributes(out)) } })?; From 886d6b58e4612ac21cc660ea4ddf1dd0b49d1c6e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 17 May 2024 11:06:01 +0200 Subject: [PATCH 08/50] feat: checkout respects options for `core.protectHFS` and `core.protectNTFS`. This also adds `gitoxide.core.protectWindows` as a way to enforce additional restrictions that are usually only available on Windows. Note that `core.protectNFS` is always enabled by default, just like [it is in Git](https://github.com/git/git/commit/9102f958ee5254b10c0be72672aa3305bf4f4704). --- gix/src/config/cache/access.rs | 31 +++++++++++++++++++++++- gix/src/config/tree/sections/core.rs | 6 +++++ gix/src/config/tree/sections/gitoxide.rs | 5 ++++ src/plumbing/progress.rs | 8 ------ 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index 1bb3e0d7e93..c44b12f3e74 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -271,6 +271,35 @@ impl Cache { }) } + fn protect_options(&self) -> Result { + const IS_WINDOWS: bool = cfg!(windows); + const IS_MACOS: bool = cfg!(target_os = "macos"); + const ALWAYS_ON_FOR_SAFETY: bool = true; + Ok(gix_validate::path::component::Options { + protect_windows: config::tree::gitoxide::Core::PROTECT_WINDOWS + .enrich_error( + self.resolved + .boolean("gitoxide", Some("core".into()), "protectWindows") + .unwrap_or(Ok(IS_WINDOWS)), + ) + .with_lenient_default_value(self.lenient_config, IS_WINDOWS)?, + protect_hfs: config::tree::Core::PROTECT_HFS + .enrich_error( + self.resolved + .boolean("core", None, "protectHFS") + .unwrap_or(Ok(IS_MACOS)), + ) + .with_lenient_default_value(self.lenient_config, IS_MACOS)?, + protect_ntfs: config::tree::Core::PROTECT_NTFS + .enrich_error( + self.resolved + .boolean("core", None, "protectNTFS") + .unwrap_or(Ok(ALWAYS_ON_FOR_SAFETY)), + ) + .with_lenient_default_value(self.lenient_config, ALWAYS_ON_FOR_SAFETY)?, + }) + } + /// Collect everything needed to checkout files into a worktree. /// Note that some of the options being returned will be defaulted so safe settings, the caller might have to override them /// depending on the use-case. @@ -310,7 +339,7 @@ impl Cache { }; Ok(gix_worktree_state::checkout::Options { filter_process_delay, - validate: Default::default(), // TODO: derive these from configuration + validate: self.protect_options()?, filters, attributes: self .assemble_attribute_globals(git_dir, attributes_source, self.attributes)? diff --git a/gix/src/config/tree/sections/core.rs b/gix/src/config/tree/sections/core.rs index 5a63020b11c..d847b4ed75f 100644 --- a/gix/src/config/tree/sections/core.rs +++ b/gix/src/config/tree/sections/core.rs @@ -44,6 +44,10 @@ impl Core { /// Needs application to use [`env::args_os`][crate::env::args_os()] to conform all input paths before they are used. pub const PRECOMPOSE_UNICODE: keys::Boolean = keys::Boolean::new_boolean("precomposeUnicode", &config::Tree::CORE) .with_note("application needs to conform all program input by using gix::env::args_os()"); + /// The `core.protectHFS` key. + pub const PROTECT_HFS: keys::Boolean = keys::Boolean::new_boolean("protectHFS", &config::Tree::CORE); + /// The `core.protectNTFS` key. + pub const PROTECT_NTFS: keys::Boolean = keys::Boolean::new_boolean("protectNTFS", &config::Tree::CORE); /// The `core.repositoryFormatVersion` key. pub const REPOSITORY_FORMAT_VERSION: keys::UnsignedInteger = keys::UnsignedInteger::new_unsigned_integer("repositoryFormatVersion", &config::Tree::CORE); @@ -116,6 +120,8 @@ impl Section for Core { &Self::SYMLINKS, &Self::TRUST_C_TIME, &Self::WORKTREE, + &Self::PROTECT_HFS, + &Self::PROTECT_NTFS, &Self::ASKPASS, &Self::EXCLUDES_FILE, &Self::ATTRIBUTES_FILE, diff --git a/gix/src/config/tree/sections/gitoxide.rs b/gix/src/config/tree/sections/gitoxide.rs index a3b05441263..37c706af0e6 100644 --- a/gix/src/config/tree/sections/gitoxide.rs +++ b/gix/src/config/tree/sections/gitoxide.rs @@ -103,6 +103,10 @@ mod subsections { pub const USE_STDEV: keys::Boolean = keys::Boolean::new_boolean("useStdev", &Gitoxide::CORE) .with_note("A runtime version of the USE_STDEV build flag."); + /// The `gitoxide.core.protectWindows` key. + pub const PROTECT_WINDOWS: keys::Boolean = keys::Boolean::new_boolean("protectWindows", &Gitoxide::CORE) + .with_note("enable protections that are enabled by default on Windows"); + /// The `gitoxide.core.shallowFile` key. pub const SHALLOW_FILE: keys::Path = keys::Path::new_path("shallowFile", &Gitoxide::CORE) .with_environment_override("GIT_SHALLOW_FILE") @@ -142,6 +146,7 @@ mod subsections { &Self::USE_NSEC, &Self::USE_STDEV, &Self::SHALLOW_FILE, + &Self::PROTECT_WINDOWS, &Self::FILTER_PROCESS_DELAY, &Self::EXTERNAL_COMMAND_STDERR, &Self::REFS_NAMESPACE, diff --git a/src/plumbing/progress.rs b/src/plumbing/progress.rs index 09b85a66e7c..c05b7f7ee11 100644 --- a/src/plumbing/progress.rs +++ b/src/plumbing/progress.rs @@ -94,14 +94,6 @@ static GIT_CONFIG: &[Record] = &[ config: "core.loosecompression", usage: Planned("") }, - Record { - config: "core.protectHFS", - usage: Planned("relevant for checkout on MacOS, and possibly on networked drives") - }, - Record { - config: "core.protectNTFS", - usage: Planned("relevant for checkout on Windows, and possibly networked drives") - }, Record { config: "core.sparseCheckout", usage: Planned("we want to support huge repos and be the fastest in doing so") From b6a67d7fe3b5645fccb785af01d72919c6761c52 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 18 May 2024 17:11:05 +0200 Subject: [PATCH 09/50] doc: make clear that indices can contain invalid or dangerous paths. It's probably best not to try to protect against violations of constraints in this free-to-mutate data-structure and instead suggest to validate entry paths before using them on disk (or use the `gix_worktree::Stack`). --- gix-index/src/lib.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/gix-index/src/lib.rs b/gix-index/src/lib.rs index a8ff94c0369..b8adf78318e 100644 --- a/gix-index/src/lib.rs +++ b/gix-index/src/lib.rs @@ -113,6 +113,21 @@ pub struct AccelerateLookup<'a> { /// /// As opposed to a snapshot, it's meant to be altered and eventually be written back to disk or converted into a tree. /// We treat index and its state synonymous. +/// +/// # A note on safety +/// +/// An index (i.e. [`State`]) created [from a tree](State::from_tree()) is not guaranteed to have valid entry paths as those +/// depend on the names contained in trees entirely, without applying any level of validation. +/// +/// This means that before using these paths to recreate files on disk, *they must be validated*. +/// +/// It's notable that it's possible to manufacture tree objects which contain names like `.git/hooks/pre-commit` +/// which then will look like `.git/hooiks/pre-commit` in the index, which doesn't care that the name came from a single +/// tree instead of from trees named `.git`, `hooks` and a blob named `pre-commit`. The effect is still the same - an invalid +/// path is presented in the index and its consumer must validate each path component before usage. +/// +/// It's recommended to do that using `gix_worktree::Stack` which has it built-in if it's created `for_checkout()`. Alternatively +/// one can validate component names with `gix_validate::path::component()`. #[derive(Clone)] pub struct State { /// The kind of object hash used when storing the underlying file. From a67d82dce4346c6e76684b4405f33bec47d5bccd Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 18 May 2024 17:50:24 +0200 Subject: [PATCH 10/50] feat: defend against `CON` device names and more if `gitoxide.core.protectWindows` is enabled. Note that trailing `.` are forbidden for some reason, but trailing ` ` (space) is forbidden as it's just ignored when creating directories or files, allowing them to be clobbered and merged silently. --- gix-validate/src/path.rs | 55 ++++++++++++++++++++++++++++++++++ gix-validate/tests/path/mod.rs | 41 +++++++++++++++++++++++-- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/gix-validate/src/path.rs b/gix-validate/src/path.rs index 5a305c06a0b..e8eec023d00 100644 --- a/gix-validate/src/path.rs +++ b/gix-validate/src/path.rs @@ -13,6 +13,10 @@ pub mod component { PathSeparator, #[error("Window path prefixes are not allowed")] WindowsPathPrefix, + #[error("Windows device-names may have side-effects and are not allowed")] + WindowsReservedName, + #[error("Trailing spaces or dots and the following characters are forbidden in Windows paths, along with non-printable ones: <>:\"|?*")] + WindowsIllegalCharacter, #[error("The .git name may never be used")] DotGitDir, #[error("The .gitmodules file must not be a symlink")] @@ -101,6 +105,12 @@ pub fn component( if is_symlink(mode) && is_dot_ntfs(input, "gitmodules", "gi7eba") { return Err(component::Error::SymlinkedGitModules); } + + if protect_windows { + if let Some(err) = check_win_devices_and_illegal_characters(input) { + return Err(err); + } + } } if !(protect_hfs | protect_ntfs) { @@ -114,6 +124,44 @@ pub fn component( Ok(input) } +fn check_win_devices_and_illegal_characters(input: &BStr) -> Option { + let in3 = input.get(..3)?; + if in3.eq_ignore_ascii_case(b"aux") && is_done_windows(input.get(3..)) { + return Some(component::Error::WindowsReservedName); + } + if in3.eq_ignore_ascii_case(b"nul") && is_done_windows(input.get(3..)) { + return Some(component::Error::WindowsReservedName); + } + if in3.eq_ignore_ascii_case(b"prn") && is_done_windows(input.get(3..)) { + return Some(component::Error::WindowsReservedName); + } + if in3.eq_ignore_ascii_case(b"com") + && input.get(3).map_or(false, |n| *n >= b'1' && *n <= b'9') + && is_done_windows(input.get(4..)) + { + return Some(component::Error::WindowsReservedName); + } + if in3.eq_ignore_ascii_case(b"lpt") + && input.get(3).map_or(false, |n| n.is_ascii_digit()) + && is_done_windows(input.get(4..)) + { + return Some(component::Error::WindowsReservedName); + } + if in3.eq_ignore_ascii_case(b"con") + && ((input.get(3..6).map_or(false, |n| n.eq_ignore_ascii_case(b"in$")) && is_done_windows(input.get(6..))) + || (input.get(3..7).map_or(false, |n| n.eq_ignore_ascii_case(b"out$")) && is_done_windows(input.get(7..)))) + { + return Some(component::Error::WindowsReservedName); + } + if input.iter().find(|b| **b < 0x20 || b":<>\"|?*".contains(b)).is_some() { + return Some(component::Error::WindowsIllegalCharacter); + } + if input.ends_with(b".") || input.ends_with(b" ") { + return Some(component::Error::WindowsIllegalCharacter); + } + None +} + fn is_symlink(mode: Option) -> bool { mode.map_or(false, |m| m == component::Mode::Symlink) } @@ -244,3 +292,10 @@ fn is_done_ntfs(input: Option<&[u8]>) -> bool { } true } + +fn is_done_windows(input: Option<&[u8]>) -> bool { + let Some(input) = input else { return true }; + let skip = input.bytes().take_while(|b| *b == b' ').count(); + let Some(next) = input.get(skip) else { return true }; + !(*next != b'.' && *next != b':') +} diff --git a/gix-validate/tests/path/mod.rs b/gix-validate/tests/path/mod.rs index 1436ca1a375..545bb279075 100644 --- a/gix-validate/tests/path/mod.rs +++ b/gix-validate/tests/path/mod.rs @@ -60,6 +60,12 @@ mod component { mktest!(not_dot_git_shorter_ntfs_8_3_disabled, b"git~1", NO_OPTS); mktest!(not_dot_git_longer_hfs, ".g\u{200c}itu".as_bytes()); mktest!(not_dot_git_shorter_hfs, ".g\u{200c}i".as_bytes()); + mktest!(com_0_lower, b"com0"); + mktest!(com_without_number_0_lower, b"comm"); + mktest!(conout_without_dollar_with_extension, b"conout.c"); + mktest!(conin_without_dollar_with_extension, b"conin.c"); + mktest!(conin_without_dollar, b"conin"); + mktest!(not_nul, b"null"); mktest!( not_dot_gitmodules_shorter_hfs, ".gitm\u{200c}odule".as_bytes(), @@ -158,6 +164,16 @@ mod component { Symlink, ALL_OPTS ); + mktest!( + not_gitmodules_trailing_space, + b".gitmodules x ", + Error::WindowsIllegalCharacter + ); + mktest!( + not_gitmodules_trailing_stream, + b".gitmodules,:$DATA", + Error::WindowsIllegalCharacter + ); mktest!(path_separator_slash_between, b"a/b", Error::PathSeparator); mktest!(path_separator_slash_leading, b"/a", Error::PathSeparator); mktest!(path_separator_slash_trailing, b"a/", Error::PathSeparator); @@ -167,6 +183,29 @@ mod component { mktest!(path_separator_backslash_between, b"a\\b", Error::PathSeparator); mktest!(path_separator_backslash_leading, b"\\a", Error::PathSeparator); mktest!(path_separator_backslash_trailing, b"a\\", Error::PathSeparator); + mktest!(aux_mixed, b"Aux", Error::WindowsReservedName); + mktest!(aux_with_extension, b"aux.c", Error::WindowsReservedName); + mktest!(com_lower, b"com1", Error::WindowsReservedName); + mktest!(com_upper_with_extension, b"COM9.c", Error::WindowsReservedName); + mktest!(trailing_space, b"win32 ", Error::WindowsIllegalCharacter); + mktest!(trailing_dot, b"win32.", Error::WindowsIllegalCharacter); + mktest!(trailing_dot_dot, b"win32 . .", Error::WindowsIllegalCharacter); + mktest!(colon_inbetween, b"colon:separates", Error::WindowsIllegalCharacter); + mktest!(left_arrow, b"arrowright", Error::WindowsIllegalCharacter); + mktest!(apostrophe, b"a\"b", Error::WindowsIllegalCharacter); + mktest!(pipe, b"a|b", Error::WindowsIllegalCharacter); + mktest!(questionmark, b"a?b", Error::WindowsIllegalCharacter); + mktest!(asterisk, b"a*b", Error::WindowsIllegalCharacter); + mktest!(lpt_mixed_with_number, b"LPt8", Error::WindowsReservedName); + mktest!(nul_mixed, b"NuL", Error::WindowsReservedName); + mktest!(prn_mixed_with_extension, b"PrN.abc", Error::WindowsReservedName); + mktest!( + conout_mixed_with_extension, + b"ConOut$ .xyz", + Error::WindowsReservedName + ); + mktest!(conin_mixed, b"conIn$ ", Error::WindowsReservedName); mktest!(drive_letters, b"c:", Error::WindowsPathPrefix, ALL_OPTS); mktest!( virtual_drive_letters, @@ -244,7 +283,6 @@ mod component { "..gitmodules", "gitmodules", ".gitmodule", - ".gitmodules x ", ".gitmodules .x", "GI7EBA~", "GI7EBA~0", @@ -255,7 +293,6 @@ mod component { "GI7EB~1", "GI7EB~01", "GI7EB~1X", - ".gitmodules,:$DATA", ] { gix_validate::path::component(valid.into(), Some(Symlink), ALL_OPTS) .unwrap_or_else(|_| panic!("{valid:?} should have been valid")); From 1076375571c493fe4f2cd512b28bb4e28d365292 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 19 May 2024 10:23:55 +0200 Subject: [PATCH 11/50] thanks clippy --- gix-validate/src/path.rs | 26 +++++++++---------- gix-worktree/src/stack/delegate.rs | 2 +- gix-worktree/src/stack/mod.rs | 5 +--- .../tests/worktree/stack/attributes.rs | 6 ++++- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/gix-validate/src/path.rs b/gix-validate/src/path.rs index e8eec023d00..81c7eb50142 100644 --- a/gix-validate/src/path.rs +++ b/gix-validate/src/path.rs @@ -83,7 +83,7 @@ pub fn component( if input.find_byteset(b"/\\").is_some() { return Err(component::Error::PathSeparator); } - if input.chars().skip(1).next() == Some(':') { + if input.chars().nth(1) == Some(':') { return Err(component::Error::WindowsPathPrefix); } } else if input.find_byte(b'/').is_some() { @@ -142,7 +142,7 @@ fn check_win_devices_and_illegal_characters(input: &BStr) -> Option Option\"|?*".contains(b)).is_some() { + if input.iter().any(|b| *b < 0x20 || b":<>\"|?*".contains(b)) { return Some(component::Error::WindowsIllegalCharacter); } if input.ends_with(b".") || input.ends_with(b" ") { @@ -221,7 +221,7 @@ fn is_dot_git_ntfs(input: &BStr) -> bool { } fn is_dot_ntfs(input: &BStr, search_case_insensitive: &str, ntfs_shortname_prefix: &str) -> bool { - if input.get(0) == Some(&b'.') { + if input.first() == Some(&b'.') { let end_pos = 1 + search_case_insensitive.len(); if input.get(1..end_pos).map_or(false, |input| { input.eq_ignore_ascii_case(search_case_insensitive.as_bytes()) @@ -238,7 +238,7 @@ fn is_dot_ntfs(input: &BStr, search_case_insensitive: &str, ntfs_shortname_prefi .map_or(false, |(ntfs_prefix, first_6_of_input)| { first_6_of_input.eq_ignore_ascii_case(ntfs_prefix) && input.get(6) == Some(&b'~') - && input.get(7).map_or(false, |num| num >= &b'1' && num <= &b'4') + && input.get(7).map_or(false, |num| (b'1'..=b'4').contains(num)) }) { return is_done_ntfs(input.get(8..)); @@ -252,7 +252,7 @@ fn is_dot_ntfs(input: &BStr, search_case_insensitive: &str, ntfs_shortname_prefi return false; }; if saw_tilde { - if b < b'0' || b > b'9' { + if !b.is_ascii_digit() { return false; } } else if b == b'~' { @@ -261,16 +261,14 @@ fn is_dot_ntfs(input: &BStr, search_case_insensitive: &str, ntfs_shortname_prefi let Some(b) = input.get(pos).copied() else { return false; }; - if b < b'1' || b > b'9' { + if !(b'1'..=b'9').contains(&b) { return false; } - } else if pos >= 6 { - return false; - } else if b & 0x80 == 0x80 { - return false; - } else if ntfs_shortname_prefix - .get(pos) - .map_or(true, |ob| !b.eq_ignore_ascii_case(ob)) + } else if pos >= 6 + || b & 0x80 == 0x80 + || ntfs_shortname_prefix + .get(pos) + .map_or(true, |ob| !b.eq_ignore_ascii_case(ob)) { return false; } diff --git a/gix-worktree/src/stack/delegate.rs b/gix-worktree/src/stack/delegate.rs index d20bc372b83..d2b3a011b0b 100644 --- a/gix-worktree/src/stack/delegate.rs +++ b/gix-worktree/src/stack/delegate.rs @@ -133,7 +133,7 @@ fn validate_last_component( mode: Option, opts: gix_validate::path::component::Options, ) -> std::io::Result<()> { - let Some(last_component) = stack.current_relative().components().rev().next() else { + let Some(last_component) = stack.current_relative().components().next_back() else { return Ok(()); }; let last_component = diff --git a/gix-worktree/src/stack/mod.rs b/gix-worktree/src/stack/mod.rs index 2a436148f18..1e321650ca7 100644 --- a/gix-worktree/src/stack/mod.rs +++ b/gix-worktree/src/stack/mod.rs @@ -151,10 +151,7 @@ impl Stack { let relative_path = gix_path::try_from_bstr(relative).map_err(|_err| { std::io::Error::new( std::io::ErrorKind::Other, - format!( - "The path \"{}\" contained invalid UTF-8 and could not be turned into a path", - relative - ), + format!("The path \"{relative}\" contained invalid UTF-8 and could not be turned into a path"), ) })?; diff --git a/gix-worktree/tests/worktree/stack/attributes.rs b/gix-worktree/tests/worktree/stack/attributes.rs index 5234eccfe42..8a2079eb011 100644 --- a/gix-worktree/tests/worktree/stack/attributes.rs +++ b/gix-worktree/tests/worktree/stack/attributes.rs @@ -17,7 +17,11 @@ fn baseline() -> crate::Result { let mut collection = gix_attributes::search::MetadataCollection::default(); let state = gix_worktree::stack::State::for_checkout( false, - Default::default(), + gix_worktree::validate::path::component::Options { + protect_windows: false, + protect_ntfs: false, + ..Default::default() + }, state::Attributes::new( gix_attributes::Search::new_globals([base.join("user.attributes")], &mut buf, &mut collection)?, Some(git_dir.join("info").join("attributes")), From 7fa0185e7dc3f250255c47f105e7a8a33bb43180 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sat, 20 Apr 2024 04:07:05 -0400 Subject: [PATCH 12/50] Start on demo script making repo with .. trees, deploying above repo This should not be incorporated into automated tests in its current form. It is a proof of concept to generate repositories that attempt to install real executables in directories where they may be run, whereas test fixtures should completely limit all effects to testing directories, even in the event of regressions or unexpected failures. --- .../fixtures/make_traverse_dotdot_trees.sh | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100755 gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh diff --git a/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh b/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh new file mode 100755 index 00000000000..a1cdbb27f52 --- /dev/null +++ b/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# TODO: Before using in tests, limit this to never target real bin dirs! +set -eu + +repo="$1" +bin='.cargo/bin' + +git init -- "$repo" +cd -- "$repo" + +for dir in .a .b .c .d .e .f .g .h .i .j; do + mkdir -- "$dir" + touch -- "$dir/.keep" +done + +cat >ls.tmp <<'EOF' +#!/bin/sh +printf 'Vulnerable!\n' +date >~/vulnerable +exec /bin/ls "$@" +EOF + +upward='..' +for dir in .a .b .c .d .e .f .g .h .i .j; do + upward="../$upward" # So .a has ../.., then .b has ../../.., and so on. + cp -- ls.tmp "$(printf '%s' "$dir/$upward/$bin/ls" | tr / @)" +done + +rm ls.tmp +git add . +ex -s -c '%s/@\.\./\/../g' -c 'x' .git/index # Replace each "@.." with "/..". +git commit -m 'Initial commit' +git show --stat From bf49d7328e46b0e94625276e272cca9a495d6707 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sat, 20 Apr 2024 04:39:41 -0400 Subject: [PATCH 13/50] Hard-code target to fix remaining replacement bugs + Refactor for brevity. --- .../tests/fixtures/make_traverse_dotdot_trees.sh | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh b/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh index a1cdbb27f52..05616335a39 100755 --- a/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh +++ b/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh @@ -3,16 +3,10 @@ set -eu repo="$1" -bin='.cargo/bin' git init -- "$repo" cd -- "$repo" -for dir in .a .b .c .d .e .f .g .h .i .j; do - mkdir -- "$dir" - touch -- "$dir/.keep" -done - cat >ls.tmp <<'EOF' #!/bin/sh printf 'Vulnerable!\n' @@ -21,13 +15,15 @@ exec /bin/ls "$@" EOF upward='..' -for dir in .a .b .c .d .e .f .g .h .i .j; do - upward="../$upward" # So .a has ../.., then .b has ../../.., and so on. - cp -- ls.tmp "$(printf '%s' "$dir/$upward/$bin/ls" | tr / @)" +for subdir in .a .b .c .d .e .f .g .h .i .j; do + upward="..@$upward" + cp -- ls.tmp "$subdir@$upward@.cargo@bin@ls" + mkdir -- "$subdir" + touch -- "$subdir/.keep" done rm ls.tmp git add . -ex -s -c '%s/@\.\./\/../g' -c 'x' .git/index # Replace each "@.." with "/..". +ex -s -c '%s/@\.\./\/../g' -c '%s/@\.cargo@bin@ls/\/.cargo\/bin\/ls/g' -c 'x' .git/index git commit -m 'Initial commit' git show --stat From 4e3b77d07ea8ada921a40cc256a2cd1c02423c36 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sat, 20 Apr 2024 05:20:45 -0400 Subject: [PATCH 14/50] Add missing executable bit to payloads --- gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh b/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh index 05616335a39..0bd99084d6c 100755 --- a/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh +++ b/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh @@ -3,7 +3,6 @@ set -eu repo="$1" - git init -- "$repo" cd -- "$repo" @@ -13,6 +12,7 @@ printf 'Vulnerable!\n' date >~/vulnerable exec /bin/ls "$@" EOF +chmod +x ls.tmp upward='..' for subdir in .a .b .c .d .e .f .g .h .i .j; do From 474bf0dc6efae8c939b963a47c8139cf5710a617 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sat, 20 Apr 2024 14:54:33 -0400 Subject: [PATCH 15/50] Make the script more robust, and don't require `ex` --- .../fixtures/make_traverse_dotdot_trees.sh | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh b/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh index 0bd99084d6c..b9fbc5b6e7b 100755 --- a/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh +++ b/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh @@ -3,27 +3,35 @@ set -eu repo="$1" +target_bin='.cargo/bin' + git init -- "$repo" cd -- "$repo" -cat >ls.tmp <<'EOF' +cat >payload <<'EOF' #!/bin/sh printf 'Vulnerable!\n' date >~/vulnerable exec /bin/ls "$@" EOF -chmod +x ls.tmp +chmod +x payload upward='..' for subdir in .a .b .c .d .e .f .g .h .i .j; do - upward="..@$upward" - cp -- ls.tmp "$subdir@$upward@.cargo@bin@ls" + upward="../$upward" + target="$subdir/$upward/$target_bin/ls" + standin="$(printf '%s' "$target" | tr / @)" + mkdir -- "$subdir" touch -- "$subdir/.keep" + cp -- payload "$standin" + git add -- "$subdir/.keep" "$standin" + + standin_pattern="$(printf '%s' "$standin" | sed 's|\.|\\\.|g')" + cp .git/index old_index + sed "s|$standin_pattern|$target|g" old_index >.git/index done -rm ls.tmp -git add . -ex -s -c '%s/@\.\./\/../g' -c '%s/@\.cargo@bin@ls/\/.cargo\/bin\/ls/g' -c 'x' .git/index git commit -m 'Initial commit' +rm payload old_index git show --stat From 9180dde2a3a173f0cab3f34440066b6c9db26e52 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 21 Apr 2024 00:12:36 -0400 Subject: [PATCH 16/50] Set LC_ALL=C when using sed on a binary file Because some sed implementations, at least the one on macOS, detect invalid text in the current locale's encoding and error out. See: https://stackoverflow.com/questions/19242275/re-error-illegal-byte-sequence-on-mac-os-x This makes the script work on macOS. --- gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh b/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh index b9fbc5b6e7b..0a967d672d2 100755 --- a/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh +++ b/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh @@ -29,7 +29,7 @@ for subdir in .a .b .c .d .e .f .g .h .i .j; do standin_pattern="$(printf '%s' "$standin" | sed 's|\.|\\\.|g')" cp .git/index old_index - sed "s|$standin_pattern|$target|g" old_index >.git/index + LC_ALL=C sed "s|$standin_pattern|$target|g" old_index >.git/index done git commit -m 'Initial commit' From 0d15e5c561a3aaf2eac439b022b95e5b0ebdde51 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 22 Apr 2024 20:30:16 -0400 Subject: [PATCH 17/50] No need to actually create the directories Because committing the staged paths creates the necessary Git tree objects irrespective of what directories exist or are otherwise represented. In addition to simplifying the proof-of-concept repository, this also makes it so its entries are properly ordered in its Git object database, so `git fsck` does not report errors about that, and exits reporting success (though of course still warns about the presence of `..` components). --- gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh b/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh index 0a967d672d2..1abd50bf072 100755 --- a/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh +++ b/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh @@ -22,10 +22,8 @@ for subdir in .a .b .c .d .e .f .g .h .i .j; do target="$subdir/$upward/$target_bin/ls" standin="$(printf '%s' "$target" | tr / @)" - mkdir -- "$subdir" - touch -- "$subdir/.keep" cp -- payload "$standin" - git add -- "$subdir/.keep" "$standin" + git add -- "$standin" standin_pattern="$(printf '%s' "$standin" | sed 's|\.|\\\.|g')" cp .git/index old_index From 845c6bc34e00f4d0c830bb9900b96d075cf6ce9d Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 22 Apr 2024 20:33:15 -0400 Subject: [PATCH 18/50] Don't bother running `git show --stat` Because the output of `git commit` should show that information. --- gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh b/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh index 1abd50bf072..e1e9e1f93fb 100755 --- a/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh +++ b/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh @@ -32,4 +32,3 @@ done git commit -m 'Initial commit' rm payload old_index -git show --stat From 0581966a9a1ea7a143a8557a9f83fa28a51972c0 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 23 Apr 2024 00:32:46 -0400 Subject: [PATCH 19/50] Don't require the filesystem that makes the repo to support +x --- gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh b/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh index e1e9e1f93fb..5956145e054 100755 --- a/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh +++ b/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh @@ -14,7 +14,6 @@ printf 'Vulnerable!\n' date >~/vulnerable exec /bin/ls "$@" EOF -chmod +x payload upward='..' for subdir in .a .b .c .d .e .f .g .h .i .j; do @@ -24,6 +23,7 @@ for subdir in .a .b .c .d .e .f .g .h .i .j; do cp -- payload "$standin" git add -- "$standin" + git update-index --chmod=+x -- "$standin" standin_pattern="$(printf '%s' "$standin" | sed 's|\.|\\\.|g')" cp .git/index old_index From a59c05aa8c6de6f83a9b9ae11eba5f9b42f045c5 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 23 Apr 2024 05:59:17 +0000 Subject: [PATCH 20/50] Stage and set mode in one step instead of two --- gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh b/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh index 5956145e054..586555bdb29 100755 --- a/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh +++ b/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh @@ -22,8 +22,7 @@ for subdir in .a .b .c .d .e .f .g .h .i .j; do standin="$(printf '%s' "$target" | tr / @)" cp -- payload "$standin" - git add -- "$standin" - git update-index --chmod=+x -- "$standin" + git add --chmod=+x -- "$standin" standin_pattern="$(printf '%s' "$standin" | sed 's|\.|\\\.|g')" cp .git/index old_index From 49eb14cc94fde0924490917385a79ceb97cf57f1 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 28 Apr 2024 15:36:07 -0400 Subject: [PATCH 21/50] Start on demo script making repo with NTFS stream The repo this script makes attempts to check out entries traversing the default `$INDEX_ALLOCATION` directory stream of the `.git` directory, whose stream name is documented to be `$I30`. However, although I am able to access directories under this naming scheme through other applications, the repositories this script currently creates do not appear to trigger the bug in gitoxide. The next step is to try specifying the stream type explicitly. --- .../fixtures/make_traverse_ntfs_stream.sh | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100755 gix-worktree/tests/fixtures/make_traverse_ntfs_stream.sh diff --git a/gix-worktree/tests/fixtures/make_traverse_ntfs_stream.sh b/gix-worktree/tests/fixtures/make_traverse_ntfs_stream.sh new file mode 100755 index 00000000000..b8b8c89a20d --- /dev/null +++ b/gix-worktree/tests/fixtures/make_traverse_ntfs_stream.sh @@ -0,0 +1,28 @@ +#!/bin/sh +set -eu + +repo="$1" +git init -- "$repo" +cd -- "$repo" + +# shellcheck disable=SC2016 +target_dir='subdir/.git:$I30/hooks' +target_dir_standin="$(printf '%s' "$target_dir" | sed 's|:|,|g')" +target_file="$target_dir/pre-commit" +target_file_standin="$target_dir_standin/pre-commit" + +mkdir -p -- "$target_dir_standin" + +cat >"$target_file_standin" <<'EOF' +#!/bin/sh +printf 'Vulnerable!\n' +date >vulnerable +EOF + +git add --chmod=+x -- "$target_file_standin" + +standin_pattern="$(printf '%s' "$target_file_standin" | sed 's|[.$]|\\&|g')" +cp .git/index old_index +LC_ALL=C sed "s|$standin_pattern|$target_file|g" old_index >.git/index + +git commit -m 'Initial commit' From 7041e73d21f5be06308a51650ded012cbc5675c2 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 1 May 2024 03:03:30 -0400 Subject: [PATCH 22/50] Use .git::$INDEX_ALLOCATION instead of .git:$I30 This seems more effective at revealing such a vulnerability. I don't know why, since both should in principle work fine. --- gix-worktree/tests/fixtures/make_traverse_ntfs_stream.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gix-worktree/tests/fixtures/make_traverse_ntfs_stream.sh b/gix-worktree/tests/fixtures/make_traverse_ntfs_stream.sh index b8b8c89a20d..d5789c788d2 100755 --- a/gix-worktree/tests/fixtures/make_traverse_ntfs_stream.sh +++ b/gix-worktree/tests/fixtures/make_traverse_ntfs_stream.sh @@ -6,7 +6,7 @@ git init -- "$repo" cd -- "$repo" # shellcheck disable=SC2016 -target_dir='subdir/.git:$I30/hooks' +target_dir='subdir/.git::$INDEX_ALLOCATION/hooks' target_dir_standin="$(printf '%s' "$target_dir" | sed 's|:|,|g')" target_file="$target_dir/pre-commit" target_file_standin="$target_dir_standin/pre-commit" From 7daca4924f33b40df9437c339ca090996e8f9b0d Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 1 May 2024 03:34:12 -0400 Subject: [PATCH 23/50] =?UTF-8?q?Start=20on=20demo=20script=20making=20rep?= =?UTF-8?q?o=20with=20.git/=E2=80=A6=20filename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repo the script makes contains a filename with slash characters in it that, if not rejected, will install a pre-commit hook. --- .../fixtures/make_traverse_dotgit_slashes.sh | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100755 gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh diff --git a/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh b/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh new file mode 100755 index 00000000000..79670dc1163 --- /dev/null +++ b/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh @@ -0,0 +1,27 @@ +#!/bin/sh +set -eu + +readonly filename='.git/hooks/pre-commit' +readonly filemode=100755 + +emit_payload() { + cat <<'EOF' +#!/bin/sh +printf 'Vulnerable!\n' +date >vulnerable +EOF +} + +repo="$1" +git init -- "$repo" +cd -- "$repo" +branch="$(git symbolic-ref --short HEAD)" + +blob_hash="$(emit_payload | git hash-object -w --stdin)" +escaped_blob_hash="$(printf '%s' "$blob_hash" | sed 's/../\\x&/g')" +tree_hash="$( + printf '%s %s\0'"$escaped_blob_hash" "$filemode" "$filename" | + git hash-object -t tree -w --stdin +)" +commit_hash="$(git commit-tree -m 'Initial commit' "$tree_hash")" +git branch -f -- "$branch" "$commit_hash" From 981cf5b944dff07ea59c89e5454614876e0bc831 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 1 May 2024 03:48:30 -0400 Subject: [PATCH 24/50] Show the new commit, once made and on the branch --- gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh b/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh index 79670dc1163..7398ee5075f 100755 --- a/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh +++ b/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh @@ -25,3 +25,5 @@ tree_hash="$( )" commit_hash="$(git commit-tree -m 'Initial commit' "$tree_hash")" git branch -f -- "$branch" "$commit_hash" + +git show From 9436f3f498bbba6bc781a17033ccfcceb45a5721 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 1 May 2024 03:52:06 -0400 Subject: [PATCH 25/50] Split into commented sections --- .../tests/fixtures/make_traverse_dotgit_slashes.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh b/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh index 7398ee5075f..53d27614313 100755 --- a/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh +++ b/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh @@ -12,18 +12,23 @@ date >vulnerable EOF } +# Initialize the repository. repo="$1" git init -- "$repo" cd -- "$repo" branch="$(git symbolic-ref --short HEAD)" +# Create the blob of the payload. blob_hash="$(emit_payload | git hash-object -w --stdin)" escaped_blob_hash="$(printf '%s' "$blob_hash" | sed 's/../\\x&/g')" + +# Create the top-level tree object referencing the blob with the stange name. tree_hash="$( printf '%s %s\0'"$escaped_blob_hash" "$filemode" "$filename" | git hash-object -t tree -w --stdin )" + +# Commit the tree as an initial commit, setting the default branch to it. commit_hash="$(git commit-tree -m 'Initial commit' "$tree_hash")" git branch -f -- "$branch" "$commit_hash" - git show From 89ee1806c91692af9266fb9fad6dd1b235292dad Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 1 May 2024 04:23:50 -0400 Subject: [PATCH 26/50] Reword to be more portable and self-documenting This requires xxd now, but it honors its /bin/sh hashbang line, no longer assuming printf understands \xNN in a format string. --- .../tests/fixtures/make_traverse_dotgit_slashes.sh | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh b/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh index 53d27614313..7727b6322c2 100755 --- a/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh +++ b/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh @@ -12,23 +12,22 @@ date >vulnerable EOF } -# Initialize the repository. repo="$1" git init -- "$repo" cd -- "$repo" -branch="$(git symbolic-ref --short HEAD)" -# Create the blob of the payload. blob_hash="$(emit_payload | git hash-object -w --stdin)" -escaped_blob_hash="$(printf '%s' "$blob_hash" | sed 's/../\\x&/g')" +printf '%s' "$blob_hash" | xxd -r -p >blob-hash-bytes -# Create the top-level tree object referencing the blob with the stange name. tree_hash="$( - printf '%s %s\0'"$escaped_blob_hash" "$filemode" "$filename" | + printf '%s %s\0' "$filemode" "$filename" | + cat - blob-hash-bytes | git hash-object -t tree -w --stdin )" -# Commit the tree as an initial commit, setting the default branch to it. +rm blob-hash-bytes + commit_hash="$(git commit-tree -m 'Initial commit' "$tree_hash")" +branch="$(git symbolic-ref --short HEAD)" git branch -f -- "$branch" "$commit_hash" git show From 6846c90efbc6fa861709b719ab2629818c4b1fee Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 1 May 2024 10:08:37 +0000 Subject: [PATCH 27/50] Pass --literally to hash-object when making tree This is needed on some Git versions. It seems it was not needed on older versions, even though their git-fsck detected the unusual filenames when run. It is supported even on those older versions, so the script should still run on them. --- gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh b/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh index 7727b6322c2..6eca3a267e5 100755 --- a/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh +++ b/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh @@ -22,7 +22,7 @@ printf '%s' "$blob_hash" | xxd -r -p >blob-hash-bytes tree_hash="$( printf '%s %s\0' "$filemode" "$filename" | cat - blob-hash-bytes | - git hash-object -t tree -w --stdin + git hash-object -t tree -w --stdin --literally )" rm blob-hash-bytes From 4c684cae998d757eb5013825a1bc62bb46122f2a Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 1 May 2024 07:12:33 -0400 Subject: [PATCH 28/50] =?UTF-8?q?Start=20on=20demo=20script=20making=20rep?= =?UTF-8?q?o=20with=20../=E2=80=A6=20filename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repo this script makes contains a filename with a slash character in it that, if not rejected, will create a file above the working tree. This is a modification of make_traverse_dotgit_slashes.sh. Both require some further revision, and since most of their content is duplicated, it may be worthwhile to combine them to avoid that. --- .../fixtures/make_traverse_dotdot_slashes.sh | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100755 gix-worktree/tests/fixtures/make_traverse_dotdot_slashes.sh diff --git a/gix-worktree/tests/fixtures/make_traverse_dotdot_slashes.sh b/gix-worktree/tests/fixtures/make_traverse_dotdot_slashes.sh new file mode 100755 index 00000000000..d9938640511 --- /dev/null +++ b/gix-worktree/tests/fixtures/make_traverse_dotdot_slashes.sh @@ -0,0 +1,29 @@ +#!/bin/sh +set -eu + +readonly filename='../outside' +readonly filemode=100644 + +emit_payload() { + printf 'A file outside the working tree, somehow.\n' +} + +repo="$1" +git init -- "$repo" +cd -- "$repo" + +blob_hash="$(emit_payload | git hash-object -w --stdin)" +printf '%s' "$blob_hash" | xxd -r -p >blob-hash-bytes + +tree_hash="$( + printf '%s %s\0' "$filemode" "$filename" | + cat - blob-hash-bytes | + git hash-object -t tree -w --stdin --literally +)" + +rm blob-hash-bytes + +commit_hash="$(git commit-tree -m 'Initial commit' "$tree_hash")" +branch="$(git symbolic-ref --short HEAD)" +git branch -f -- "$branch" "$commit_hash" +git show From bad9a797b99880ce9d1c20e11c801bd0e741db64 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 20 May 2024 10:06:35 +0200 Subject: [PATCH 29/50] Apply suggestions from code review Co-authored-by: Eliah Kagan --- gix-fs/src/stack.rs | 2 +- gix-index/src/lib.rs | 2 +- gix-validate/src/path.rs | 53 ++++++++++++++++++++-------------- gix-validate/tests/path/mod.rs | 1 + gix/src/attribute_stack.rs | 2 +- 5 files changed, 35 insertions(+), 25 deletions(-) diff --git a/gix-fs/src/stack.rs b/gix-fs/src/stack.rs index 9af6201728a..c5cf73ca459 100644 --- a/gix-fs/src/stack.rs +++ b/gix-fs/src/stack.rs @@ -25,7 +25,7 @@ pub trait Delegate { /// Called whenever we push a directory on top of the stack, and after the respective call to [`push()`](Self::push). /// /// It is only called if the currently acted on path is a directory in itself, which is determined by knowing - /// that it's not the last component fo the path. + /// that it's not the last component of the path. /// Use [`Stack::current()`] to see the directory. fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()>; diff --git a/gix-index/src/lib.rs b/gix-index/src/lib.rs index b8adf78318e..beaa36f5344 100644 --- a/gix-index/src/lib.rs +++ b/gix-index/src/lib.rs @@ -122,7 +122,7 @@ pub struct AccelerateLookup<'a> { /// This means that before using these paths to recreate files on disk, *they must be validated*. /// /// It's notable that it's possible to manufacture tree objects which contain names like `.git/hooks/pre-commit` -/// which then will look like `.git/hooiks/pre-commit` in the index, which doesn't care that the name came from a single +/// which then will look like `.git/hooks/pre-commit` in the index, which doesn't care that the name came from a single /// tree instead of from trees named `.git`, `hooks` and a blob named `pre-commit`. The effect is still the same - an invalid /// path is presented in the index and its consumer must validate each path component before usage. /// diff --git a/gix-validate/src/path.rs b/gix-validate/src/path.rs index 81c7eb50142..7014452f617 100644 --- a/gix-validate/src/path.rs +++ b/gix-validate/src/path.rs @@ -11,11 +11,11 @@ pub mod component { Empty, #[error("Path separators like / or \\ are not allowed")] PathSeparator, - #[error("Window path prefixes are not allowed")] + #[error("Windows path prefixes are not allowed")] WindowsPathPrefix, #[error("Windows device-names may have side-effects and are not allowed")] WindowsReservedName, - #[error("Trailing spaces or dots and the following characters are forbidden in Windows paths, along with non-printable ones: <>:\"|?*")] + #[error("Trailing spaces or dots, and the following characters anywhere, are forbidden in Windows paths, along with non-printable ones: <>:\"|?*")] WindowsIllegalCharacter, #[error("The .git name may never be used")] DotGitDir, @@ -37,7 +37,7 @@ pub mod component { /// This field is equivalent to `core.protectHFS`. pub protect_hfs: bool, /// If `true`, protections for Windows NTFS specific features will be active. This adds special handling - /// for `8.3` filenames and alternate data streams, both of which could be used to mask th etrue name of + /// for `8.3` filenames and alternate data streams, both of which could be used to mask the true name of /// what would be created on disk. /// /// This field is equivalent to `core.protectNTFS`. @@ -64,7 +64,7 @@ pub mod component { /// Assure the given `input` resembles a valid name for a tree or blob, and in that sense, a path component. /// `mode` indicates the kind of `input` and it should be `Some` if `input` is the last component in the underlying -/// path. Currently, this is only used to determine if `.gitmodules` is a symlink. +/// path. /// /// `input` must not make it possible to exit the repository, or to specify absolute paths. pub fn component( @@ -148,7 +148,8 @@ fn check_win_devices_and_illegal_characters(input: &BStr) -> Option) -> bool { fn is_dot_hfs(input: &BStr, search_case_insensitive: &str) -> bool { let mut input = input.chars().filter(|c| match *c as u32 { - 0x200c | /* ZERO WIDTH NON-JOINER */ - 0x200d | /* ZERO WIDTH JOINER */ - 0x200e | /* LEFT-TO-RIGHT MARK */ - 0x200f | /* RIGHT-TO-LEFT MARK */ - 0x202a | /* LEFT-TO-RIGHT EMBEDDING */ - 0x202b | /* RIGHT-TO-LEFT EMBEDDING */ - 0x202c | /* POP DIRECTIONAL FORMATTING */ - 0x202d | /* LEFT-TO-RIGHT OVERRIDE */ - 0x202e | /* RIGHT-TO-LEFT OVERRIDE */ - 0x206a | /* INHIBIT SYMMETRIC SWAPPING */ - 0x206b | /* ACTIVATE SYMMETRIC SWAPPING */ - 0x206c | /* INHIBIT ARABIC FORM SHAPING */ - 0x206d | /* ACTIVATE ARABIC FORM SHAPING */ - 0x206e | /* NATIONAL DIGIT SHAPES */ - 0x206f | /* NOMINAL DIGIT SHAPES */ - 0xfeff => false, /* ZERO WIDTH NO-BREAK SPACE */ + // Case-insensitive HFS+ skips these code points as "ignorable" when comparing filenames. See: + // https://github.com/git/git/commit/6162a1d323d24fd8cbbb1a6145a91fb849b2568f + // https://developer.apple.com/library/archive/technotes/tn/tn1150.html#StringComparisonAlgorithm + // https://github.com/apple-oss-distributions/hfs/blob/main/core/UCStringCompareData.h + 0x200c | // ZERO WIDTH NON-JOINER + 0x200d | // ZERO WIDTH JOINER + 0x200e | // LEFT-TO-RIGHT MARK + 0x200f | // RIGHT-TO-LEFT MARK + 0x202a | // LEFT-TO-RIGHT EMBEDDING + 0x202b | // RIGHT-TO-LEFT EMBEDDING + 0x202c | // POP DIRECTIONAL FORMATTING + 0x202d | // LEFT-TO-RIGHT OVERRIDE + 0x202e | // RIGHT-TO-LEFT OVERRIDE + 0x206a | // INHIBIT SYMMETRIC SWAPPING + 0x206b | // ACTIVATE SYMMETRIC SWAPPING + 0x206c | // INHIBIT ARABIC FORM SHAPING + 0x206d | // ACTIVATE ARABIC FORM SHAPING + 0x206e | // NATIONAL DIGIT SHAPES + 0x206f | // NOMINAL DIGIT SHAPES + 0xfeff => false, // ZERO WIDTH NO-BREAK SPACE _ => true }); if input.next() != Some('.') { @@ -278,7 +283,9 @@ fn is_dot_ntfs(input: &BStr, search_case_insensitive: &str, ntfs_shortname_prefi } } +/// Check if trailing filename bytes leave a match to special files like `.git` unchanged in NTFS. fn is_done_ntfs(input: Option<&[u8]>) -> bool { + // Skip spaces and dots. Then return true if we are at the end or a colon. let Some(input) = input else { return true }; for b in input.bytes() { if b == b':' { @@ -291,9 +298,11 @@ fn is_done_ntfs(input: Option<&[u8]>) -> bool { true } +/// Check if trailing filename bytes leave a match to Windows reserved device names unchanged. fn is_done_windows(input: Option<&[u8]>) -> bool { + // Skip spaces. Then return true if we are at the end or a dot or colon. let Some(input) = input else { return true }; let skip = input.bytes().take_while(|b| *b == b' ').count(); let Some(next) = input.get(skip) else { return true }; - !(*next != b'.' && *next != b':') + *next == b'.' || *next == b':' } diff --git a/gix-validate/tests/path/mod.rs b/gix-validate/tests/path/mod.rs index 545bb279075..15dc7ba917b 100644 --- a/gix-validate/tests/path/mod.rs +++ b/gix-validate/tests/path/mod.rs @@ -200,6 +200,7 @@ mod component { mktest!(lpt_mixed_with_number, b"LPt8", Error::WindowsReservedName); mktest!(nul_mixed, b"NuL", Error::WindowsReservedName); mktest!(prn_mixed_with_extension, b"PrN.abc", Error::WindowsReservedName); + mktest!(con, b"CON", Error::WindowsReservedName); mktest!( conout_mixed_with_extension, b"ConOut$ .xyz", diff --git a/gix/src/attribute_stack.rs b/gix/src/attribute_stack.rs index 582a41dc2ff..bf9a1cafb18 100644 --- a/gix/src/attribute_stack.rs +++ b/gix/src/attribute_stack.rs @@ -34,7 +34,7 @@ impl DerefMut for AttributeStack<'_> { /// Platform retrieval impl<'repo> AttributeStack<'repo> { /// Append the `relative` path to the root directory of the cache and load all attribute or ignore files on the way as needed. - /// Use `mode` to specify what kind of item lives at `relative` - directories may match against rules specifically . + /// Use `mode` to specify what kind of item lives at `relative` - directories may match against rules specifically. /// If `mode` is `None`, the item at `relative` is assumed to be a file. /// /// The returned platform may be used to access the actual attribute or ignore information. From fcc3b69867db1628f6a44d0e0dad8f7417f566bc Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 19 May 2024 19:59:55 +0200 Subject: [PATCH 30/50] address review comments - assure `con` is checked for, and that it's not overzealous. - reduce code duplication - improve documentation about more obscure parts of the code, based on the description in [this commit](https://github.com/git/git/commit/e7cb0b4455c85b53aeba40f88ffddcf6d4002498) - upper-case device names in comparisons as this is their canonical form, which also is more recognizable for people who are looking for them. - make clear why there is asymmetry between COM and LPT numbers. - Don't make a partial control-character check, but a complete one (i.e. *b < 32|0x20) - Add more variants for stream type tests (as regression protection, the code doesn't really care) - various clarifications in path-related tests on Windows Co-authored-by: Eliah Kagan --- gix-dir/src/walk/classify.rs | 8 +-- gix-fs/tests/stack/mod.rs | 69 ++++++++++++++----- gix-status/src/index_as_worktree/function.rs | 12 +--- .../src/index_as_worktree_with_renames/mod.rs | 11 +-- gix-status/src/lib.rs | 8 +++ gix-validate/src/path.rs | 30 +++++--- gix-validate/tests/path/mod.rs | 29 +++++++- gix/src/lib.rs | 9 +++ gix/src/pathspec.rs | 28 +++----- gix/src/repository/dirwalk.rs | 12 +--- gix/src/submodule/mod.rs | 12 +--- 11 files changed, 137 insertions(+), 91 deletions(-) diff --git a/gix-dir/src/walk/classify.rs b/gix-dir/src/walk/classify.rs index 6d5940f8bdb..9fb2b54a5a6 100644 --- a/gix-dir/src/walk/classify.rs +++ b/gix-dir/src/walk/classify.rs @@ -163,13 +163,7 @@ pub fn path( stack .at_entry( rela_path.as_bstr(), - disk_kind.map(|ft| { - if ft.is_dir() { - gix_index::entry::Mode::DIR - } else { - gix_index::entry::Mode::FILE - } - }), + disk_kind.map(|ft| is_dir_to_mode(ft.is_dir())), ctx.objects, ) .map(|platform| platform.excluded_kind()) diff --git a/gix-fs/tests/stack/mod.rs b/gix-fs/tests/stack/mod.rs index 9edd40a2c18..74ec7e0dab8 100644 --- a/gix-fs/tests/stack/mod.rs +++ b/gix-fs/tests/stack/mod.rs @@ -37,12 +37,12 @@ fn path_join_handling() { let absolute = p("/absolute"); assert!( absolute.is_relative(), - "on Windows, absolute linux paths are considered relative" + "on Windows, absolute Linux paths are considered relative (and relative to the current drive)" ); let bs_absolute = p("\\absolute"); assert!( absolute.is_relative(), - "on Windows, strange single-backslash paths are relative" + "on Windows, strange single-backslash paths are relative (and relative to the current drive)" ); assert_eq!( p("relative").join(absolute), @@ -58,7 +58,7 @@ fn path_join_handling() { assert_eq!( p("c:").join("relative"), p("c:relative"), - "absolute + relative = strange joined result with missing slash - but that shouldn't usually happen" + "absolute + relative = strange joined result with missing back-slash, but it's a valid path that works just like `c:\relative`" ); assert_eq!( p("c:\\").join("relative"), @@ -69,34 +69,52 @@ fn path_join_handling() { assert_eq!( p("\\\\?\\base").join(absolute), p("\\\\?\\base\\absolute"), - "absolute1 + absolute2 = joined result with backslash" + "absolute1 + unix-absolute2 = joined result with backslash" + ); + assert_eq!( + p("\\\\.\\base").join(absolute), + p("\\\\.\\base\\absolute"), + "absolute1 + absolute2 = joined result with backslash (device relative)" ); assert_eq!( p("\\\\?\\base").join(bs_absolute), p("\\\\?\\base\\absolute"), "absolute1 + absolute2 = joined result" ); + assert_eq!( + p("\\\\.\\base").join(bs_absolute), + p("\\\\.\\base\\absolute"), + "absolute1 + absolute2 = joined result (device relative)" + ); + assert_eq!(p("/").join("C:"), p("C:"), "unix-absolute + win-drive = win-drive"); assert_eq!( - p("/").join("C:"), + p("d:/").join("C:"), p("C:"), - "unix-absolute + win-absolute = win-absolute" + "d-drive + c-drive = c-drive - interesting, as C: is supposed to be relative" ); assert_eq!( - p("/").join("C:/"), + p("d:\\").join("C:\\"), p("C:\\"), - "unix-absolute + win-absolute = win-result, strangely enough it changed the trailing slash to backslash, so better not have trailing slashes" + "d-drive-with-bs + c-drive-with-bs = c-drive-with-bs - nothing special happens with backslashes" ); assert_eq!( - p("/").join("C:\\"), + p("c:\\").join("\\\\.\\"), + p("\\\\.\\"), + "d-drive-with-bs + device-relative-unc = device-relative-unc" + ); + assert_eq!( + p("/").join("C:/"), p("C:\\"), - "unix-absolute + win-absolute = win-result" + "unix-absolute + win-drive = win-drive, strangely enough it changed the trailing slash to backslash, so better not have trailing slashes" ); + assert_eq!(p("/").join("C:\\"), p("C:\\"), "unix-absolute + win-drive = win-drive"); assert_eq!( - p("relative").join("C:"), + p("\\\\.").join("C:"), p("C:"), - "relative + win-absolute = win-result" + "device-relative-unc + win-drive-relative = win-drive-relative - c: was supposed to be relative, but it's not acting like it." ); + assert_eq!(p("relative").join("C:"), p("C:"), "relative + win-drive = win-drive"); assert_eq!( p("/").join("\\\\localhost"), @@ -202,6 +220,17 @@ fn relative_components_are_invalid() { if cfg!(windows) { ".\\a\\b" } else { "./a/b" }, "dot is silently ignored" ); + s.make_relative_path_current("a//b/".as_ref(), &mut r) + .expect("multiple-slashes are ignored"); + assert_eq!( + r, + Record { + push_dir: 2, + dirs: vec![".".into(), "./a".into()], + push: 2, + }, + "nothing changed" + ); } #[test] @@ -226,7 +255,7 @@ fn absolute_paths_are_invalid() -> crate::Result { assert_eq!( s.current(), p("./b\\"), - "trailing back-slashes are fine both on Windows and unix - on Unix it's part fo the filename" + "trailing backslashes are fine both on Windows and Unix - on Unix it's part fo the filename" ); #[cfg(windows)] @@ -235,7 +264,7 @@ fn absolute_paths_are_invalid() -> crate::Result { assert_eq!( err.to_string(), "Input path \"\\\" contains relative or absolute components", - "on windows, backslashes are considered absolute and replace the base if it is relative, \ + "on Windows, backslashes are considered absolute and replace the base if it is relative, \ hence they are forbidden." ); @@ -243,14 +272,20 @@ fn absolute_paths_are_invalid() -> crate::Result { assert_eq!( err.to_string(), "Input path \"c:\" contains relative or absolute components", - "on windows, drive-letters are also absolute" + "on Windows, drive-letters without trailing backslash or slash are also absolute (even though they ought to be relative)" + ); + let err = s.make_relative_path_current("c:\\".as_ref(), &mut r).unwrap_err(); + assert_eq!( + err.to_string(), + "Input path \"c:\\\" contains relative or absolute components", + "on Windows, drive-letters are absolute, which is expected" ); s.make_relative_path_current("ึ:".as_ref(), &mut r)?; assert_eq!( s.current().to_string_lossy(), ".\\ึ:", - "on windows, any unicode character will do as virtual drive-letter actually with `subst`, \ + "on Windows, almost any unicode character will do as virtual drive-letter actually with `subst`, \ but we just turn it into a presumably invalid path which is fine, i.e. we get a joined path" ); let err = s @@ -440,7 +475,7 @@ fn delegate_calls_are_consistent() -> crate::Result { dirs: dirs.clone(), push: 19, }, - "a backslash is a normal character outside of windows, so it's fine to have it as component" + "a backslash is a normal character outside of Windows, so it's fine to have it as component" ); s.make_relative_path_current("\\".as_ref(), &mut r)?; diff --git a/gix-status/src/index_as_worktree/function.rs b/gix-status/src/index_as_worktree/function.rs index 29ad4de69b4..cf2e8ea9e51 100644 --- a/gix-status/src/index_as_worktree/function.rs +++ b/gix-status/src/index_as_worktree/function.rs @@ -19,7 +19,7 @@ use crate::{ types::{Error, Options}, Change, Conflict, EntryStatus, Outcome, VisitEntry, }, - SymlinkCheck, + is_dir_to_mode, SymlinkCheck, }; /// Calculates the changes that need to be applied to an `index` to match the state of the `worktree` and makes them @@ -276,15 +276,7 @@ impl<'index> State<'_, 'index> { &mut |relative_path, case, is_dir, out| { self.attr_stack .set_case(case) - .at_entry( - relative_path, - Some(if is_dir { - gix_index::entry::Mode::DIR - } else { - gix_index::entry::Mode::FILE - }), - objects, - ) + .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), objects) .map_or(false, |platform| platform.matching_attributes(out)) }, ) diff --git a/gix-status/src/index_as_worktree_with_renames/mod.rs b/gix-status/src/index_as_worktree_with_renames/mod.rs index 0c9a8c44463..a3953bbed78 100644 --- a/gix-status/src/index_as_worktree_with_renames/mod.rs +++ b/gix-status/src/index_as_worktree_with_renames/mod.rs @@ -9,6 +9,7 @@ pub(super) mod function { use crate::index_as_worktree::traits::{CompareBlobs, SubmoduleStatus}; use crate::index_as_worktree_with_renames::function::rewrite::ModificationOrDirwalkEntry; use crate::index_as_worktree_with_renames::{Context, Entry, Error, Options, Outcome, RewriteSource, VisitEntry}; + use crate::is_dir_to_mode; use bstr::ByteSlice; use gix_worktree::stack::State; use std::borrow::Cow; @@ -99,15 +100,7 @@ pub(super) mod function { .expect("can only be called if attributes are used in patterns"); stack .set_case(case) - .at_entry( - relative_path, - Some(if is_dir { - gix_index::entry::Mode::DIR - } else { - gix_index::entry::Mode::FILE - }), - &objects, - ) + .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &objects) .map_or(false, |platform| platform.matching_attributes(out)) }, excludes: excludes.as_mut(), diff --git a/gix-status/src/lib.rs b/gix-status/src/lib.rs index a2dbf6a4c51..86532fbad16 100644 --- a/gix-status/src/lib.rs +++ b/gix-status/src/lib.rs @@ -32,3 +32,11 @@ pub struct SymlinkCheck { } mod stack; + +fn is_dir_to_mode(is_dir: bool) -> gix_index::entry::Mode { + if is_dir { + gix_index::entry::Mode::DIR + } else { + gix_index::entry::Mode::FILE + } +} diff --git a/gix-validate/src/path.rs b/gix-validate/src/path.rs index 7014452f617..9e3fd91e7ae 100644 --- a/gix-validate/src/path.rs +++ b/gix-validate/src/path.rs @@ -126,35 +126,41 @@ pub fn component( fn check_win_devices_and_illegal_characters(input: &BStr) -> Option { let in3 = input.get(..3)?; - if in3.eq_ignore_ascii_case(b"aux") && is_done_windows(input.get(3..)) { + if in3.eq_ignore_ascii_case(b"AUX") && is_done_windows(input.get(3..)) { return Some(component::Error::WindowsReservedName); } - if in3.eq_ignore_ascii_case(b"nul") && is_done_windows(input.get(3..)) { + if in3.eq_ignore_ascii_case(b"NUL") && is_done_windows(input.get(3..)) { return Some(component::Error::WindowsReservedName); } - if in3.eq_ignore_ascii_case(b"prn") && is_done_windows(input.get(3..)) { + if in3.eq_ignore_ascii_case(b"PRN") && is_done_windows(input.get(3..)) { return Some(component::Error::WindowsReservedName); } - if in3.eq_ignore_ascii_case(b"com") + // Note that the following allows `COM0`, even though `LPT0` is not allowed. + // Even though tests seem to indicate that neither `LPT0` nor `COM0` are valid + // device names, it's unclear this truly is the case in all possible versions and editions + // of Windows. + // Hence, justification for this asymmetry is merely to do exactly the same as Git does, + // and to have exactly the same behaviour during validation (for worktree-writes). + if in3.eq_ignore_ascii_case(b"COM") && input.get(3).map_or(false, |n| *n >= b'1' && *n <= b'9') && is_done_windows(input.get(4..)) { return Some(component::Error::WindowsReservedName); } - if in3.eq_ignore_ascii_case(b"lpt") + if in3.eq_ignore_ascii_case(b"LPT") && input.get(3).map_or(false, u8::is_ascii_digit) && is_done_windows(input.get(4..)) { return Some(component::Error::WindowsReservedName); } - if in3.eq_ignore_ascii_case(b"con") + if in3.eq_ignore_ascii_case(b"CON") && (is_done_windows(input.get(3..)) - || (input.get(3..6).map_or(false, |n| n.eq_ignore_ascii_case(b"in$")) && is_done_windows(input.get(6..))) - || (input.get(3..7).map_or(false, |n| n.eq_ignore_ascii_case(b"out$")) && is_done_windows(input.get(7..)))) + || (input.get(3..6).map_or(false, |n| n.eq_ignore_ascii_case(b"IN$")) && is_done_windows(input.get(6..))) + || (input.get(3..7).map_or(false, |n| n.eq_ignore_ascii_case(b"OUT$")) && is_done_windows(input.get(7..)))) { return Some(component::Error::WindowsReservedName); } - if input.iter().any(|b| *b < 0x20 || b":<>\"|?*".contains(b)) { + if input.iter().any(|b| b.is_ascii_control() || b":<>\"|?*".contains(b)) { return Some(component::Error::WindowsIllegalCharacter); } if input.ends_with(b".") || input.ends_with(b" ") { @@ -225,6 +231,10 @@ fn is_dot_git_ntfs(input: &BStr) -> bool { false } +/// The `search_case_insensitive` name is the actual name to look for (in a case-insensitive way). +/// Opposed to that there is the special `ntfs_shortname_prefix` which is derived from `search_case_insensitive` +/// but looks more like a hash, one that NTFS uses to disambiguate things, for when there is a lot of files +/// with the same prefix. fn is_dot_ntfs(input: &BStr, search_case_insensitive: &str, ntfs_shortname_prefix: &str) -> bool { if input.first() == Some(&b'.') { let end_pos = 1 + search_case_insensitive.len(); @@ -243,6 +253,8 @@ fn is_dot_ntfs(input: &BStr, search_case_insensitive: &str, ntfs_shortname_prefi .map_or(false, |(ntfs_prefix, first_6_of_input)| { first_6_of_input.eq_ignore_ascii_case(ntfs_prefix) && input.get(6) == Some(&b'~') + // It's notable that only `~1` to `~4` are possible before the disambiguation algorithm + // switches to using the `ntfs_shortname_prefix`, which is checked hereafter. && input.get(7).map_or(false, |num| (b'1'..=b'4').contains(num)) }) { diff --git a/gix-validate/tests/path/mod.rs b/gix-validate/tests/path/mod.rs index 15dc7ba917b..1d4cbed3bbf 100644 --- a/gix-validate/tests/path/mod.rs +++ b/gix-validate/tests/path/mod.rs @@ -65,6 +65,8 @@ mod component { mktest!(conout_without_dollar_with_extension, b"conout.c"); mktest!(conin_without_dollar_with_extension, b"conin.c"); mktest!(conin_without_dollar, b"conin"); + mktest!(not_con, b"com"); + mktest!(also_not_con, b"co"); mktest!(not_nul, b"null"); mktest!( not_dot_gitmodules_shorter_hfs, @@ -115,7 +117,7 @@ mod component { mktest!(empty, b"", Error::Empty); mktest!(dot_git_lower, b".git", Error::DotGitDir, NO_OPTS); mktest!(dot_git_lower_hfs, ".g\u{200c}it".as_bytes(), Error::DotGitDir); - mktest!(dot_git_lower_hfs_simple, ".Git".as_bytes(), Error::DotGitDir); + mktest!(dot_git_mixed_hfs_simple, b".Git", Error::DotGitDir); mktest!(dot_git_upper, b".GIT", Error::DotGitDir, NO_OPTS); mktest!(dot_git_upper_hfs, ".GIT\u{200e}".as_bytes(), Error::DotGitDir); mktest!(dot_git_upper_ntfs_8_3, b"GIT~1", Error::DotGitDir); @@ -164,6 +166,30 @@ mod component { Symlink, ALL_OPTS ); + mktest!( + dot_gitmodules_lower_ntfs_stream_default_implicit, + b".gitmodules::$DATA", + Error::SymlinkedGitModules, + Symlink, + ALL_OPTS + ); + mktest!( + ntfs_stream_default_implicit, + b"file::$DATA", + Error::WindowsIllegalCharacter + ); + mktest!( + ntfs_stream_default_explicit, + b"file:$ANYTHING_REALLY:$DATA", + Error::WindowsIllegalCharacter + ); + mktest!( + dot_gitmodules_lower_ntfs_stream_default_explicit, + b".gitmodules:$DATA:$DATA", + Error::SymlinkedGitModules, + Symlink, + ALL_OPTS + ); mktest!( not_gitmodules_trailing_space, b".gitmodules x ", @@ -201,6 +227,7 @@ mod component { mktest!(nul_mixed, b"NuL", Error::WindowsReservedName); mktest!(prn_mixed_with_extension, b"PrN.abc", Error::WindowsReservedName); mktest!(con, b"CON", Error::WindowsReservedName); + mktest!(con_with_extension, b"CON.abc", Error::WindowsReservedName); mktest!( conout_mixed_with_extension, b"ConOut$ .xyz", diff --git a/gix/src/lib.rs b/gix/src/lib.rs index 23f37e9de28..62b55ee47bc 100644 --- a/gix/src/lib.rs +++ b/gix/src/lib.rs @@ -345,3 +345,12 @@ pub mod shallow; pub mod discover; pub mod env; + +#[cfg(feature = "index")] +fn is_dir_to_mode(is_dir: bool) -> gix_index::entry::Mode { + if is_dir { + gix_index::entry::Mode::DIR + } else { + gix_index::entry::Mode::FILE + } +} diff --git a/gix/src/pathspec.rs b/gix/src/pathspec.rs index e2242609268..4ea3ac7375f 100644 --- a/gix/src/pathspec.rs +++ b/gix/src/pathspec.rs @@ -137,15 +137,7 @@ impl<'repo> Pathspec<'repo> { let stack = self.stack.as_mut().expect("initialized in advance"); stack .set_case(case) - .at_entry( - relative_path, - Some(if is_dir { - gix_index::entry::Mode::DIR - } else { - gix_index::entry::Mode::FILE - }), - &self.repo.objects, - ) + .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &self.repo.objects) .map_or(false, |platform| platform.matching_attributes(out)) }, ) @@ -201,15 +193,7 @@ impl PathspecDetached { let stack = self.stack.as_mut().expect("initialized in advance"); stack .set_case(case) - .at_entry( - relative_path, - Some(if is_dir { - gix_index::entry::Mode::DIR - } else { - gix_index::entry::Mode::FILE - }), - &self.odb, - ) + .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &self.odb) .map_or(false, |platform| platform.matching_attributes(out)) }, ) @@ -223,3 +207,11 @@ impl PathspecDetached { .map_or(false, |m| !m.is_excluded()) } } + +fn is_dir_to_mode(is_dir: bool) -> gix_index::entry::Mode { + if is_dir { + gix_index::entry::Mode::DIR + } else { + gix_index::entry::Mode::FILE + } +} diff --git a/gix/src/repository/dirwalk.rs b/gix/src/repository/dirwalk.rs index 762c69def60..db64410e42a 100644 --- a/gix/src/repository/dirwalk.rs +++ b/gix/src/repository/dirwalk.rs @@ -1,7 +1,7 @@ use crate::bstr::{BStr, BString}; use crate::util::OwnedOrStaticAtomicBool; use crate::worktree::IndexPersistedOrInMemory; -use crate::{config, dirwalk, Repository}; +use crate::{config, dirwalk, is_dir_to_mode, Repository}; use std::sync::atomic::AtomicBool; impl Repository { @@ -64,15 +64,7 @@ impl Repository { .expect("can only be called if attributes are used in patterns"); stack .set_case(case) - .at_entry( - relative_path, - Some(if is_dir { - gix_index::entry::Mode::DIR - } else { - gix_index::entry::Mode::FILE - }), - &self.objects, - ) + .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &self.objects) .map_or(false, |platform| platform.matching_attributes(out)) }, excludes: Some(&mut excludes.inner), diff --git a/gix/src/submodule/mod.rs b/gix/src/submodule/mod.rs index a530ba6b036..dfeb34ef8cf 100644 --- a/gix/src/submodule/mod.rs +++ b/gix/src/submodule/mod.rs @@ -9,7 +9,7 @@ use std::{ pub use gix_submodule::*; -use crate::{bstr::BStr, worktree::IndexPersistedOrInMemory, Repository, Submodule}; +use crate::{bstr::BStr, is_dir_to_mode, worktree::IndexPersistedOrInMemory, Repository, Submodule}; pub(crate) type ModulesFileStorage = gix_features::threading::OwnShared>; /// A lazily loaded and auto-updated worktree index. @@ -154,15 +154,7 @@ impl<'repo> Submodule<'repo> { &mut |relative_path, case, is_dir, out| { attributes .set_case(case) - .at_entry( - relative_path, - Some(if is_dir { - gix_index::entry::Mode::DIR - } else { - gix_index::entry::Mode::FILE - }), - &self.state.repo.objects, - ) + .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &self.state.repo.objects) .map_or(false, |platform| platform.matching_attributes(out)) } })?; From ccbc1197b6dcb7e7118e206183582d6b46fc5ebc Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 21 May 2024 07:19:37 +0200 Subject: [PATCH 31/50] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The [Naming Files, Paths, and Namespaces](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file) article does not state that control characters or non-printable characters are in general forbidden in filenames. Instead, it says that it is okay to > Use any character in the current code page for a name, including Unicode characters and characters in the extended character set (128โ€“255), except for the following: and then lists various things that are not allowed, where the one that is relevant to control characters is: > Characters whose integer representations are in the range from 1 through 31, except for alternate data streams where these characters are allowed. *[...]* No mention is made of 127 (0x7F). On Windows 10, I used PowerShell 7 for this experiment, which I believe would also work in PowerShell 6, but not Windows PowerShell, which doesn't support `` `u ``. First, as a baseline, I checked what happened if I tried to create a file whose name contained a low-numbered control character: ```text C:\Users\ek\source\repos\unusual-filenames [main]> echo hello > a`u{8}b Out-File: The filename, directory name, or volume label syntax is incorrect. : 'C:\Users\ek\source\repos\unusual-filenames\b' C:\Users\ek\source\repos\unusual-filenames [main]> echo hello > a`u{08}b Out-File: The filename, directory name, or volume label syntax is incorrect. : 'C:\Users\ek\source\repos\unusual-filenames\b' ``` I created a file whose name contained the `DEL` character, and even a file whose entire name is that character: ```text C:\Users\ek\source\repos\unusual-filenames [main]> echo hello > a`u{7F}b C:\Users\ek\source\repos\unusual-filenames [main +1 ~0 -0 !]> echo goodbye > `u{7F} C:\Users\ek\source\repos\unusual-filenames [main +2 ~0 -0 !]> ls Directory: C:\Users\ek\source\repos\unusual-filenames Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 5/20/2024 5:59 PM 9 -a--- 5/20/2024 5:59 PM 7 ab ``` Thus this appears to work fine on Windows, and it seems fine that Git permits it: ```text C:\Users\ek\source\repos\unusual-filenames [main +2 ~0 -0 !]> git status On branch main No commits yet Untracked files: (use "git add ..." to include in what will be committed) "a\177b" "\177" nothing added to commit but untracked files present (use "git add" to track) C:\Users\ek\source\repos\unusual-filenames [main +2 ~0 -0 !]> git add . C:\Users\ek\source\repos\unusual-filenames [main +2 ~0 -0 ~]> git commit -m 'Initial commit' [main (root-commit) 543ccd5] Initial commit 2 files changed, 2 insertions(+) create mode 100644 "a\177b" create mode 100644 "\177" ``` Thus, gitoxide should probably permit it too. To be sure, I also tried creating such a file in Python 3.12 on the same system, by calling the `touch` method on a `Path` object. That worked, too. Co-authored-by: Eliah Kagan --- gix-fs/tests/stack/mod.rs | 14 +++++++------- gix-validate/src/path.rs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gix-fs/tests/stack/mod.rs b/gix-fs/tests/stack/mod.rs index 74ec7e0dab8..64a293eda38 100644 --- a/gix-fs/tests/stack/mod.rs +++ b/gix-fs/tests/stack/mod.rs @@ -58,7 +58,7 @@ fn path_join_handling() { assert_eq!( p("c:").join("relative"), p("c:relative"), - "absolute + relative = strange joined result with missing back-slash, but it's a valid path that works just like `c:\relative`" + "absolute + relative = strange joined result with missing backslash, but it's a valid path that works just like `c:\relative`" ); assert_eq!( p("c:\\").join("relative"), @@ -74,7 +74,7 @@ fn path_join_handling() { assert_eq!( p("\\\\.\\base").join(absolute), p("\\\\.\\base\\absolute"), - "absolute1 + absolute2 = joined result with backslash (device relative)" + "absolute1 + absolute2 = joined result with backslash (device namespace)" ); assert_eq!( p("\\\\?\\base").join(bs_absolute), @@ -84,7 +84,7 @@ fn path_join_handling() { assert_eq!( p("\\\\.\\base").join(bs_absolute), p("\\\\.\\base\\absolute"), - "absolute1 + absolute2 = joined result (device relative)" + "absolute1 + absolute2 = joined result (device namespace)" ); assert_eq!(p("/").join("C:"), p("C:"), "unix-absolute + win-drive = win-drive"); @@ -101,7 +101,7 @@ fn path_join_handling() { assert_eq!( p("c:\\").join("\\\\.\\"), p("\\\\.\\"), - "d-drive-with-bs + device-relative-unc = device-relative-unc" + "d-drive-with-bs + device-namespace-unc = device-namespace-unc" ); assert_eq!( p("/").join("C:/"), @@ -112,7 +112,7 @@ fn path_join_handling() { assert_eq!( p("\\\\.").join("C:"), p("C:"), - "device-relative-unc + win-drive-relative = win-drive-relative - c: was supposed to be relative, but it's not acting like it." + "device-namespace-unc + win-drive-relative = win-drive-relative - c: was supposed to be relative, but it's not acting like it." ); assert_eq!(p("relative").join("C:"), p("C:"), "relative + win-drive = win-drive"); @@ -150,7 +150,7 @@ fn path_join_handling() { "absolute1 + absolute2 = absolute2" ); - assert_eq!(p("/").join("C:"), p("/C:"), "absolute + win-absolute = joined result"); + assert_eq!(p("/").join("C:"), p("/C:"), "absolute + win-drive = joined result"); assert_eq!(p("/").join("C:/"), p("/C:/"), "absolute + win-absolute = joined result"); assert_eq!( p("/").join("C:\\"), @@ -160,7 +160,7 @@ fn path_join_handling() { assert_eq!( p("relative").join("C:"), p("relative/C:"), - "relative + win-absolute = joined result" + "relative + win-drive = joined result" ); assert_eq!( diff --git a/gix-validate/src/path.rs b/gix-validate/src/path.rs index 9e3fd91e7ae..dc85fa8f7fd 100644 --- a/gix-validate/src/path.rs +++ b/gix-validate/src/path.rs @@ -160,7 +160,7 @@ fn check_win_devices_and_illegal_characters(input: &BStr) -> Option\"|?*".contains(b)) { + if input.iter().any(|b| *b < 0x20 || b":<>\"|?*".contains(b)) { return Some(component::Error::WindowsIllegalCharacter); } if input.ends_with(b".") || input.ends_with(b" ") { From fe8c2c939db69ffce855059e2b16be50efcc05e6 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 20 May 2024 21:28:16 -0400 Subject: [PATCH 32/50] Adjust make_traverse_dotdot_slashes.sh for environment These are changes that do not significantly affect behavior but use the set of tools that should be availalble in testing environments, as well as refactorings that are useful to do not before really making this usable as a fixture. - Use bash shebang, enable pipefail. - Don't require xxd. - Don't create an extra temporary file. - Shorten, simplify, and clarify some logic. --- .../fixtures/make_traverse_dotdot_slashes.sh | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/gix-worktree/tests/fixtures/make_traverse_dotdot_slashes.sh b/gix-worktree/tests/fixtures/make_traverse_dotdot_slashes.sh index d9938640511..d247de6036f 100755 --- a/gix-worktree/tests/fixtures/make_traverse_dotdot_slashes.sh +++ b/gix-worktree/tests/fixtures/make_traverse_dotdot_slashes.sh @@ -1,28 +1,28 @@ -#!/bin/sh -set -eu +#!/bin/bash +set -eu -o pipefail readonly filename='../outside' readonly filemode=100644 emit_payload() { - printf 'A file outside the working tree, somehow.\n' + echo 'A file outside the working tree, somehow.' } repo="$1" git init -- "$repo" cd -- "$repo" -blob_hash="$(emit_payload | git hash-object -w --stdin)" -printf '%s' "$blob_hash" | xxd -r -p >blob-hash-bytes +blob_hash_escaped="$( + emit_payload | + git hash-object -w --stdin | + sed 's/../\\x&/g' +)" tree_hash="$( - printf '%s %s\0' "$filemode" "$filename" | - cat - blob-hash-bytes | + printf "%s %s\\0$blob_hash_escaped" "$filemode" "$filename" | git hash-object -t tree -w --stdin --literally )" -rm blob-hash-bytes - commit_hash="$(git commit-tree -m 'Initial commit' "$tree_hash")" branch="$(git symbolic-ref --short HEAD)" git branch -f -- "$branch" "$commit_hash" From 7e9c76993a72f1d982cb1c1f73dcd42afe3ec6d2 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 20 May 2024 21:36:45 -0400 Subject: [PATCH 33/50] Combine "slashes" scripts and make it a fixture Keeping the changes to make_traverse_dotdot_slashes.sh, this folds the make_traverse_dotgit_slsahes.sh logic into it, extracting the twice-used parts (which are most of the script) into a function that both call. Other changes: - No longer use command-line arguments. There are two repositories that are currently useful to make in this way, and this calls the function for each of them. - Change the style to mostly match that of other fixture scripts, including decreasing the indent from 4 to 2 and using the function keyword when defining functions. - Shorten variable names in cases where doing so is unambiguous (but not otherwise). - Eliminate the emit_payload function, since the new make_repo function now receives the content on standard input, which can be provided by whatever means is convenient (the current calls use a here string for the one-line file and a heredoc otherwise). --- .../fixtures/make_traverse_dotdot_slashes.sh | 29 --------------- .../fixtures/make_traverse_dotgit_slashes.sh | 33 ----------------- .../fixtures/make_traverse_literal_slashes.sh | 35 +++++++++++++++++++ 3 files changed, 35 insertions(+), 62 deletions(-) delete mode 100755 gix-worktree/tests/fixtures/make_traverse_dotdot_slashes.sh delete mode 100755 gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh create mode 100755 gix-worktree/tests/fixtures/make_traverse_literal_slashes.sh diff --git a/gix-worktree/tests/fixtures/make_traverse_dotdot_slashes.sh b/gix-worktree/tests/fixtures/make_traverse_dotdot_slashes.sh deleted file mode 100755 index d247de6036f..00000000000 --- a/gix-worktree/tests/fixtures/make_traverse_dotdot_slashes.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -set -eu -o pipefail - -readonly filename='../outside' -readonly filemode=100644 - -emit_payload() { - echo 'A file outside the working tree, somehow.' -} - -repo="$1" -git init -- "$repo" -cd -- "$repo" - -blob_hash_escaped="$( - emit_payload | - git hash-object -w --stdin | - sed 's/../\\x&/g' -)" - -tree_hash="$( - printf "%s %s\\0$blob_hash_escaped" "$filemode" "$filename" | - git hash-object -t tree -w --stdin --literally -)" - -commit_hash="$(git commit-tree -m 'Initial commit' "$tree_hash")" -branch="$(git symbolic-ref --short HEAD)" -git branch -f -- "$branch" "$commit_hash" -git show diff --git a/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh b/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh deleted file mode 100755 index 6eca3a267e5..00000000000 --- a/gix-worktree/tests/fixtures/make_traverse_dotgit_slashes.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/sh -set -eu - -readonly filename='.git/hooks/pre-commit' -readonly filemode=100755 - -emit_payload() { - cat <<'EOF' -#!/bin/sh -printf 'Vulnerable!\n' -date >vulnerable -EOF -} - -repo="$1" -git init -- "$repo" -cd -- "$repo" - -blob_hash="$(emit_payload | git hash-object -w --stdin)" -printf '%s' "$blob_hash" | xxd -r -p >blob-hash-bytes - -tree_hash="$( - printf '%s %s\0' "$filemode" "$filename" | - cat - blob-hash-bytes | - git hash-object -t tree -w --stdin --literally -)" - -rm blob-hash-bytes - -commit_hash="$(git commit-tree -m 'Initial commit' "$tree_hash")" -branch="$(git symbolic-ref --short HEAD)" -git branch -f -- "$branch" "$commit_hash" -git show diff --git a/gix-worktree/tests/fixtures/make_traverse_literal_slashes.sh b/gix-worktree/tests/fixtures/make_traverse_literal_slashes.sh new file mode 100755 index 00000000000..bd8b5fa6245 --- /dev/null +++ b/gix-worktree/tests/fixtures/make_traverse_literal_slashes.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -eu -o pipefail + +# Makes a repo carrying a literally named file, which may even contain "/". +# File content is from stdin. Arguments are repo name, file name, and file mode. +function make_repo() ( + local repo="$1" file="$2" mode="$3" + local blob_hash_escaped tree_hash commit_hash branch + + git init -- "$repo" + cd -- "$repo" # Temporary, as the function body is a ( ) subshell. + + blob_hash_escaped="$(git hash-object -w --stdin | sed 's/../\\x&/g')" + + tree_hash="$( + printf "%s %s\\0$blob_hash_escaped" "$mode" "$file" | + git hash-object -t tree -w --stdin --literally + )" + + commit_hash="$(git commit-tree -m 'Initial commit' "$tree_hash")" + + branch="$(git symbolic-ref --short HEAD)" + git branch -f -- "$branch" "$commit_hash" + test -z "${DEBUG_FIXTURE-}" || git show # TODO: How should verbosity be controlled? +) + +make_repo traverse_dotdot_slashes ../outside 100644 \ + <<<'A file outside the working tree, somehow.' + +# TODO: Should the payload be simplified to a single side effect for tests to check? +make_repo traverse_dotgit_slashes .git/hooks/pre-commit 100755 <<'EOF' +#!/bin/sh +printf 'Vulnerable!\n' +date >vulnerable +EOF From 6f44aca4dc1e04f082f5d6c6bdf0b11df28d3a28 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 21 May 2024 00:07:43 -0400 Subject: [PATCH 34/50] Combine non-"slashes" (i.e. trees) scripts and make it a fixture At least for now, this does not test the creation of multiple files at a time outside of a repository, nor multi-step upward traversal with many `../../..` components, since tests using such fixtures would be complicated, and may or may not be warranted in the test suite. However, this combines substantial elements of the scripts that create repositories with unexpected tree objects (e.g., `..` trees) to make a make_traverse_trees.sh script that, when run, produces repositories for testing that traverse: - Upward with a `..` tree: `traverse_dotdot_tree` - Downward with `.git` and `hooks` trees: `traverse_dotgit_trees` - Similar but with an NTFS stream alias: `traverse_dotgit_stream` This replaces the `make_traverse_dotdot_trees.sh` and `make_traverse_ntfs_streams.sh` scripts with one script that takes no command-line arguments and creates multiple repos by calling a function. This is thus architecturally similar, broadly speaking, to `make_traverse_literal_slashes.sh`, but that produces repos with very strangely named blobs, rather than with strangly named trees. --- .../fixtures/make_traverse_dotdot_trees.sh | 33 ---------------- .../fixtures/make_traverse_ntfs_stream.sh | 28 ------------- .../tests/fixtures/make_traverse_trees.sh | 39 +++++++++++++++++++ 3 files changed, 39 insertions(+), 61 deletions(-) delete mode 100755 gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh delete mode 100755 gix-worktree/tests/fixtures/make_traverse_ntfs_stream.sh create mode 100755 gix-worktree/tests/fixtures/make_traverse_trees.sh diff --git a/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh b/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh deleted file mode 100755 index 586555bdb29..00000000000 --- a/gix-worktree/tests/fixtures/make_traverse_dotdot_trees.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/sh -# TODO: Before using in tests, limit this to never target real bin dirs! -set -eu - -repo="$1" -target_bin='.cargo/bin' - -git init -- "$repo" -cd -- "$repo" - -cat >payload <<'EOF' -#!/bin/sh -printf 'Vulnerable!\n' -date >~/vulnerable -exec /bin/ls "$@" -EOF - -upward='..' -for subdir in .a .b .c .d .e .f .g .h .i .j; do - upward="../$upward" - target="$subdir/$upward/$target_bin/ls" - standin="$(printf '%s' "$target" | tr / @)" - - cp -- payload "$standin" - git add --chmod=+x -- "$standin" - - standin_pattern="$(printf '%s' "$standin" | sed 's|\.|\\\.|g')" - cp .git/index old_index - LC_ALL=C sed "s|$standin_pattern|$target|g" old_index >.git/index -done - -git commit -m 'Initial commit' -rm payload old_index diff --git a/gix-worktree/tests/fixtures/make_traverse_ntfs_stream.sh b/gix-worktree/tests/fixtures/make_traverse_ntfs_stream.sh deleted file mode 100755 index d5789c788d2..00000000000 --- a/gix-worktree/tests/fixtures/make_traverse_ntfs_stream.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/sh -set -eu - -repo="$1" -git init -- "$repo" -cd -- "$repo" - -# shellcheck disable=SC2016 -target_dir='subdir/.git::$INDEX_ALLOCATION/hooks' -target_dir_standin="$(printf '%s' "$target_dir" | sed 's|:|,|g')" -target_file="$target_dir/pre-commit" -target_file_standin="$target_dir_standin/pre-commit" - -mkdir -p -- "$target_dir_standin" - -cat >"$target_file_standin" <<'EOF' -#!/bin/sh -printf 'Vulnerable!\n' -date >vulnerable -EOF - -git add --chmod=+x -- "$target_file_standin" - -standin_pattern="$(printf '%s' "$target_file_standin" | sed 's|[.$]|\\&|g')" -cp .git/index old_index -LC_ALL=C sed "s|$standin_pattern|$target_file|g" old_index >.git/index - -git commit -m 'Initial commit' diff --git a/gix-worktree/tests/fixtures/make_traverse_trees.sh b/gix-worktree/tests/fixtures/make_traverse_trees.sh new file mode 100755 index 00000000000..0dd59db0da1 --- /dev/null +++ b/gix-worktree/tests/fixtures/make_traverse_trees.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -eu -o pipefail + +# Makes a repo carrying a tree structure representing the given path to a blob. +# File content is from stdin. Args are repo name, path, -x or +x, and tr sets. +function make_repo() ( + local repo="$1" path="$2" xbit="$3" set1="$4" set2="$5" + local dir dir_standin path_standin path_standin_pattern path_replacement + + git init -- "$repo" + cd -- "$repo" # Temporary, as the function body is a ( ) subshell. + + dir="${path%/*}" + dir_standin="$(tr "$set1" "$set2" <<<"$dir")" + path_standin="$(tr "$set1" "$set2" <<<"$path")" + mkdir -p -- "$dir_standin" + cat >"$path_standin" + git add --chmod="$xbit" -- "$path_standin" + path_standin_pattern="$(sed 's/[|.*^$\]/\\&/g' <<<"$path_standin")" + path_replacement="$(sed 's/[|&\]/\\&/g' <<<"$path")" + cp .git/index old_index + LC_ALL=C sed "s|$path_standin_pattern|$path_replacement|g" old_index >.git/index + git commit -m 'Initial commit' +) + +make_repo traverse_dotdot_trees '../outside' -x '.' '@' \ + <<<'A file outside the working tree, somehow.' + +make_repo traverse_dotgit_trees '.git/hooks/pre-commit' +x '.' '@' <<'EOF' +#!/bin/sh +printf 'Vulnerable!\n' +date >vulnerable +EOF + +make_repo traverse_dotgit_stream '.git::$INDEX_ALLOCATION/hooks/pre-commit' +x ':' ',' <<'EOF' +#!/bin/sh +printf 'Vulnerable!\n' +date >vulnerable +EOF From f3edaa352ab266de2d24b7b71133bcc17ee661b3 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 21 May 2024 00:24:14 -0400 Subject: [PATCH 35/50] Make more test repos with traversal-attempting blob names The approach in make_traverse_literal_slases.sh works about equally well for any top level file with strange characters. Before, it was only generating such repositores where the filename has slashes, causing traversal on all platforms. This has is generate two more repositories, with backslashes instead of slashes. That script's name is accordingly updated to make_traverse_literal_separators.sh. Note that while such names with backslashes may be blocked on multiple systems under various circumstances, they will only perform traversal on Windows. --- ..._slashes.sh => make_traverse_literal_separators.sh} | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) rename gix-worktree/tests/fixtures/{make_traverse_literal_slashes.sh => make_traverse_literal_separators.sh} (81%) diff --git a/gix-worktree/tests/fixtures/make_traverse_literal_slashes.sh b/gix-worktree/tests/fixtures/make_traverse_literal_separators.sh similarity index 81% rename from gix-worktree/tests/fixtures/make_traverse_literal_slashes.sh rename to gix-worktree/tests/fixtures/make_traverse_literal_separators.sh index bd8b5fa6245..533b5f27975 100755 --- a/gix-worktree/tests/fixtures/make_traverse_literal_slashes.sh +++ b/gix-worktree/tests/fixtures/make_traverse_literal_separators.sh @@ -27,9 +27,17 @@ function make_repo() ( make_repo traverse_dotdot_slashes ../outside 100644 \ <<<'A file outside the working tree, somehow.' -# TODO: Should the payload be simplified to a single side effect for tests to check? make_repo traverse_dotgit_slashes .git/hooks/pre-commit 100755 <<'EOF' #!/bin/sh printf 'Vulnerable!\n' date >vulnerable EOF + +make_repo traverse_dotdot_backslashes '..\outside' 100644 \ + <<<'A file outside the working tree, somehow.' + +make_repo traverse_dotgit_backslashes '.git\hooks\pre-commit' 100755 <<'EOF' +#!/bin/sh +printf 'Vulnerable!\n' +date >vulnerable +EOF From 4791e314f217e83b628baec60ad05f5f8f571f36 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 21 May 2024 08:39:18 +0200 Subject: [PATCH 36/50] further testing of `.git` path variants This is to see if anything should be done to more effectively prevent paths containing `.git` (icase). In conclusion, I think it's fine to keep allowing it as none of the component-validations really kicks in on Linux if backslashes are used as path separator. Thus, `.git` shouldn't be more special than `..` for example. The only way to fix this on Linux would be to either enable Windows protections, or to disallow `\` as path seprator by default which seems too limitting. Windows Users will naturally be protected as path-splitting will turn these into components, with each of them checked as normal. --- gix-validate/tests/path/mod.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/gix-validate/tests/path/mod.rs b/gix-validate/tests/path/mod.rs index 1d4cbed3bbf..7adf39db830 100644 --- a/gix-validate/tests/path/mod.rs +++ b/gix-validate/tests/path/mod.rs @@ -54,6 +54,11 @@ mod component { mktest!(not_dot_gitmodules_longer, b".gitmodulesa", Symlink, NO_OPTS); mktest!(not_dot_gitmodules_longer_all, b".gitmodulesa", Symlink, ALL_OPTS); mktest!(dot_gitmodules_as_file, b".gitmodules", UNIX_OPTS); + mktest!( + starts_with_dot_git_with_backslashes_on_linux, + b".git\\hooks\\precommit", + UNIX_OPTS + ); mktest!(not_dot_git_shorter, b".gi", NO_OPTS); mktest!(not_dot_git_shorter_ntfs_8_3, b"gi~1"); mktest!(not_dot_git_longer_ntfs_8_3, b"gitu~1"); @@ -119,6 +124,11 @@ mod component { mktest!(dot_git_lower_hfs, ".g\u{200c}it".as_bytes(), Error::DotGitDir); mktest!(dot_git_mixed_hfs_simple, b".Git", Error::DotGitDir); mktest!(dot_git_upper, b".GIT", Error::DotGitDir, NO_OPTS); + mktest!( + starts_with_dot_git_with_backslashes_on_windows, + b".git\\hooks\\precommit", + Error::PathSeparator + ); mktest!(dot_git_upper_hfs, ".GIT\u{200e}".as_bytes(), Error::DotGitDir); mktest!(dot_git_upper_ntfs_8_3, b"GIT~1", Error::DotGitDir); mktest!(dot_git_mixed, b".gIt", Error::DotGitDir, NO_OPTS); From 00a1c47e7f3566feb14a4926b0cd3834f7007686 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 21 May 2024 09:14:28 +0200 Subject: [PATCH 37/50] better detection of pre-requisites for symlink test (#1373) If we are dependent on symlinks, we should be sure that the probe actually detects symlinks. --- gix-worktree-state/tests/state/checkout.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gix-worktree-state/tests/state/checkout.rs b/gix-worktree-state/tests/state/checkout.rs index c38a81877af..5591b1e17ff 100644 --- a/gix-worktree-state/tests/state/checkout.rs +++ b/gix-worktree-state/tests/state/checkout.rs @@ -178,6 +178,10 @@ fn overwriting_files_and_lone_directories_works() -> crate::Result { gix_filter::driver::apply::Delay::Forbid, ] { let mut opts = opts_from_probe(); + assert!( + opts.fs.symlink, + "BUG: the probe must detect to be able to generate symlinks" + ); opts.overwrite_existing = true; opts.filter_process_delay = delay; opts.destination_is_initially_empty = false; @@ -197,7 +201,7 @@ fn overwriting_files_and_lone_directories_works() -> crate::Result { let dir = dir.join("sub-dir"); std::fs::create_dir(&dir)?; - symlink::symlink_dir(empty, dir.join("symlink"))?; // 'symlink' is a symlink to another file + symlink::symlink_dir(empty, dir.join("symlink"))?; // 'symlink' is a symlink to a directory. Ok(()) }, )?; From bec648dc5790186cddbe12277978baf572f8e164 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 21 May 2024 09:44:09 +0200 Subject: [PATCH 38/50] fix: multi-process safe parallel filesystem capabilities probing (#1373) This is achieved by making filenames unique so they won't clash. --- Cargo.lock | 22 ++++++++++++---------- gix-fs/Cargo.toml | 4 ++++ gix-fs/src/capabilities.rs | 11 +++++++---- gix-fs/tests/capabilities/mod.rs | 26 ++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ab1a3612362..374ce53324c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,7 +175,7 @@ dependencies = [ "async-lock 3.2.0", "async-task", "concurrent-queue", - "fastrand 2.0.1", + "fastrand 2.1.0", "futures-lite 2.1.0", "slab", ] @@ -390,7 +390,7 @@ dependencies = [ "async-channel 2.1.1", "async-lock 3.2.0", "async-task", - "fastrand 2.0.1", + "fastrand 2.1.0", "futures-io", "futures-lite 2.1.0", "piper", @@ -1004,9 +1004,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "filetime" @@ -1146,7 +1146,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" dependencies = [ - "fastrand 2.0.1", + "fastrand 2.1.0", "futures-core", "futures-io", "parking", @@ -1810,6 +1810,8 @@ dependencies = [ name = "gix-fs" version = "0.10.2" dependencies = [ + "crossbeam-channel", + "fastrand 2.1.0", "gix-features 0.38.1", "gix-utils 0.1.12", "serde", @@ -2593,7 +2595,7 @@ version = "0.14.0" dependencies = [ "bstr", "crc", - "fastrand 2.0.1", + "fastrand 2.1.0", "fs_extra", "gix-discover 0.26.0", "gix-fs 0.10.2", @@ -2727,7 +2729,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f82c41937f00e15a1f6cb0b55307f0ca1f77f4407ff2bf440be35aa688c6a3e" dependencies = [ - "fastrand 2.0.1", + "fastrand 2.1.0", ] [[package]] @@ -2735,7 +2737,7 @@ name = "gix-utils" version = "0.1.12" dependencies = [ "bstr", - "fastrand 2.0.1", + "fastrand 2.1.0", "unicode-normalization", ] @@ -3709,7 +3711,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" dependencies = [ "atomic-waker", - "fastrand 2.0.1", + "fastrand 2.1.0", "futures-io", ] @@ -4533,7 +4535,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" dependencies = [ "cfg-if", - "fastrand 2.0.1", + "fastrand 2.1.0", "rustix 0.38.31", "windows-sys 0.52.0", ] diff --git a/gix-fs/Cargo.toml b/gix-fs/Cargo.toml index ebdd3993a24..447c9787ffa 100644 --- a/gix-fs/Cargo.toml +++ b/gix-fs/Cargo.toml @@ -21,5 +21,9 @@ gix-features = { version = "^0.38.1", path = "../gix-features", features = ["fs- gix-utils = { version = "^0.1.12", path = "../gix-utils" } serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"] } +# For `Capabilities` to assure parallel operation works. +fastrand = { version = "2.1.0", default-features = false, features = ["std"] } + [dev-dependencies] tempfile = "3.5.0" +crossbeam-channel = "0.5.0" diff --git a/gix-fs/src/capabilities.rs b/gix-fs/src/capabilities.rs index 3a384a26dca..4fa6892eed7 100644 --- a/gix-fs/src/capabilities.rs +++ b/gix-fs/src/capabilities.rs @@ -60,7 +60,8 @@ impl Capabilities { use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; // test it exactly as we typically create executable files, not using chmod. - let test_path = root.join("_test_executable_bit"); + let rand = fastrand::usize(..); + let test_path = root.join(format!("_test_executable_bit{rand}")); let res = std::fs::OpenOptions::new() .create_new(true) .write(true) @@ -87,8 +88,9 @@ impl Capabilities { } fn probe_precompose_unicode(root: &Path) -> std::io::Result { - let precomposed = "รค"; - let decomposed = "a\u{308}"; + let rand = fastrand::usize(..); + let precomposed = format!("รค{rand}"); + let decomposed = format!("a\u{308}{rand}"); let precomposed = root.join(precomposed); std::fs::OpenOptions::new() @@ -101,7 +103,8 @@ impl Capabilities { } fn probe_symlink(root: &Path) -> std::io::Result { - let link_path = root.join("__file_link"); + let rand = fastrand::usize(..); + let link_path = root.join(format!("__file_link{rand}")); if crate::symlink::create("dangling".as_ref(), &link_path).is_err() { return Ok(false); } diff --git a/gix-fs/tests/capabilities/mod.rs b/gix-fs/tests/capabilities/mod.rs index 42e5255d423..749bfb3a61f 100644 --- a/gix-fs/tests/capabilities/mod.rs +++ b/gix-fs/tests/capabilities/mod.rs @@ -20,3 +20,29 @@ fn probe() { assert!(caps.executable_bit, "Unix should always honor executable bits"); } } + +#[test] +fn parallel_probe() { + let dir = tempfile::tempdir().unwrap(); + std::fs::File::create(dir.path().join("config")).unwrap(); + let baseline = gix_fs::Capabilities::probe(dir.path()); + + let (tx, rx) = crossbeam_channel::unbounded::<()>(); + let threads: Vec<_> = (0..10) + .map(|_id| { + std::thread::spawn({ + let dir = dir.path().to_owned(); + let rx = rx.clone(); + move || { + for _ in rx {} + let actual = gix_fs::Capabilities::probe(&dir); + assert_eq!(actual, baseline); + } + }) + }) + .collect(); + drop((rx, tx)); + for thread in threads { + thread.join().expect("no panic"); + } +} From a6710c552670412cbb3d3d175c243ed086f25f33 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 21 May 2024 08:52:12 +0200 Subject: [PATCH 39/50] add tests for actual worktree checkouts to assure validations kick in --- .../make_traverse_trees.tar.xz | Bin 0 -> 12144 bytes .../tests/fixtures/make_traverse_trees.sh | 0 gix-worktree-state/tests/state/checkout.rs | 84 +++++++++++++++--- .../make_traverse_literal_separators.sh | 43 --------- 4 files changed, 71 insertions(+), 56 deletions(-) create mode 100644 gix-worktree-state/tests/fixtures/generated-archives/make_traverse_trees.tar.xz rename {gix-worktree => gix-worktree-state}/tests/fixtures/make_traverse_trees.sh (100%) delete mode 100755 gix-worktree/tests/fixtures/make_traverse_literal_separators.sh diff --git a/gix-worktree-state/tests/fixtures/generated-archives/make_traverse_trees.tar.xz b/gix-worktree-state/tests/fixtures/generated-archives/make_traverse_trees.tar.xz new file mode 100644 index 0000000000000000000000000000000000000000..930081b86fe18e2fcd7b92656b393f24f4f81f99 GIT binary patch literal 12144 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 literal 0 HcmV?d00001 diff --git a/gix-worktree/tests/fixtures/make_traverse_trees.sh b/gix-worktree-state/tests/fixtures/make_traverse_trees.sh similarity index 100% rename from gix-worktree/tests/fixtures/make_traverse_trees.sh rename to gix-worktree-state/tests/fixtures/make_traverse_trees.sh diff --git a/gix-worktree-state/tests/state/checkout.rs b/gix-worktree-state/tests/state/checkout.rs index 5591b1e17ff..2a852ce7165 100644 --- a/gix-worktree-state/tests/state/checkout.rs +++ b/gix-worktree-state/tests/state/checkout.rs @@ -51,7 +51,7 @@ fn assure_is_empty(dir: impl AsRef) -> std::io::Result<()> { fn submodules_are_instantiated_as_directories() -> crate::Result { let mut opts = opts_from_probe(); opts.overwrite_existing = false; - let (_source_tree, destination, _index, _outcome) = checkout_index_in_tmp_dir(opts.clone(), "make_mixed")?; + let (_source_tree, destination, _index, _outcome) = checkout_index_in_tmp_dir(opts.clone(), "make_mixed", None)?; for path in ["m1", "modules/m1"] { let sm = destination.path().join(path); @@ -68,7 +68,7 @@ fn accidental_writes_through_symlinks_are_prevented_if_overwriting_is_forbidden( // without overwrite mode, everything is safe. opts.overwrite_existing = false; let (source_tree, destination, _index, outcome) = - checkout_index_in_tmp_dir(opts.clone(), "make_dangerous_symlink").unwrap(); + checkout_index_in_tmp_dir(opts.clone(), "make_dangerous_symlink", None).unwrap(); let source_files = dir_structure(&source_tree); let worktree_files = dir_structure(&destination); @@ -109,7 +109,7 @@ fn writes_through_symlinks_are_prevented_even_if_overwriting_is_allowed() { // with overwrite mode opts.overwrite_existing = true; let (source_tree, destination, _index, outcome) = - checkout_index_in_tmp_dir(opts.clone(), "make_dangerous_symlink").unwrap(); + checkout_index_in_tmp_dir(opts.clone(), "make_dangerous_symlink", None).unwrap(); let source_files = dir_structure(&source_tree); let worktree_files = dir_structure(&destination); @@ -144,8 +144,13 @@ fn delayed_driver_process() -> crate::Result { opts.filter_process_delay = gix_filter::driver::apply::Delay::Allow; opts.destination_is_initially_empty = false; setup_filter_pipeline(opts.filters.options_mut()); - let (_source, destination, _index, outcome) = - checkout_index_in_tmp_dir_opts(opts, "make_mixed_without_submodules_and_symlinks", |_| true, |_| Ok(()))?; + let (_source, destination, _index, outcome) = checkout_index_in_tmp_dir_opts( + opts, + "make_mixed_without_submodules_and_symlinks", + None, + |_| true, + |_| Ok(()), + )?; assert_eq!(outcome.collisions.len(), 0); assert_eq!(outcome.errors.len(), 0); assert_eq!(outcome.files_updated, 5); @@ -189,6 +194,7 @@ fn overwriting_files_and_lone_directories_works() -> crate::Result { let (source, destination, _index, outcome) = checkout_index_in_tmp_dir_opts( opts.clone(), "make_mixed", + None, |_| true, |d| { let empty = d.join("empty"); @@ -254,7 +260,7 @@ fn symlinks_become_files_if_disabled() -> crate::Result { let mut opts = opts_from_probe(); opts.fs.symlink = false; let (source_tree, destination, _index, outcome) = - checkout_index_in_tmp_dir(opts.clone(), "make_mixed_without_submodules")?; + checkout_index_in_tmp_dir(opts.clone(), "make_mixed_without_submodules", None)?; assert_equality(&source_tree, &destination, opts.fs.symlink)?; assert!(outcome.collisions.is_empty()); @@ -270,7 +276,7 @@ fn dangling_symlinks_can_be_created() -> crate::Result { } let (_source_tree, destination, _index, outcome) = - checkout_index_in_tmp_dir(opts.clone(), "make_dangling_symlink")?; + checkout_index_in_tmp_dir(opts.clone(), "make_dangling_symlink", None)?; let worktree_files = dir_structure(&destination); let worktree_files_stripped = stripped_prefix(&destination, &worktree_files); @@ -291,7 +297,7 @@ fn allow_or_disallow_symlinks() -> crate::Result { for allowed in &[false, true] { opts.fs.symlink = *allowed; let (source_tree, destination, _index, outcome) = - checkout_index_in_tmp_dir(opts.clone(), "make_mixed_without_submodules")?; + checkout_index_in_tmp_dir(opts.clone(), "make_mixed_without_submodules", None)?; assert_equality(&source_tree, &destination, opts.fs.symlink)?; assert!(outcome.collisions.is_empty()); @@ -307,6 +313,7 @@ fn keep_going_collects_results() { let (_source_tree, destination, _index, outcome) = checkout_index_in_tmp_dir_opts( opts, "make_mixed_without_submodules", + None, |_id| { count .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| { @@ -369,7 +376,7 @@ fn no_case_related_collisions_on_case_sensitive_filesystem() { return; } let (source_tree, destination, index, outcome) = - checkout_index_in_tmp_dir(opts.clone(), "make_ignorecase_collisions").unwrap(); + checkout_index_in_tmp_dir(opts.clone(), "make_ignorecase_collisions", None).unwrap(); assert!(outcome.collisions.is_empty()); let num_files = assert_equality(&source_tree, &destination, opts.fs.symlink).unwrap(); @@ -384,6 +391,48 @@ fn no_case_related_collisions_on_case_sensitive_filesystem() { ); } +#[test] +fn safety_checks_dotdot_trees() { + let mut opts = opts_from_probe(); + let err = + checkout_index_in_tmp_dir(opts.clone(), "make_traverse_trees", Some("traverse_dotdot_trees")).unwrap_err(); + let expected_err_msg = "Input path \"../outside\" contains relative or absolute components"; + assert_eq!(err.source().expect("inner").to_string(), expected_err_msg); + + opts.keep_going = true; + let (_source_tree, _destination, _index, outcome) = + checkout_index_in_tmp_dir(opts, "make_traverse_trees", Some("traverse_dotdot_trees")) + .expect("keep-going checks out as much as possible"); + assert_eq!(outcome.errors.len(), 1, "one path could not be checked out"); + assert_eq!( + outcome.errors[0].error.source().expect("inner").to_string(), + expected_err_msg + ); +} + +#[test] +fn safety_checks_dotgit_trees() { + let opts = opts_from_probe(); + let err = + checkout_index_in_tmp_dir(opts.clone(), "make_traverse_trees", Some("traverse_dotgit_trees")).unwrap_err(); + assert_eq!( + err.source().expect("inner").to_string(), + "The .git name may never be used" + ); +} + +#[test] +fn safety_checks_dotgit_ntfs_stream() { + let opts = opts_from_probe(); + let err = + checkout_index_in_tmp_dir(opts.clone(), "make_traverse_trees", Some("traverse_dotgit_stream")).unwrap_err(); + assert_eq!( + err.source().expect("inner").to_string(), + "The .git name may never be used", + "note how it is still discovered even though the path is `.git::$INDEX_ALLOCATION`" + ); +} + #[test] fn collisions_are_detected_on_a_case_insensitive_filesystem_even_with_delayed_filters() { let mut opts = opts_from_probe(); @@ -394,7 +443,7 @@ fn collisions_are_detected_on_a_case_insensitive_filesystem_even_with_delayed_fi setup_filter_pipeline(opts.filters.options_mut()); opts.filter_process_delay = gix_filter::driver::apply::Delay::Allow; let (source_tree, destination, _index, outcome) = - checkout_index_in_tmp_dir(opts, "make_ignorecase_collisions").unwrap(); + checkout_index_in_tmp_dir(opts, "make_ignorecase_collisions", None).unwrap(); let source_files = dir_structure(&source_tree); assert_eq!( @@ -506,17 +555,26 @@ pub fn dir_structure>(path: P) -> Vec, ) -> crate::Result<(PathBuf, TempDir, gix_index::File, gix_worktree_state::checkout::Outcome)> { - checkout_index_in_tmp_dir_opts(opts, name, |_d| true, |_| Ok(())) + checkout_index_in_tmp_dir_opts(opts, name, subdir_name, |_d| true, |_| Ok(())) } fn checkout_index_in_tmp_dir_opts( opts: gix_worktree_state::checkout::Options, - name: &str, + script_name: &str, + subdir_name: Option<&str>, allow_return_object: impl FnMut(&gix_hash::oid) -> bool + Send + Clone, prep_dest: impl Fn(&Path) -> std::io::Result<()>, ) -> crate::Result<(PathBuf, TempDir, gix_index::File, gix_worktree_state::checkout::Outcome)> { - let source_tree = fixture_path(name); + let source_tree = { + let root = fixture_path(script_name); + if let Some(name) = subdir_name { + root.join(name) + } else { + root + } + }; let git_dir = source_tree.join(".git"); let mut index = gix_index::File::at(git_dir.join("index"), gix_hash::Kind::Sha1, false, Default::default())?; let odb = gix_odb::at(git_dir.join("objects"))?.into_inner().into_arc()?; diff --git a/gix-worktree/tests/fixtures/make_traverse_literal_separators.sh b/gix-worktree/tests/fixtures/make_traverse_literal_separators.sh deleted file mode 100755 index 533b5f27975..00000000000 --- a/gix-worktree/tests/fixtures/make_traverse_literal_separators.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -set -eu -o pipefail - -# Makes a repo carrying a literally named file, which may even contain "/". -# File content is from stdin. Arguments are repo name, file name, and file mode. -function make_repo() ( - local repo="$1" file="$2" mode="$3" - local blob_hash_escaped tree_hash commit_hash branch - - git init -- "$repo" - cd -- "$repo" # Temporary, as the function body is a ( ) subshell. - - blob_hash_escaped="$(git hash-object -w --stdin | sed 's/../\\x&/g')" - - tree_hash="$( - printf "%s %s\\0$blob_hash_escaped" "$mode" "$file" | - git hash-object -t tree -w --stdin --literally - )" - - commit_hash="$(git commit-tree -m 'Initial commit' "$tree_hash")" - - branch="$(git symbolic-ref --short HEAD)" - git branch -f -- "$branch" "$commit_hash" - test -z "${DEBUG_FIXTURE-}" || git show # TODO: How should verbosity be controlled? -) - -make_repo traverse_dotdot_slashes ../outside 100644 \ - <<<'A file outside the working tree, somehow.' - -make_repo traverse_dotgit_slashes .git/hooks/pre-commit 100755 <<'EOF' -#!/bin/sh -printf 'Vulnerable!\n' -date >vulnerable -EOF - -make_repo traverse_dotdot_backslashes '..\outside' 100644 \ - <<<'A file outside the working tree, somehow.' - -make_repo traverse_dotgit_backslashes '.git\hooks\pre-commit' 100755 <<'EOF' -#!/bin/sh -printf 'Vulnerable!\n' -date >vulnerable -EOF From f9616871e83502e720edad621bc6a9cbcfc53de3 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 21 May 2024 10:42:50 +0200 Subject: [PATCH 40/50] fix compile warnings --- gix/src/config/cache/access.rs | 1 + gix/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index c44b12f3e74..5acf240b036 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -271,6 +271,7 @@ impl Cache { }) } + #[cfg(feature = "worktree-mutation")] fn protect_options(&self) -> Result { const IS_WINDOWS: bool = cfg!(windows); const IS_MACOS: bool = cfg!(target_os = "macos"); diff --git a/gix/src/lib.rs b/gix/src/lib.rs index 62b55ee47bc..f193ecd74aa 100644 --- a/gix/src/lib.rs +++ b/gix/src/lib.rs @@ -346,7 +346,7 @@ pub mod discover; pub mod env; -#[cfg(feature = "index")] +#[cfg(feature = "attributes")] fn is_dir_to_mode(is_dir: bool) -> gix_index::entry::Mode { if is_dir { gix_index::entry::Mode::DIR From 268323587faeada1abdd7f933d616af3165a37cf Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 21 May 2024 10:55:05 +0200 Subject: [PATCH 41/50] fix: assure high-speed SHA1 assembly is only used in not on Windows (#917) --- gix-features/Cargo.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gix-features/Cargo.toml b/gix-features/Cargo.toml index 3fc3b20df72..7302c9fbef1 100644 --- a/gix-features/Cargo.toml +++ b/gix-features/Cargo.toml @@ -25,7 +25,7 @@ progress-unit-human-numbers = ["prodash?/unit-human"] progress-unit-bytes = ["dep:bytesize", "prodash?/unit-bytes"] ## If set, walkdir iterators will be multi-threaded. -fs-walkdir-parallel = [ "dep:jwalk", "dep:gix-utils" ] +fs-walkdir-parallel = ["dep:jwalk", "dep:gix-utils"] ## Provide utilities suitable for working with the `std::fs::read_dir()`. fs-read-dir = ["dep:gix-utils"] @@ -34,10 +34,10 @@ fs-read-dir = ["dep:gix-utils"] ## ## Note that this may have overhead as well, thus instrumentations should be used stategically, only providing coarse tracing by default and adding details ## only where needed while marking them with the appropriate level. -tracing = [ "gix-trace/tracing" ] +tracing = ["gix-trace/tracing"] ## If enabled, detailed tracing is also emitted, which can greatly increase insights but at a cost. -tracing-detail = [ "gix-trace/tracing-detail" ] +tracing-detail = ["gix-trace/tracing-detail"] ## Use scoped threads and channels to parallelize common workloads on multiple objects. If enabled, it is used everywhere ## where it makes sense. @@ -45,7 +45,7 @@ tracing-detail = [ "gix-trace/tracing-detail" ] ## The `threading` module will contain thread-safe primitives for shared ownership and mutation, otherwise these will be their single threaded counterparts. ## This way, single-threaded applications don't have to pay for threaded primitives. parallel = ["dep:crossbeam-channel", - "dep:parking_lot"] + "dep:parking_lot"] ## If enabled, OnceCell will be made available for interior mutability either in sync or unsync forms. once_cell = ["dep:once_cell"] ## Makes facilities of the `walkdir` crate partially available. @@ -159,7 +159,7 @@ bstr = { version = "1.3.0", default-features = false } # Assembly doesn't yet compile on MSVC on windows, but does on GNU, see https://github.com/RustCrypto/asm-hashes/issues/17 # At this time, only aarch64, x86 and x86_64 are supported. -[target.'cfg(all(any(target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64"), not(target_env = "msvc")))'.dependencies] +[target.'cfg(all(any(target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64"), not(target_os = "windows")))'.dependencies] sha1 = { version = "0.10.0", optional = true, features = ["asm"] } [package.metadata.docs.rs] From 2ea87f0060fd796961a2173569f16f362ed61617 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 21 May 2024 10:37:07 +0200 Subject: [PATCH 42/50] fix!: `State::from_tree()` now performs name validation. Previously, malicious trees could be used to create a index with invalid names, which is one step closer to actually abusing it. --- Cargo.lock | 4 +- gix-index/Cargo.toml | 1 + gix-index/src/init.rs | 87 +++++++++++++++--- gix-index/src/lib.rs | 8 +- gix-index/tests/Cargo.toml | 7 +- .../make_traverse_literal_separators.tar.xz | Bin 0 -> 11696 bytes .../fixtures/generated-archives/v2.tar.xz | Bin 9812 -> 9872 bytes .../v2_all_file_kinds.tar.xz | Bin 11040 -> 11088 bytes .../generated-archives/v2_more_files.tar.xz | Bin 10036 -> 10096 bytes .../v2_sparse_index_no_dirs.tar.xz | Bin 9980 -> 9992 bytes .../v3_sparse_index_non_cone.tar.xz | Bin 10584 -> 10608 bytes .../v4_more_files_IEOT.tar.xz | Bin 10356 -> 10404 bytes gix-index/tests/fixtures/make_index/v2.sh | 2 + .../fixtures/make_index/v2_all_file_kinds.sh | 2 + .../fixtures/make_index/v2_more_files.sh | 2 + .../make_index/v2_sparse_index_no_dirs.sh | 2 +- .../make_index/v3_sparse_index_non_cone.sh | 2 +- .../fixtures/make_index/v4_more_files_IEOT.sh | 2 + .../make_traverse_literal_separators.sh | 44 +++++++++ gix-index/tests/index/file/read.rs | 2 +- gix-index/tests/index/file/write.rs | 2 +- gix-index/tests/index/init.rs | 46 +++++++-- 22 files changed, 184 insertions(+), 29 deletions(-) create mode 100644 gix-index/tests/fixtures/generated-archives/make_traverse_literal_separators.tar.xz create mode 100644 gix-index/tests/fixtures/make_traverse_literal_separators.sh diff --git a/Cargo.lock b/Cargo.lock index 374ce53324c..09058e67861 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1963,6 +1963,7 @@ dependencies = [ "gix-object 0.42.1", "gix-traverse 0.39.0", "gix-utils 0.1.12", + "gix-validate 0.8.4", "hashbrown 0.14.3", "itoa", "libc", @@ -1979,10 +1980,11 @@ version = "0.0.0" dependencies = [ "bstr", "filetime", - "gix", "gix-features 0.38.1", "gix-hash 0.14.2", "gix-index 0.32.1", + "gix-object 0.42.1", + "gix-odb", "gix-testtools", ] diff --git a/gix-index/Cargo.toml b/gix-index/Cargo.toml index 9c0dce406e9..201e996b0f1 100644 --- a/gix-index/Cargo.toml +++ b/gix-index/Cargo.toml @@ -27,6 +27,7 @@ gix-features = { version = "^0.38.1", path = "../gix-features", features = [ gix-hash = { version = "^0.14.2", path = "../gix-hash" } gix-bitmap = { version = "^0.2.11", path = "../gix-bitmap" } gix-object = { version = "^0.42.1", path = "../gix-object" } +gix-validate = { version = "^0.8.4", path = "../gix-validate" } gix-traverse = { version = "^0.39.0", path = "../gix-traverse" } gix-lock = { version = "^13.0.0", path = "../gix-lock" } gix-fs = { version = "^0.10.2", path = "../gix-fs" } diff --git a/gix-index/src/init.rs b/gix-index/src/init.rs index ecb1f0b13a8..1301df77e9e 100644 --- a/gix-index/src/init.rs +++ b/gix-index/src/init.rs @@ -1,4 +1,6 @@ -mod from_tree { +#[allow(clippy::empty_docs)] +/// +pub mod from_tree { use std::collections::VecDeque; use bstr::{BStr, BString, ByteSlice, ByteVec}; @@ -10,6 +12,19 @@ mod from_tree { Entry, PathStorage, State, Version, }; + /// The error returned by [State::from_tree()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("The path \"{path}\" is invalid")] + InvalidComponent { + path: BString, + source: gix_validate::path::component::Error, + }, + #[error(transparent)] + Traversal(#[from] gix_traverse::tree::breadthfirst::Error), + } + /// Initialization impl State { /// Return a new and empty in-memory index assuming the given `object_hash`. @@ -32,23 +47,42 @@ mod from_tree { } /// Create an index [`State`] by traversing `tree` recursively, accessing sub-trees /// with `objects`. + /// `validate` is used to determine which validations to perform on every path component we see. /// /// **No extension data is currently produced**. - pub fn from_tree(tree: &gix_hash::oid, objects: Find) -> Result + pub fn from_tree( + tree: &gix_hash::oid, + objects: Find, + validate: gix_validate::path::component::Options, + ) -> Result where Find: gix_object::Find, { let _span = gix_features::trace::coarse!("gix_index::State::from_tree()"); let mut buf = Vec::new(); - let root = objects.find_tree_iter(tree, &mut buf)?; - let mut delegate = CollectEntries::new(); - breadthfirst(root, breadthfirst::State::default(), &objects, &mut delegate)?; + let root = objects + .find_tree_iter(tree, &mut buf) + .map_err(breadthfirst::Error::from)?; + let mut delegate = CollectEntries::new(validate); + match breadthfirst(root, breadthfirst::State::default(), &objects, &mut delegate) { + Ok(()) => {} + Err(gix_traverse::tree::breadthfirst::Error::Cancelled) => { + let (path, err) = delegate + .invalid_path + .take() + .expect("cancellation only happens on validation error"); + return Err(Error::InvalidComponent { path, source: err }); + } + Err(err) => return Err(err.into()), + } let CollectEntries { mut entries, path_backing, path: _, path_deque: _, + validate: _, + invalid_path: _, } = delegate; entries.sort_by(|a, b| Entry::cmp_filepaths(a.path_in(&path_backing), b.path_in(&path_backing))); @@ -76,15 +110,19 @@ mod from_tree { path_backing: PathStorage, path: BString, path_deque: VecDeque, + validate: gix_validate::path::component::Options, + invalid_path: Option<(BString, gix_validate::path::component::Error)>, } impl CollectEntries { - pub fn new() -> CollectEntries { + pub fn new(validate: gix_validate::path::component::Options) -> CollectEntries { CollectEntries { entries: Vec::new(), path_backing: Vec::new(), path: BString::default(), path_deque: VecDeque::new(), + validate, + invalid_path: None, } } @@ -93,6 +131,11 @@ mod from_tree { self.path.push(b'/'); } self.path.push_str(name); + if self.invalid_path.is_none() { + if let Err(err) = gix_validate::path::component(name, None, self.validate) { + self.invalid_path = Some((self.path.clone(), err)) + } + } } pub fn add_entry(&mut self, entry: &tree::EntryRef<'_>) { @@ -103,6 +146,18 @@ mod from_tree { EntryKind::Link => Mode::SYMLINK, EntryKind::Commit => Mode::COMMIT, }; + // There are leaf-names that require special validation, specific to their mode. + // Double-validate just for this case, as the previous validation didn't know the mode yet. + if self.invalid_path.is_none() { + let start = self.path.rfind_byte(b'/').map(|pos| pos + 1).unwrap_or_default(); + if let Err(err) = gix_validate::path::component( + self.path[start..].as_ref(), + (entry.mode.kind() == EntryKind::Link).then_some(gix_validate::path::component::Mode::Symlink), + self.validate, + ) { + self.invalid_path = Some((self.path.clone(), err)); + } + } let path_start = self.path_backing.len(); self.path_backing.extend_from_slice(&self.path); @@ -117,6 +172,14 @@ mod from_tree { self.entries.push(new_entry); } + + fn determine_action(&self) -> Action { + if self.invalid_path.is_none() { + Action::Continue + } else { + Action::Cancel + } + } } impl Visit for CollectEntries { @@ -127,12 +190,12 @@ mod from_tree { .expect("every call is matched with push_tracked_path_component"); } - fn push_back_tracked_path_component(&mut self, component: &bstr::BStr) { + fn push_back_tracked_path_component(&mut self, component: &BStr) { self.push_element(component); self.path_deque.push_back(self.path.clone()); } - fn push_path_component(&mut self, component: &bstr::BStr) { + fn push_path_component(&mut self, component: &BStr) { self.push_element(component); } @@ -144,13 +207,13 @@ mod from_tree { } } - fn visit_tree(&mut self, _entry: &gix_object::tree::EntryRef<'_>) -> gix_traverse::tree::visit::Action { - Action::Continue + fn visit_tree(&mut self, _entry: &gix_object::tree::EntryRef<'_>) -> Action { + self.determine_action() } - fn visit_nontree(&mut self, entry: &gix_object::tree::EntryRef<'_>) -> gix_traverse::tree::visit::Action { + fn visit_nontree(&mut self, entry: &gix_object::tree::EntryRef<'_>) -> Action { self.add_entry(entry); - Action::Continue + self.determine_action() } } } diff --git a/gix-index/src/lib.rs b/gix-index/src/lib.rs index beaa36f5344..1e157bdc0d5 100644 --- a/gix-index/src/lib.rs +++ b/gix-index/src/lib.rs @@ -26,7 +26,9 @@ pub mod entry; mod access; -mod init; +/// +#[allow(clippy::empty_docs)] +pub mod init; /// #[allow(clippy::empty_docs)] @@ -116,8 +118,8 @@ pub struct AccelerateLookup<'a> { /// /// # A note on safety /// -/// An index (i.e. [`State`]) created [from a tree](State::from_tree()) is not guaranteed to have valid entry paths as those -/// depend on the names contained in trees entirely, without applying any level of validation. +/// An index (i.e. [`State`]) created by hand is not guaranteed to have valid entry paths as they are entirely controlled +/// by the caller, without applying any level of validation. /// /// This means that before using these paths to recreate files on disk, *they must be validated*. /// diff --git a/gix-index/tests/Cargo.toml b/gix-index/tests/Cargo.toml index 4d15e5aed4f..8e5700826f5 100644 --- a/gix-index/tests/Cargo.toml +++ b/gix-index/tests/Cargo.toml @@ -19,8 +19,9 @@ gix-features-parallel = ["gix-features/parallel"] [dev-dependencies] gix-index = { path = ".." } gix-features = { path = "../../gix-features", features = ["rustsha1", "progress"] } -gix-testtools = { path = "../../tests/tools"} -gix = { path = "../../gix", default-features = false, features = ["index"] } -gix-hash = { path = "../../gix-hash"} +gix-testtools = { path = "../../tests/tools" } +gix-odb = { path = "../../gix-odb" } +gix-object = { path = "../../gix-object" } +gix-hash = { path = "../../gix-hash" } filetime = "0.2.15" bstr = { version = "1.3.0", default-features = false } diff --git a/gix-index/tests/fixtures/generated-archives/make_traverse_literal_separators.tar.xz b/gix-index/tests/fixtures/generated-archives/make_traverse_literal_separators.tar.xz new file mode 100644 index 0000000000000000000000000000000000000000..3e93c175191c1b5102fc4018a3726508ad7f367d GIT binary patch literal 11696 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 G1X)`Ak-~2P literal 0 HcmV?d00001 diff --git a/gix-index/tests/fixtures/generated-archives/v2.tar.xz b/gix-index/tests/fixtures/generated-archives/v2.tar.xz index 42e49dc2c72ea8177c5f227313690eca0ccbe75e..596f9af672cce2806f3b08c544183b3623616671 100644 GIT binary patch literal 9872 zcmV;BCU4pOH+ooF000E$*0e?f03iVs00030=j;jK<^Lv5T>uvgyc~T2mB1Z8f})DV zo{cYQ-SvMkK=)Q#6n3S0F?tQM*-3aSwTv`B)gYWCI?smUIEj`XRSOtZ!iQhW;vlsL z^#Hdr%Mv&rSTLw-PKrAonB5~eaw8OtN=^MiZS|Xv(LfK)|72!@me3$~k#hhIe}CzH z`db%yq#u}7W!D|06sgJ_()5YXQndB<#NjQ-PY|#Cr zqGY1xY%Feu3Qti0yogfg5RMmIjxgbbimw;rAyRyOGwI}2P{%7RqaHDe(_yW%cspog z9}Qf?)fyh;KF`UiVkfudZp|L34FeMx05{BHulsdq&-D4l~FGY`&g=F(BXWzb*h z49lIL6ybUxU;|kk3u0r*A5OT`{lsr#Df(4SyB8e{zj3^u2s4BYsWF}35uwA4Ot34f zrEXV<5V}cHC7_rdhuC-}5FMRE*-@`ScUYHH!3g}c`Ic@o#LwB8aa)DJ_k5czd{DcL z-Q$jU7c^7(5Wc`ysg!LSItb}udPem|cXZChYLJt|PRpzHuC>TeZ1O}!j$nq@(BL8M z%}bl%j_}EPP6Qji6Pt&-sfGL7YsUYsx?X`ub>=ZYjseebCUH2YCu@0&iuK70`1FoWn_VM z2aD0X5$v1GAgQcIVtXh__uH3Mypig_CKZXDpnT&%8fH-$n&NG^s#K5TULBiw-c^+Q zS+NiAR!<**Ay@W@D9sHymXMq*!yJg#In{a$@WIIIaCMPqgJ18~h zNY$B;;lVz=Y*^b7yH@o4#MCOiWyw-K`FigEh(?x&tl3NbS9$dXz>Zr{(5oY;VjpHR z=mt#)6KMGL_=qY6svmo<7@P{lHvo`ldenFk={qewr=o+9_O7?-X|r1el0}$=IGfjZ zQ0D^2K3mDg=d<$ic}uf$`*+03HPY30%5Xfm7M31jShnLuqc!tfzkTvZ-WLq^YUt+& zwQbxlU$#R!1@Q-sAikN_Kct^U0XCZ$YY0GyDPOopd{DQdS1 z=T|76-!|0h&r0w()sp_4$^C`4ZGV~DHz0eHsZ~*)03G1A0ittVENA~WbUu{nvK_R1 z$uE3bi(I)83z*+kJD^@chPho+3gF@{`Itl%2nS_mhMEl`G}zOk(N!}hzk^)%m1es8 zw1zWy>pNfwFhrxjlvko{r()Jk6mBWKB8^iXA2-B`-3Uk2zBz2)UVg2GG=VnstqJhS zP*cqT?1lhS^4U+DVs@H|yKvlG%2nLcn^A#QUz;lTkna&3pq1!-&Qq&*2W`?MySdxl z36Ys~rfi{RqHI`Shf+m`YLsw?oYTgVmT|B|>S~Fi_g3cKZqR%Z&ThxpM?yZ~yj9*L z_}x0aFDC2BLB#QX!{IsTMzE6~HxuW9EuF?e{N#ny*DHfZ>gsG}wEYl5K`TsN#ib}4wr(cr{&!Hj&=&XW?MlgEuZeObB-6i?E4l-JJ5UGBu)#--kc5n6+ zoiH&u>;lPW?FhxS91y#l`-LA%v_Dm?8>@Uxa0W&7=X9zK2%{>aWJg1U*Tz@9l5GmS zqk?76%y)CjX2*IIihwFK6?coeGtqIFE3Y`s-E|^c!%uw8eZhg_3lwW|=M81#^8)h1 zY}i`34*(7E#%`yYS`XU7GO8=3Q)EDWKWc!Lmpn6)UW@7fojj5P#(rn|*p{FFLeqm^ z6GZ(sTMBO00aeAsAcJf=Ym?AYPeol_>zhBNZ?-JUg^@I`)ursuT(OE3&h_LZTJ*d$ zP5Xi(wb)p(-ncQ{3NNxPcHaK7UC(%bvA5-~Uzip4ZwHgGT~y3Amslp^{C5-#xiyoML{-BetBZ-cY{J(i-${>)=RlDx|@x=MF zKD&m(fK<%*@h0+bmoUNRY|};)Yra99d8;zl3eG;t4gm3#?BK{#oqu`K! zIS-93eZ>6bX3*1fvpsr6M7MBj-MILu76>Nf6=QI@bUnYhXF02XH_f{Np`*8+ zzKBkXO1OOtbVYgSIj%D_0S+Jr?~e=wV76P(2p_A34mIKycZb^Fpb~v$f~GgAni5FX2@@JGeQNr=~ zqUXsXZhAm~dT0q*D2d~Ix-8=P^fml|GDpzbAOV?&v?>)lF0hFWY3&LAbjEh9|2m z?@D8O0py1ejYgAN8_=u_A~Y2iTNzY0eyNPAE9b9qnt(Cl^n;CIs<;&3Tcqo`YJ`RX zXQhNQ=}fBEZv)1}e%}+O=kN;|kIa%aw}so_EuXKY{)~#;qzlYRNZbskHPc2;l%mCZ8zN)Ho2I5 zw!2f`-6N@?rQ#=xf1)UMQ;VQIWiblYr4ng^LWzDfNqcuqqwp+OhU?zF{Jvu@yro#L zES_*NRMAaaDe$MytVWd=Eywhca8vr3msc|#n^-2(1yfNvj(QM-9(w@WEFY1xUx>sqKeJeg%SG=co$|X(T%B^-xv^kxCPuCFi1F56KGuy z9k}cmD>V$*;L^3n!lHoO9y|cF=l#dC#O51-InPC(1+VRoq%le%%|TDYW~&zUM{(*o zLDn)cbF4PPL8S zy`>{T@Np28+=a`acl?vxDXXqwb@1EE3|`y2dqh0#p8H+o0_NB3Q&OY*AMG0#ZfJE} zEEE~wTT`mmDcYg~0OrnDFV=-l5jyLp)K~K3)vGmyl zC;T~HB&Q2wuB3qu1uQei?3Z1qr*8>N@+|?fGtr;9zMp}cUV|JeOSU=tNJu$K(94#n zf=~T&j70T&FwPn#*$;_Ra_ok1SeqzTU0~KJFct$KzEsU94Om0}QZNLJsz+|+-^UBY zwr!f_%HTps!PcbpK8D()MdYYR;JIT6XL~`)wK?QPQ@b2)PgbksZE7`g6eI(?$5Kpk z!XGpbd$>l`2fIh>Wx@=2I}_?~IBajlW`TByHIZyG9GXDLFm)5hrtKF?nAQ+F3kC&9 zmxDWZ9R7SC;)$~HZZt5E5Pz%hJO^<$tSXl1fng!S7)LIGk!oWdX{RL>oxtYEuwaCS z#zE?>4DsUsMShnaL^oR`|N4{IOeQGG2@^Ny-2g%t);HV7+n!$`0P1)obt>b3OzD0_6MskdDFV##G6E@oXB3FbecYQZiq~rKYaTBBoOe$);`QFv^VdJJXb`E#I1dVGdYE+m9S^tKV{;^DzKVqQH2O zV~|wbF4>zcb&NDei&jhis4CevXAJA)Lbl~Q?C)4x1QB5QLv}2v&O}Mp(rT&r3Qt~9 zkhBHqY($8;&?(OCm?}xFJU_-qj+T&{(DXiE1(sc@BY+wJQfN3sj4LuDUFWJr#h?S3 zT(G!>_rD`D%`!Sm(Y-VIJwH;3(Pz=smRihNK~Gj<5N4W}O*w8(CjtBEnb2|MEC2+d zle@|NXOsjo1h}_WE=Elq{eA%2Sy;ndgryB3rDkH__@2LIstS=5uU=hgAXM^u5#QZq zZakHik9OBDs@%Rimoa9GiIs&$qdYTKn38%27)$NZv)ni_bKMFto#>B%+&-TI%>87+ z<{xUPnXt5(VPu~BQSUvQSS`}UrDbAG(<|wBqYqd)VV9Zc(Dj^3VLOa{$Mw*K!%{rZ z>VDU&@ZCcx^}SLc3NF59(>^84)Y~{z-Lr06RJ-$!kre?VXeV2UV`fuBR}>x@e5MFP z7BT_SV*U->2#PoZp4rf@opxrmnrA?wepMOfO95TRl#-t0mv$=!P($%zOU$UzGd@=Q z?EFgA<6)!G6%t&1wE?9Lnvegi#<;cP3)0jDPw+{M&zP@}5PJ)!W>#kLj)=Xx>C7(I zoE)EwJnuW39+$$|AP>0yTZ^>>Gq(*YBl57_S#zdiF)}F_zLa;tf8bXOM|GOvPEu?p zPl}Fr(kNY?<`OaqJ9Kj&mQTqtkNZ*00qZ+EVd2sJiGYWU=3DJ%Pmcce!?za8CR zG8Vp+6Lx!b>JnV*TTz26dEq&#&{lx(>ss|0!s@aD&NF)m6aSs>wCcF^5$Xd)pCo^f zEDy&ThFEW@N8MlS1iWXq^}~b?o1#;=SSvG3LBKBaG(J4D)&3ev!1oAT3(PH=v8#ad zP#aXA=Af-uTkMfFQ+=m`_RJ2B*JJml9uw{5Gr1XW-RRhckIq7X$=WPFs4V^@XG!qc zM{r(FPA=5nlfiszYGKtig9KZ3#IK|#gVFKM+wS3RX_(js(c#gP`;AZmaqkPQ!5;Q= zqcFn(e!b$7XdLieD>d6mez}nz3hBfExF%)Ks_@c9^F;km1vU9&Y3aY;JCkLmi z(Ly$Arg%7fhk*b^;q~3)r;&&+BEq0q{QX6hnv}abp1xB(Lb%1@E+0&!O}YIuM}na$ zIaE_EJ>Ra>ffVmo0vuuDRg0O4E+~-iB)`)mWv?o`-;%e=15j&{J@BrxkasHNoNW7- zMg)=~JzFNz>AsNfT@SZY#Vau>bVr4{to8bW`Zf3qGQlE1DtOrxWAEztmM_UXsJ4KJ zx347D?Ykf_hbNh#D6F6(Nm4Eya|IFZn0r+QEi|ZAKra((aEezmZ#)@)F=8QgL)=t}fU5;%~A?)LmXJQSvq}BHRIkZj0*y9e?$et1?#}T`xGe1sm zc6W6PUu(1X!%J>Uq#+s2d9@>t0y5(EsBz#oUf*p&GEggHt=qgN4iUW5nGsn6E zBJUh$g&2ZI+Gi?LU@7z3uE6_2@SaG??Y?EpRLryjGDB!Ba4D9~J_nh9-^=rlfBhOm z&1m?Vu!aM=sj(FmKLRuf8@nLmjAHm=8}%u*P+6N$9<~ZZEK54pMlS^_wA{&|w-&E^ zps^v9eR)B`MPbT1y~G?@yFPSs7P#RfObv$Y2zI5yREnLrJp-`sy&pfKw^bMwAR$Cf z+SUQXRxnfPy0k#kG>2E|alI<-qjC9pA9&@;TG>*}b2`~S)tg|9C`$LK?bK9n`vPSM z-F3H9kN7%2N%Pd+$hKUbAUE8%9~Hji3dgo0&3ES*e0Ub}049&DMqPJ_%ZA#- zI=P-FS$vcIM~vu9jt6*Od*Lh^Qmi6en^>>9sT!XOZ)R04;u7mYef^P)J~rf=B>8t< ze&Z?*Ko#BRLd>$XLlQn8iC6DE2R%~g&_hR|6^}k;+}9yddf9xj61a>dePtGSXC!kk z-ivIZ?fl0C@xe=%_k7bYECf&R19UNzb@1a9_CA-1&opuG%dvXLYt>w7bER;Y$+Xw;yw{GgFTR|C^;u$k<-%t`E zeEpzDoacPBjkK*6*h){5*dFvsrL`Q$HnJ)o^HJK~yP337rKi%rZ)S9bXN#weWw>Lj@3JA)7ZY2IRdIv5j zHl5<}+`>{&&>E{=0Yy{>fvH)vKr>H~kr^`3P}aFzk6!b$`&3ayRCJN53(ap<%#1N%6JoZHIkjr3X!auCRPn3?<%B2XA z_!b((vM5LObNWCzU2dM#B(orFKB8@g%#3IXKVBllg_9g`jPhSlUk796@tz0CS-c_i z6BA@7#n|dqp)n9NhkaH+d&$03J&~DaCeg%$N9tE`)zOE8ohn*(mBJP1gVuMZ1iy(6 zG&rgbAoNf%TcV0lQf=H(>#*?)`LuzWyv%SaAX$6t@owF4Q;N@>J|~axA)Gjgfz`V% zN`9*@VlZIq+Yh?bNS22Hbq@a3s=mOL9VV>sRRJ%?6KhuK0v4Ul2L);qN-&_?Sp8b3 z>3{yuS>6g6FP87C+pJn_dI1<02%$mL1!xqQ;eIs;!6DDsMdC72fj*pYB!>J^_5>|Ybu1<}|rA`k5<@B967kovQ?eS8P3 zdP=EiQC@x@wp4h50zC_o`lpVd}QdsyF!|KZg%?o}&vk1j52W*Oa=ZUPwnMga6$|@~8XJ z>b@Zmg2zl$ew>VLVR{MqCa+!i>Sl)F*bF8n>9yb0@FkFrUwz+=f26cK6ftWXRZv+C z;F8^4o1n8G4j^7jeRRM;k!iOfh(Y5Q$+0{N-B%;$>FRgbxei9 zeJ%}}#CP=dcoBon`mqXP{B8;xP~Gxj@Wq3e$xZ=5HZC0t=pEo3Mqj9SaA3b`FJyDu z!g-w|;dxbsA?&8o)s0f%wpM)&L0+^kdfKDvZ)g@oQ2f;QNxCf?LvhG)+(9HqsVmR@ zc8-&j$kyWXikenpGV6P#FeXDG%1~gMwR;nbWXCJPzToC8^YOX~ZNS57oGT1{In*wq z%wgfaFY&g8pf41$nwT!ZXvaqwCLKz)@U|Rqh7%iQ6j)|9} zq;{IT{>*fp8>u4YUWNu`uBR*Hg$A?`=KmvYWkYP&e|-kjFJ$&~c;v>pC3t|Xu^JLF zJT}`S99g_r{2B$q1XPI&RCaG7y(Z(@j?>vNA0SM)sY`tLEkWN;OM3131toyHDc=Gq zt%gX~4D*Hm7mtuLt{YTj3G|Uy6OV|TmHQqK1{r>I%kcGk;gKOrv;a3!uVhm%3Th8%K@p6a|E5;Jym`m zBDo6QJ)|^xgqLz0{!da(Z@yNTVLj@Lx!?zNBMauE{M_sz@IyTKRt&bFgpV=bdx)k5 zF0nV3x}zVN-X6mC)a6=rSsRR)dVceO!%?90cVO0&rk*PFV*8M2iI~$0)?Gp^jID_p zTys$e>jVUA50y}~u+F>vAN&WH@L8N}4%x5`Qs6BxG{Z!1M}1KH0OgE7PRb>onE{US zMLuWd_*F#y+??0UQH!1-9^+SMo@zb&t}al*fOm=^a(>9XWr*o3tcDXy1wR2#cxv%e z^zS+qAW@>5OPe6u39g-};hg_gS^vk8$764%Qg?BeRkyMGSfi<8-YxV7?2pI8nPO&iCqyPejkKWGoxF|+Xqm;ihqabT*9Mi6l~cDlRLB*Uuy8T%C5J&ua} z`$kVYvZUv6u!Cj}aVV&bO)V!;vmLRSD@mzr=QH5?g^vIhQTT)r#C~duCse|9L%^eY zs{B}ZGdX>s^w6Or`Z)eqkKRWrC@3Uv>5HI$=sqWqAE-zG&MQvNYnNA6U3*})Pwk(=Gxb?EO#BxP8o(9!Ul#+uu^3UPy z!j|l~Gw+{`}u|?qUcOj+sR3!y{Em8{#ue>#R-6#5lH_HT-qNr6Ne&{7Hfbx zX&cvpmSI2ldoEVd0UN_}e_8q%iCEP-8RvLpi))9icX@xpy_h;G;hI}uT^ivB1fi?5 zN}R&VnnXwS^JD7=H62wPZTLVL`cpEJeIlI`_Sz|VK>>pmiNFns4sofNey!nM<$q** z!L#Gg`9~To%(Xy~F^CRXs@0_aFyguebcw>O6GF6TO^JDa17Z%bFEqCVuB0R6Rd>62 z`}+FxpE0-i7;eMqRCdHL^zx^2X5JFh`WT}XQ&_-&VpnGbg=vpSW>B&JqMcuCUvI+? z;gc^5|If!-O$BSJ#A8z3JQ>ebkZ&|Efmu)r?#fvtAXri4RSnZ|D^TCL3l;9P zvPBA*!Ld!G&R#Je!$TQ|vtAR_j@J(dqTcS}k>M@WLVSY-(`|LI9@u|cH}<}l*UAHT6#DG<~z zzKRebu{=&uB<>pOCu`6Ddb*8?R}c}J_eaP{n$uv1@{n|ybQeKEQ_>Iw$3$S^fGll- z^9e_6B$fFYY*i*HE(HWj$q>EZmnp>z&Z#6?LNe|?0p-wprLVEdH6-9y1P`qxy737X zPXffsBF>+PO)Z7_1DlV6gvw5qLb^fOj&yoH?yQJ3@X~-~)&E3=Q-?p#7utyc&rl`( z+b81GwTY%%ve$U!1N2g_I=`2i71OC9pj4k&0e2qd2Ue#zwAP#Ze{HmCZ=d{6xwA;{ zI~qooaT8EbjD|mcy%zKfLQeNKJ(D#f4XLUNUG&4eCI>{=sV*V#v_))&fnZZiV$m;s zfrJS1=0veLs>kFj`dhKE$!=Hi03=L=S2{87GieMLzO#TyssDpx zKWAfEm80VUwFr3An;v8=`#wsw^CVbL+ngW#3r7iHi*m0x!NKvhTmFIV@3ueP#|K#W z#LfS>587sudS>y75nIlp;?{CaqbN8+L0hDfzlj$+A)DAr^=w6^Xhsf{{GDFQGX2&x zWQYX6=m*Q$d( zTpKY2BEtKKO67DP&AOaG*|`OFDN=dqRv8N_RG$RJu~=3(k3xBkO7Jdb-qx9g{i7Z9bhItk$vom)t@B6^*=UHNy)yYf7%RkJ4u7hr=PCm?ilgj2e6Z-gMC+pJ7U z#jb$QV7$RuDqDuj=3TtWAcx4M_Ut4iee$MHiMFKrM-F1&I57zFY{>NMSBd7)dpOi{ zJipK6uL%T(fjWM@DhTNXc<3E9^Fe|Amm{}9mU!A6C>s{?eoeQNI0HP}*L#oa8_l@Q z@)8tmCVqat*yCqRJ3SmZbW0qRVE%mVuvgyc~T2mB1Z8f})DV zo{cYQ-SvMkK=)Q#6n3S0F?tQM*-3aSwTv`B)gYWCI?smUIEj`S;iyZc>X_c2B)V7) z&kD&~xgi(3Xr~LQN^y{gH^%PvA4DHD*!=OXE=z#7Gea$?EOtTU*Rh>O)DbADzBX92 z_e*w|BxEprN^Q7xS_tzdGj;!Yb?iMRPDe2tU;u)b{B-}t4^{@#B&sGM1srveDW>@T z#u6X)k1|%mU&gz3qot&XL|33tz#0N$;YyhnUxE1*X?JEs+gn zu{b>6HlIBC#~bl`uoa3Q%5mN|4f~M8kU)v| zF^xX{$Gf-MlXCrM-S8uGG(NRc7=SO51(~Z=*0?Wai1EQRQZKLH&OZ!16|bxrvkq<7 z0e7-igTg{Urb=WX0GjX$fTu0T_w4aIo+WfL^!JYMzZP^q>5kI(Bb;p_WPe4{@+YxWTY$4VP|E68 z80pEJ8OM9@zS78iHLmx%#$gZ%dBBTeWpmGacDxzbrSzy%XpeyGQQ4cs<$?zKy$cr? zJfZsg>GP62ZHaf-0P3F>HP)Pf$WT_7)n>Iyx~(*p-B(5_OX)#E6cKaoQd6*?r8Owe zbyL*f96StV>67tTIh zi*rvy5P&w2$-~kU0Ss6tWaeF!ryI&J3GwIl8NT1}y~MoX&3n6!`lUrmg;B4Eq~v=M ze(vuG^+2pH$dXYiRr(4{he7Ko16&;lD4J6_!}(pbN>FnCoo(KUK0kU(KESE3Zh?DA zn_S&&7f<6yicSe)^nx6Y+DcE)3=hslg4=icX0!sz*quJn<_^)(tyOiZ^pV;f#0bSb z4Wv#=u-?12%}o4VqJ5ScX{`NQ9cH?lDXsPW_j+heV!eMB$UbnDXVW>ldcox(exW8S zdW*L)N$S}FQ#hJ2MJ^S{ z@`CVjv?Ae0mY^!whUoqPSREHY+4Fr|^|}#~gTnYhcU1!8v_lU_Jy3j7g_gY8@`Hkp zq0duR`Vl7dtyDHOnfVutV|p)-)Za;6gFT;PKb++xMoh_@;|}Yel1ET6ybF7^lSy0U z3x6~}8sQ>hQdv5H+7pUgVo1Bnr#G@97-{vsPPDy;ykH@X`VLuqVV)R<<=dU+r+bNk_(&x1uS zncc)IWf9Fv(L#ZD|J<{~3S{r$A>S(y$<>r+d0N;_$)b6CL;VDgSiUNzaIqfs$AW=M zH)=k%lv!2%&p;*GVfU23+@bSXPSwE+0l|k7OtWe$+SD93zV+IrETm_aew@5h{ zy&TcNcO%c&U^w(^wE3+q^Ed1zE}&_=KvA#sEfAI@PF`tb*eE>$8YvG zgT%~U|NJ~~P=%wIv@QyX?o%`(D=D>H;EhiCAZk?}ln&?3mz{ZGQ9=wRmJH)a$$;}_ z#8bnGL#Ntv>pl>;F7|OI#_#QummaQovdyg@l9FrTimNu3G5gE064?Dsho;5;V=Fn? z_yabq>%o4^-JWcnWiAIqJLYEdo%AEd5J_l<{IjIJA6%IA#Yjv$XAqb6ag0TukxN4s z4~H8}uDd2|%V#8Ox*V<%?q+2`FIA#zI46?0zdpez=$M!Jc048}F+FYP2UPjVb|nlj z-!q=UwEup7`4k^JD!LBCYq~^}8e@icazvhObrITRNJc;LqzJ zXtkt6+K|D+P{TH{@KHbF{F#0BC?V6x_R31gsI-wKV&}!UUI*%;S)9ml$zo&&gdTWNk@6OOgn863zxR%oOKS>CETX;_z5{rL&&!YIZ8aOSvTM!(}cW ziXw{0^C`hYI1eP3bJVw-2%Ulq)hlh7a0*nt5=?J22`$`|cEJ{CiKqQAGfi&kKMfNv zm;q-AgBv<$_Ij(}r>JEH2V3`2KVa~Mj>J*%%aq^N2X>3!3?qdB=doTpS~tFJ!`;!t z$=kq@#n*6)c|AJpim!WG0lk9lS%?y@W6L#1SjSlKJ$@MC>CG1TdNwg-<8`cXm_p)( zs;^76aram_mtv+w>{0AY)v(WL9cqm!N~3vt(9o8mUrIA+bR{$$_r9vwxkmJ~Ow+{c z2QkuQ^CaMYM$bt$0fC^xFa)CLc|GLMw z)JtDy?+ZKxig_4X{{*XX zK3^y)w%aJQa$`z$K$M@*g8|Vm5cFMV&(_~43gD3z=zCcfhVQqka1HMPy>@ACP`lFE zB>0mD>vmEq5J@At>Rw8|x?u}^lWX}ehzV>1?Y+B>Y)f{t-3}RX1~vyhcjsXRNq)iy zj!82othS4>-|Vx^2n;pWQ8z)^j?t2C$CV}YcP5I{5BsPVu^OI+q?8dd1bP*?aikDL z<~3zKCR*@0`5ZT%lNnw+AL{0+iXoIGg)mQ^e$#ZEpttJg?K-P99kF?(A^AbS6NXx< z3=yoN8-%Z5tcPXJ1_?uQkz;D>U6(x9DoFwj*m#Hjg^28+d$mdzUX%$6sk*wnH_EkC zfI8GOi2cZ#0(C7m0T$p<`y^!e!%^J|Nfax;tdxX-MFWD66t?z!MQkihOD+`H{y_>} za@9CN7RVA;g5L3M?osxZKq$KT)D&j=V`5lmJJFB<@*1y=L#XyalPQ@Gc+{) zk8S9^ds*7P<}6QqoCYM4!&4_0LjS}Udt8hd6t7XOdarO3W+|6Mrykx-c$mzJ^XjmK zwE>bPz2O&SA{+KueN7Is)K>8PYUN$}mLlPa>WGH7GBzu8iJa^?om@_?vb!6~UIX`= z%n}MnngP$rFu)n0=0KDfLISJcX6#d|esUr5=72`cv;vy~NS`nu(crq(Yuw~AKfz81 z2jm$&R7?j`?IL2;aMxZddDS2^u1f#Mz+8{_Pohce%A8Q@qxK&Gkj^xvoQGIU>=spe zEpN^940$_1>LB#vT^}4=H9mssH6q&sidAXNWhHRumz1|+=WP#Oc3UZdFE0OxhhGW+ zydRqFf}hGAorgSmuLr6L*uXt?>KWP<(|On;2%_$(GoTW}^+WKoRcu3w)JVX^q<>m#o;1MOwXWU`Vn@bXZiDM85qc{^>uQ}?NQ24&rw zBp7iEA&P-m#%YD2nRja&7W|=5O_XC<=teQ`heeRzQQV@I*am(){=J{W5Xr@rhZ`KP zy`iEPH=c4LctXej438(W7mIzfdk~URjBHrq-t49)J=+xwCKN(N^+Ceb@DPe`{{W&K zaOUhLn=N6$e~wzYBmoNHh(L^+JW{JCTpd1_`U90ej zw2$?)V+ReC*eKj^y5GC4XuH~Frm8QlbHoRZe$0wSqUy2_y(&xH^6FP;JJSR>x5KBy z*l2VeaddBTZF#({@^FxFJjD^qi@99~Kzke_;t9VUB|$Ou*7TNnN<(i z70t|JK5fSO88zyDU#Y6G_!PL3kEig5a0Zkvc5Gl6w=!HL_str13xdW9V+ZGF{7a$@AuE+kNTWp6;Y4R zNnQuoZ$cQc`BWaSUC(8NSW$bm=d6{CW<`ehhhaX@kp$GGE|9nPGqMjpGhv96y%Fc; zx_%`r)d?agBFL=L{cr9el5#R<(gnV2(-iH?$uNTle$~5r!1!5mLl-RM{ilmfR{{&z z;x?II^63F1fEH(jtVVfaveEw83I2UHN&|QLjqh}nAS-)cCZPL1a z)JC*YZuV(S{HnJizx4A{mg{ii|0U$^{4;CNUT?G$pss)7yr>zyjTnO%1Kafc* zGdOCMXz=iS=c?%~?pI1P5zGu%bEC91_ky~E9(I=cN?s*6i~DCYRN!~affT_%N+iz( znp7q)jD?haFG2{uVI{K#WPxXJ;k+|01-XVt&c8z2l-)i;6%{rV?%Tg^DtnmDTdaWZ z8oYYk>F0A0xt`9=pH|YdI|380bZ1N%Qv?v1>*uA5uPZ!Q)yq~d`pG!8`HYO$Tv#J1 zE$QItWZD2=AUgakvH8g@JLCjF8kn@;5hULLJD#!lpEr9_b3axw{0R4kN1FVh=MbS| zCiTi>`Jz5JL*vnGO$>OgA6xQ>BZUYB1h~b=PN>3}MwOlQhEcq!IZ7~W-$l?#W_dCT zbN!{PpuUhZt1#mHIp#3@f|j0DM)k?1n#1rs`iB5bBizX5g$3TXhKXOBPLN0uqEZhRDeZ|AD34&h8gL(wri+wZkT z+?DmQB3hgQ5i0ZKRCaJ6_c@z!FG^f}Af6av0@u_5@CP}fZvhLU0Iv!RfcjwD)2@wTe&evnP&Y=MS z?hte3hY^UgaU7c=9s5=(0if6m1!fngmQk1ht*$*V74U)g%$Fa7=Pe~Pxs3vvlHKHb z0s#CWQMZ|gOb%rg8$ZKXU^?bX#MoA85-o@;f(M;k$4G%2|9-iXS-%2m+!2&wI#Mw^ z_K-u0yp_znype(4z$>U<2(F%b5yHsBXxN9y)|$0gUH&l?s?y8-`ypZPBq$Rf_p>T{i+2Eu@_|?E=r``JGN9iHAaGk-I)+PW==x#q zbBXX!X9gz-9dSM0W{v@)DykN~lP+ma-I(sdQ-2a? zAO2xYmI#T({&#k9Yc0=$jWR;WnI(w#&zrI>!A&a`hO=pgvv!z0*v zJGEb^IzeT9waLHlhC%jwdk}8Jh<^D|yoILAKzsL%iorota8%9E!=fvP@7WG4po#`E>_&ck28p(onXePbNzGwB)kMiLiSU?zn zsO`S!Qqzg~v4yB%!tFDPyp6j-oa0YOT3yt$9@u-D-iFkwdT;*qvC)O$$tHuwLN*4I z!7vA_(vx!$Vz#KKouUcR`A(7Nf15S261?3Jc3S$@i0>7ff>)juLa+oUQWMmw{6hS7 zEc>-HtswCRMOU4*SpM9pJ{(?5=>Nf1U;SW*>;=-Kff{G8$aXP!Ft6zrD>Zul=! zR9DM?{sn37B{0ur(zMvUH~E@5GMO5+3%2qECFYag!Qiyw2r~)RaNUQa#m>h`7Q)cF zAX@`XO-a`($h1^zq5P#&4gJuEu|10ggc*6@0lz7@LEQ9FN1q%H*_1%e*&k(q#8f>D zmV%@nIqRDJyCKGY`0kE0;Q?RqDAa+?iqK1Oh(kEqTk5-*h-dzzIszp|B{sTfugt== zwdvBH!IkLp@+bthy${+pqIYCd+h3z_;-PJ%C>tzmUA4`#YvMft)CFu4R<`~J3Z<++ zOPg*vQ@p^zC6r1A`yrB@jl@-&fQBUAJZRqYnXJ_%30*FN9;gB3y>hyzuOdBt#^w;2702~9xS+MW->UJsjeAKUr`}Pi}JfN zV8~>+E{!A_Yw9$WEsP5&ye!bhd1OsogoRY5<+ewJQedO|{aPt6ickq6!{uT;dJ!|} zQzPv^G1@w^EEYUv?Cu)EX^>jgVe^{fUiO#BHwB$n1;N7#*>F&hT0czs8n-t!kbpZH zFVxUBe^dwgz13rAt+rTe>>>*`fS_KZ{s^Dt8znv!`skM;q7$>x(ioFET|0v&|3v{YcuO@6(5okm0QqsNMdI%lc5>hs^LzowyWVfBp zr(K#SM(_Z`@<2ei=fI(Ft{e8x&I=M&Zgza@=D-0-uZ*oOjp{7~*KO?)XO&X6>emn{ zJT?cA5zm-M!X{lKcg~7Z-eb5|iDph1MN{6G$idzRpF-F0^h@sVty2rXJyDYaX)dTHA7;7N1(*8iQ#*H^SoVtT~SY3@j zEHr7QqB5+#HDS>t#a2c`>`=;UwAuqYN^O2_e>On#Lf%@bIidkAQ<-yOV-pN}z%;m* z8Ae=P&8Sp&gb#4#UTZDrtjl{|{YhNdV1oVOmm%0#&A&yd#&d7r;WZY9zjL;UPnmT+ z+s`;K{5UwLGyHu|xFm2Sk^=~QCxM;sWfz8#tJ1N~vOj#)=IS>9gtIHk;UBINYKATd zbosZ?U0V%vKv33jZZ5@ja@nO*QNMNkgFRllje#7WWO`hohZc(ZaHIyx`Qijp6CI2Y zAUtP~iYQc})gQCwU5A!8lzf#`mwn-4JQUR&QC{MPz;5fI?tsg-Vz3#58jBESGV8PI z^i{OMuP5^)TUGD(p+>+!bfZ*C*GyM zbl+vBegC?wKspH$bs7-a@Vx;fC&$w5fl3{iIY<6|_jcPnC$gD;V!VSh(ucvLFIVtK z*)s$jc*Qwn;uQ_d5pR7H0nXwZN35NMkY9T@#SvH#PKVH|aY2=FRkm#-Cq!4Xjl_#E z`ZTf1&C}#0tF6d>P3jN9{$9tJjKXj$qY!b&%)=dxh(rJ0p2{x@7mS`Rz844*cN#WsNlEucp=Hm$7 z37>n`++RSO3wc3$NyJP{smq`nF8)>pBB~?65ovwG#f(2 z%a0^^yAtFQ);ayh0Xz?nex3w>o3xHR!kMudVqk@KK9uB%!?CrV6^4?qoz0@FVr-vx ztl|mZHUILMktWskH!1LTGpJuLPoq}`CB)WRTQnTV_ za&7Ep4%?e%t7iQ1d0&emu6~}9;k#h+S@FIK0vHVb0E&4RMo(+@ili2|-s54bO?_k8 zq)iyBD1`m<8or3#ihO=xA?P3Ht78L3eD0z4g(A;Y7B!qts&xJHqV&q8D21)*8OTVYUCh&#qTIxF-@F5n zIMA#&PiABOUm>J8zQ<0M**dXn$yFBZK(S-sP}i?SdYu5vJ>p$SpsJ)Rrclu-^KYhq zdI%p^!pwzBTGIh!o`$Q6)Cex~a|dUBpvFUVSdHu}Nqi6o78n z-+(qkb28T)n~aO*2}qB&qSg0pKlcsk!fxn_GRo`zMtP%k`y87LS?LKgQxdnXBQr4mdaQ$Jy9$ap^* z!-t51!AAod;8UEeyaKI^Tx?y&k0&aw+~Q$eo3ma~>MW-UmC-J&bUhBVCiakmqhOpu zHQzdqvF*v;XUrb!+_^l~lV4Q|_Rh7xs#rt!p(!sN&VV5BI)QIwB01?Bi<*GT$#q^H zPJc#&9IoY|(|&4q7=h*dVzoSLV(@mHt;6lhz;Ale-P)6cBK7j=B7uLN0MY4*LGqP1 zBYZMib_Z_cpI%}^u~+ndW2B|1jQ!_I0jv7>F_?@a657J{`rV5C*D&C$d0!aM=QLT% zwE(f>%z)nha@^>vi``s!Ua6Begxi6$!a@#UF-=JEj@Fr)qc}a{lX9f7b-3yUL~6vY z)1_5Yc}EO3qqAz?-Y_^&x2I~s|D zi+Hed+~tHsg^&^!(%3G+Fro|9c!me18!lOYjl6HLU4PKO_e~Su+`dGkrK!2eN4rU`e;)JZ8-;J z`*4jkHF${!c@uY7+g+5G^cDz#qU8R@^+laxG^M2HS2BG}EUntbDWi-EqILeIBA#*4 z2;g_B>yG`2GfKTDX)8)2s%VmQsr0ERXv4N0P;6x=2@hmC||ZEi&u5`AU^D?J@~ z2!eLFZcqYBMrPQWlWjz;HQv!HVYd%Ooh%Q@%d^`;{JOKaFzYY%d!LKgloKG7|a>KTEoMo=cC|9aWVm9OaEfd~jCf~JMCsIjsgdDwqWVIgWL=9cV zdIZx~%{NF9uu!Y^UoXDBJbKGP z4~0qDE@a%E!w`*(V^|t&?7ar7w8arpIhpK&y`4FVSqD3(yw>e}-UV{V;|0g8($Q$W z!e)m6(!H}b6kc^|$6WvhS(S?ubj8Y^kGq-=6RH`K`?2c3A+ERfKAzw4f1L4W2j6dL zls{9=Iuk4h;NOg_>8+?^Gx3(4H_9E}VSq9LlvKu`f}W|gHW%aYYj0?fGdyLwl7uw% znwQ|4rP~c0Ex{R$ZG_LEk6P$)(k?TWHgpucBynyRUQQ2A8&KKRIL?EA1z?qkg)^5S z-Yfa_nzuxbPUi^En^eR6FJF-dBUW zInf4Chf*v;8om$0&nJmE?6;b6$Z#B3m+_*57J~h7oG}xg0vWRopjbeFzl_;|(2@I? zruHM#ejsjJI-`7& u3n_O*%M(lcvbg|rZ1b~tkz-^40kBMf!~+0?sZJEJ#Ao{g000001X)_>J^z9L diff --git a/gix-index/tests/fixtures/generated-archives/v2_all_file_kinds.tar.xz b/gix-index/tests/fixtures/generated-archives/v2_all_file_kinds.tar.xz index 169e964ba09391697fed10b6d15b31da052da2fe..63b0c86054d9c3e83161584e91a54d22061f4da2 100644 GIT binary patch delta 11040 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 as#buo2N3|$0>`Vd#Ao{g000001X)@U97|yU delta 10991 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 M`vL#}000D8S{My?yZ`_I delta 10430 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 head.tree diff --git a/gix-index/tests/fixtures/make_index/v2_all_file_kinds.sh b/gix-index/tests/fixtures/make_index/v2_all_file_kinds.sh index b7a4610da2f..5869a181928 100755 --- a/gix-index/tests/fixtures/make_index/v2_all_file_kinds.sh +++ b/gix-index/tests/fixtures/make_index/v2_all_file_kinds.sh @@ -22,3 +22,5 @@ mkdir d git add . git commit -m "init" + +git rev-parse @^{tree} > head.tree diff --git a/gix-index/tests/fixtures/make_index/v2_more_files.sh b/gix-index/tests/fixtures/make_index/v2_more_files.sh index d4cafddc097..11d6fb12b15 100755 --- a/gix-index/tests/fixtures/make_index/v2_more_files.sh +++ b/gix-index/tests/fixtures/make_index/v2_more_files.sh @@ -11,3 +11,5 @@ mkdir d git add . git commit -m "empty" + +git rev-parse @^{tree} > head.tree diff --git a/gix-index/tests/fixtures/make_index/v2_sparse_index_no_dirs.sh b/gix-index/tests/fixtures/make_index/v2_sparse_index_no_dirs.sh index 890483b4276..9c6275a9927 100755 --- a/gix-index/tests/fixtures/make_index/v2_sparse_index_no_dirs.sh +++ b/gix-index/tests/fixtures/make_index/v2_sparse_index_no_dirs.sh @@ -17,4 +17,4 @@ git config --worktree index.sparse true echo "/*" > .git/info/sparse-checkout && echo "!/*/" >> .git/info/sparse-checkout -git checkout main \ No newline at end of file +git checkout main diff --git a/gix-index/tests/fixtures/make_index/v3_sparse_index_non_cone.sh b/gix-index/tests/fixtures/make_index/v3_sparse_index_non_cone.sh index 542d9c8ba2d..36e8f4ff6d7 100755 --- a/gix-index/tests/fixtures/make_index/v3_sparse_index_non_cone.sh +++ b/gix-index/tests/fixtures/make_index/v3_sparse_index_non_cone.sh @@ -13,4 +13,4 @@ mkdir d git add . git commit -m "init" -git sparse-checkout set c1/c2 --no-cone \ No newline at end of file +git sparse-checkout set c1/c2 --no-cone diff --git a/gix-index/tests/fixtures/make_index/v4_more_files_IEOT.sh b/gix-index/tests/fixtures/make_index/v4_more_files_IEOT.sh index 3b22aaa6692..fe6368e096b 100755 --- a/gix-index/tests/fixtures/make_index/v4_more_files_IEOT.sh +++ b/gix-index/tests/fixtures/make_index/v4_more_files_IEOT.sh @@ -12,3 +12,5 @@ touch x git add . git commit -m "empty" + +git rev-parse @^{tree} > head.tree diff --git a/gix-index/tests/fixtures/make_traverse_literal_separators.sh b/gix-index/tests/fixtures/make_traverse_literal_separators.sh new file mode 100644 index 00000000000..a29fe25fa22 --- /dev/null +++ b/gix-index/tests/fixtures/make_traverse_literal_separators.sh @@ -0,0 +1,44 @@ +#!/bin/bash +set -eu -o pipefail + +# Makes a repo carrying a literally named file, which may even contain "/". +# File content is from stdin. Arguments are repo name, file name, and file mode. +function make_repo() ( + local repo="$1" file="$2" mode="$3" + local blob_hash_escaped tree_hash commit_hash branch + + git init -- "$repo" + cd -- "$repo" # Temporary, as the function body is a ( ) subshell. + + blob_hash_escaped="$(git hash-object -w --stdin | sed 's/../\\x&/g')" + + tree_hash="$( + printf "%s %s\\0$blob_hash_escaped" "$mode" "$file" | + git hash-object -t tree -w --stdin --literally + )" + + commit_hash="$(git commit-tree -m 'Initial commit' "$tree_hash")" + + branch="$(git symbolic-ref --short HEAD)" + git branch -f -- "$branch" "$commit_hash" + test -z "${DEBUG_FIXTURE-}" || git show # TODO: How should verbosity be controlled? + git rev-parse @^{tree} > head.tree +) + +make_repo traverse_dotdot_slashes ../outside 100644 \ + <<<'A file outside the working tree, somehow.' + +make_repo traverse_dotgit_slashes .git/hooks/pre-commit 100755 <<'EOF' +#!/bin/sh +printf 'Vulnerable!\n' +date >vulnerable +EOF + +make_repo traverse_dotdot_backslashes '..\outside' 100644 \ + <<<'A file outside the working tree, somehow.' + +make_repo traverse_dotgit_backslashes '.git\hooks\pre-commit' 100755 <<'EOF' +#!/bin/sh +printf 'Vulnerable!\n' +date >vulnerable +EOF \ No newline at end of file diff --git a/gix-index/tests/index/file/read.rs b/gix-index/tests/index/file/read.rs index 53387266f4c..bf1804384be 100644 --- a/gix-index/tests/index/file/read.rs +++ b/gix-index/tests/index/file/read.rs @@ -11,7 +11,7 @@ use crate::{hex_to_id, index::Fixture, loose_file_path}; fn verify(index: gix_index::File) -> gix_index::File { index.verify_integrity().unwrap(); index.verify_entries().unwrap(); - index.verify_extensions(false, gix::objs::find::Never).unwrap(); + index.verify_extensions(false, gix_object::find::Never).unwrap(); index } diff --git a/gix-index/tests/index/file/write.rs b/gix-index/tests/index/file/write.rs index a01963c8e31..10d924b4e21 100644 --- a/gix-index/tests/index/file/write.rs +++ b/gix-index/tests/index/file/write.rs @@ -228,7 +228,7 @@ fn compare_states_against_baseline( fn compare_states(actual: &State, actual_version: Version, expected: &State, options: Options, fixture: &str) { actual.verify_entries().expect("valid"); - actual.verify_extensions(false, gix::objs::find::Never).expect("valid"); + actual.verify_extensions(false, gix_object::find::Never).expect("valid"); assert_eq!( actual.version(), diff --git a/gix-index/tests/index/init.rs b/gix-index/tests/index/init.rs index 22595526605..cc3b0494119 100644 --- a/gix-index/tests/index/init.rs +++ b/gix-index/tests/index/init.rs @@ -1,5 +1,7 @@ use gix_index::State; use gix_testtools::scripted_fixture_read_only_standalone; +use std::error::Error; +use std::path::Path; #[test] fn from_tree() -> crate::Result { @@ -11,19 +13,45 @@ fn from_tree() -> crate::Result { ]; for fixture in fixtures { - let repo_dir = scripted_fixture_read_only_standalone(fixture)?; - let repo = gix::open(&repo_dir)?; + let worktree_dir = scripted_fixture_read_only_standalone(fixture)?; - let tree_id = repo.head_commit()?.tree_id()?; + let tree_id = tree_id(&worktree_dir); - let expected_state = repo.index()?; - let actual_state = State::from_tree(&tree_id, &repo.objects)?; + let git_dir = worktree_dir.join(".git"); + let expected_state = + gix_index::File::at(git_dir.join("index"), gix_hash::Kind::Sha1, false, Default::default())?; + let odb = gix_odb::at(git_dir.join("objects"))?; + let actual_state = State::from_tree(&tree_id, &odb, Default::default())?; compare_states(&actual_state, &expected_state, fixture) } Ok(()) } +#[test] +fn from_tree_validation() -> crate::Result { + let root = scripted_fixture_read_only_standalone("make_traverse_literal_separators.sh")?; + for repo_name in [ + "traverse_dotdot_slashes", + "traverse_dotgit_slashes", + "traverse_dotgit_backslashes", + "traverse_dotdot_backslashes", + ] { + let worktree_dir = root.join(repo_name); + let tree_id = tree_id(&worktree_dir); + let git_dir = worktree_dir.join(".git"); + let odb = gix_odb::at(git_dir.join("objects"))?; + + let err = State::from_tree(&tree_id, &odb, Default::default()).unwrap_err(); + assert_eq!( + err.source().expect("inner").to_string(), + "Path separators like / or \\ are not allowed", + "Note that this effectively tests what would happen on Windows, where \\ also isn't allowed" + ); + } + Ok(()) +} + #[test] fn new() { let state = State::new(gix_hash::Kind::Sha1); @@ -34,7 +62,7 @@ fn new() { fn compare_states(actual: &State, expected: &State, fixture: &str) { actual.verify_entries().expect("valid"); - actual.verify_extensions(false, gix::objs::find::Never).expect("valid"); + actual.verify_extensions(false, gix_object::find::Never).expect("valid"); assert_eq!( actual.entries().len(), @@ -49,3 +77,9 @@ fn compare_states(actual: &State, expected: &State, fixture: &str) { assert_eq!(a.path(actual), e.path(expected), "entry path mismatch in {fixture:?}"); } } + +fn tree_id(root: &Path) -> gix_hash::ObjectId { + let hex_hash = + std::fs::read_to_string(root.join("head.tree")).expect("head.tree was created by git rev-parse @^{tree}"); + hex_hash.trim().parse().expect("valid hash") +} From 5f86e6b11bb73921b458ffee9091bc028a7d6204 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 21 May 2024 16:43:09 +0200 Subject: [PATCH 43/50] adapt to changes in `gix-index` --- gix-fs/tests/stack/mod.rs | 1 + gix/src/clone/checkout.rs | 13 ++++++++----- gix/src/config/cache/access.rs | 4 ++-- gix/src/repository/filter.rs | 2 +- gix/src/repository/index.rs | 12 +++++++----- gix/src/repository/mod.rs | 25 +++++++++++++++++++++++-- 6 files changed, 42 insertions(+), 15 deletions(-) diff --git a/gix-fs/tests/stack/mod.rs b/gix-fs/tests/stack/mod.rs index 64a293eda38..7e50d40898e 100644 --- a/gix-fs/tests/stack/mod.rs +++ b/gix-fs/tests/stack/mod.rs @@ -1,3 +1,4 @@ +#![allow(clippy::join_absolute_paths)] use std::path::{Path, PathBuf}; use gix_fs::Stack; diff --git a/gix/src/clone/checkout.rs b/gix/src/clone/checkout.rs index e241adb18a1..e04a32fc484 100644 --- a/gix/src/clone/checkout.rs +++ b/gix/src/clone/checkout.rs @@ -18,8 +18,10 @@ pub mod main_worktree { #[error("Could not create index from tree at {id}")] IndexFromTree { id: gix_hash::ObjectId, - source: gix_traverse::tree::breadthfirst::Error, + source: gix_index::init::from_tree::Error, }, + #[error("Couldn't obtain configuration for core.protect*")] + BooleanConfig(#[from] crate::config::boolean::Error), #[error(transparent)] WriteIndex(#[from] gix_index::file::write::Error), #[error(transparent)] @@ -95,10 +97,11 @@ pub mod main_worktree { )) } }; - let index = gix_index::State::from_tree(&root_tree, &repo.objects).map_err(|err| Error::IndexFromTree { - id: root_tree, - source: err, - })?; + let index = gix_index::State::from_tree(&root_tree, &repo.objects, repo.config.protect_options()?) + .map_err(|err| Error::IndexFromTree { + id: root_tree, + source: err, + })?; let mut index = gix_index::File::from_state(index, repo.index_path()); let mut opts = repo diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index 5acf240b036..d752dc169ce 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -271,8 +271,8 @@ impl Cache { }) } - #[cfg(feature = "worktree-mutation")] - fn protect_options(&self) -> Result { + #[cfg(feature = "index")] + pub(crate) fn protect_options(&self) -> Result { const IS_WINDOWS: bool = cfg!(windows); const IS_MACOS: bool = cfg!(target_os = "macos"); const ALWAYS_ON_FOR_SAFETY: bool = true; diff --git a/gix/src/repository/filter.rs b/gix/src/repository/filter.rs index d5dc5690ea2..33ee80177bb 100644 --- a/gix/src/repository/filter.rs +++ b/gix/src/repository/filter.rs @@ -12,7 +12,7 @@ pub mod pipeline { #[error(transparent)] DecodeCommit(#[from] gix_object::decode::Error), #[error("Could not create index from tree at HEAD^{{tree}}")] - TreeTraverse(#[from] gix_traverse::tree::breadthfirst::Error), + TreeTraverse(#[from] crate::repository::index_from_tree::Error), #[error(transparent)] BareAttributes(#[from] crate::config::attribute_stack::Error), #[error(transparent)] diff --git a/gix/src/repository/index.rs b/gix/src/repository/index.rs index 9d2ba0ccfb1..c50abd3673b 100644 --- a/gix/src/repository/index.rs +++ b/gix/src/repository/index.rs @@ -111,12 +111,14 @@ impl crate::Repository { /// Create new index-file, which would live at the correct location, in memory from the given `tree`. /// /// Note that this is an expensive operation as it requires recursively traversing the entire tree to unpack it into the index. - pub fn index_from_tree( - &self, - tree: &gix_hash::oid, - ) -> Result { + pub fn index_from_tree(&self, tree: &gix_hash::oid) -> Result { Ok(gix_index::File::from_state( - gix_index::State::from_tree(tree, &self.objects)?, + gix_index::State::from_tree(tree, &self.objects, self.config.protect_options()?).map_err(|err| { + super::index_from_tree::Error::IndexFromTree { + id: tree.into(), + source: err, + } + })?, self.git_dir().join("index"), )) } diff --git a/gix/src/repository/mod.rs b/gix/src/repository/mod.rs index 9c2ffab4274..5f3fbc0b215 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -41,12 +41,15 @@ pub mod attributes; mod cache; mod config; /// +#[allow(clippy::empty_docs)] #[cfg(feature = "blob-diff")] pub mod diff; /// +#[allow(clippy::empty_docs)] #[cfg(feature = "dirwalk")] mod dirwalk; /// +#[allow(clippy::empty_docs)] #[cfg(feature = "attributes")] pub mod filter; mod graph; @@ -73,6 +76,24 @@ mod submodule; mod thread_safe; mod worktree; +/// +#[allow(clippy::empty_docs)] +#[cfg(feature = "index")] +pub mod index_from_tree { + /// The error returned by [Repository::index_from_tree()](crate::Repository::index_from_tree). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("Could not create index from tree at {id}")] + IndexFromTree { + id: gix_hash::ObjectId, + source: gix_index::init::from_tree::Error, + }, + #[error("Couldn't obtain configuration for core.protect*")] + BooleanConfig(#[from] crate::config::boolean::Error), + } +} + /// #[allow(clippy::empty_docs)] pub mod branch_remote_ref_name { @@ -133,7 +154,7 @@ pub mod index_or_load_from_head { #[error(transparent)] TreeId(#[from] gix_object::decode::Error), #[error(transparent)] - TraverseTree(#[from] gix_traverse::tree::breadthfirst::Error), + TraverseTree(#[from] crate::repository::index_from_tree::Error), #[error(transparent)] OpenIndex(#[from] crate::worktree::open_index::Error), } @@ -149,7 +170,7 @@ pub mod worktree_stream { #[error(transparent)] FindTree(#[from] crate::object::find::existing::Error), #[error(transparent)] - OpenTree(#[from] gix_traverse::tree::breadthfirst::Error), + OpenTree(#[from] crate::repository::index_from_tree::Error), #[error(transparent)] AttributesCache(#[from] crate::config::attribute_stack::Error), #[error(transparent)] From f1f0ba51cf1633bc2ca7e90904c01b8f8fee810e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 21 May 2024 17:25:16 +0200 Subject: [PATCH 44/50] feat: add `path::component_is_windows_device()` That way it's easy to determine if a component contains a windows device name --- gix-validate/src/path.rs | 30 +++++++++++++++++++++++------- gix-validate/tests/path/mod.rs | 10 ++++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/gix-validate/src/path.rs b/gix-validate/src/path.rs index dc85fa8f7fd..6a18fe7468e 100644 --- a/gix-validate/src/path.rs +++ b/gix-validate/src/path.rs @@ -124,16 +124,25 @@ pub fn component( Ok(input) } -fn check_win_devices_and_illegal_characters(input: &BStr) -> Option { - let in3 = input.get(..3)?; +/// Return `true` if the path component at `input` looks like a Windows device, like `CON` +/// or `LPT1` (case-insensitively). +/// +/// This is relevant only on Windows, where one may be tricked into reading or writing to such devices. +/// When reading from `CON`, a console-program may block until the user provided input. +pub fn component_is_windows_device(input: &BStr) -> bool { + is_win_device(input) +} + +fn is_win_device(input: &BStr) -> bool { + let Some(in3) = input.get(..3) else { return false }; if in3.eq_ignore_ascii_case(b"AUX") && is_done_windows(input.get(3..)) { - return Some(component::Error::WindowsReservedName); + return true; } if in3.eq_ignore_ascii_case(b"NUL") && is_done_windows(input.get(3..)) { - return Some(component::Error::WindowsReservedName); + return true; } if in3.eq_ignore_ascii_case(b"PRN") && is_done_windows(input.get(3..)) { - return Some(component::Error::WindowsReservedName); + return true; } // Note that the following allows `COM0`, even though `LPT0` is not allowed. // Even though tests seem to indicate that neither `LPT0` nor `COM0` are valid @@ -145,19 +154,26 @@ fn check_win_devices_and_illegal_characters(input: &BStr) -> Option= b'1' && *n <= b'9') && is_done_windows(input.get(4..)) { - return Some(component::Error::WindowsReservedName); + return true; } if in3.eq_ignore_ascii_case(b"LPT") && input.get(3).map_or(false, u8::is_ascii_digit) && is_done_windows(input.get(4..)) { - return Some(component::Error::WindowsReservedName); + return true; } if in3.eq_ignore_ascii_case(b"CON") && (is_done_windows(input.get(3..)) || (input.get(3..6).map_or(false, |n| n.eq_ignore_ascii_case(b"IN$")) && is_done_windows(input.get(6..))) || (input.get(3..7).map_or(false, |n| n.eq_ignore_ascii_case(b"OUT$")) && is_done_windows(input.get(7..)))) { + return true; + } + false +} + +fn check_win_devices_and_illegal_characters(input: &BStr) -> Option { + if is_win_device(input) { return Some(component::Error::WindowsReservedName); } if input.iter().any(|b| *b < 0x20 || b":<>\"|?*".contains(b)) { diff --git a/gix-validate/tests/path/mod.rs b/gix-validate/tests/path/mod.rs index 7adf39db830..0257270ad23 100644 --- a/gix-validate/tests/path/mod.rs +++ b/gix-validate/tests/path/mod.rs @@ -1,3 +1,13 @@ +#[test] +fn component_is_windows_device() { + for device in ["con", "CONIN$", "lpt1.txt", "AUX", "Prn", "NUL", "COM9"] { + assert!(gix_validate::path::component_is_windows_device(device.into())); + } + for not_device in ["coni", "CONIN", "lpt", "AUXi", "aPrn", "NULl", "COM"] { + assert!(!gix_validate::path::component_is_windows_device(not_device.into())); + } +} + mod component { use gix_validate::path::component; From 9555efe8964d3de3c692f79cef390916e34daefb Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 21 May 2024 20:10:13 +0200 Subject: [PATCH 45/50] fix!: assure that special device names on Windows aren't allowed. Otherwise it's possible to read or write to devices when interacting with references of the 'right' name. This behaviour can be controlled with the new `prohibit_windows_device_names` flag, which is adjustable on the `Store` instance as field, and which now has to be passed during instantiation as part of the new `store::init::Options` struct. --- gix-ref/src/lib.rs | 22 ++++++- gix-ref/src/store/file/find.rs | 14 ++++- gix-ref/src/store/file/loose/mod.rs | 30 ++++++---- .../loose/reflog/create_or_update/tests.rs | 8 ++- gix-ref/src/store/file/mod.rs | 3 + gix-ref/src/store/general/init.rs | 19 ++---- gix-ref/tests/file/mod.rs | 17 +----- gix-ref/tests/file/store/mod.rs | 16 +++-- gix-ref/tests/file/store/reflog.rs | 7 ++- gix-ref/tests/file/transaction/mod.rs | 7 +-- .../create_or_update/mod.rs | 58 +++++++++++++++++++ gix-ref/tests/file/worktree.rs | 10 +--- 12 files changed, 144 insertions(+), 67 deletions(-) diff --git a/gix-ref/src/lib.rs b/gix-ref/src/lib.rs index ecf54ed49fe..4aeae1b037e 100644 --- a/gix-ref/src/lib.rs +++ b/gix-ref/src/lib.rs @@ -62,6 +62,25 @@ pub mod peel; /// #[allow(clippy::empty_docs)] pub mod store { + /// + #[allow(clippy::empty_docs)] + pub mod init { + + /// Options for use during [initialization](crate::Store::at). + #[derive(Debug, Copy, Clone, Default)] + pub struct Options { + /// How to write the ref-log. + pub write_reflog: super::WriteReflog, + /// The kind of hash to expect in + pub object_hash: gix_hash::Kind, + /// The equivalent of `core.precomposeUnicode`. + pub precompose_unicode: bool, + /// If `true`, we will avoid reading from or writing to references that contains Windows device names + /// to avoid side effects. This only needs to be `true` on Windows, but can be `true` on other platforms + /// if they need to remain compatible with Windows. + pub prohibit_windows_device_names: bool, + } + } /// The way a file store handles the reflog #[derive(Default, Debug, PartialOrd, PartialEq, Ord, Eq, Hash, Clone, Copy)] pub enum WriteReflog { @@ -93,9 +112,8 @@ pub mod store { /// #[path = "general/handle/mod.rs"] mod handle; - pub use handle::find; - use crate::file; + pub use handle::find; } /// The git reference store. diff --git a/gix-ref/src/store/file/find.rs b/gix-ref/src/store/file/find.rs index b8a45e86b2e..c84eeda6ecc 100644 --- a/gix-ref/src/store/file/find.rs +++ b/gix-ref/src/store/file/find.rs @@ -251,7 +251,19 @@ impl file::Store { /// Read the file contents with a verified full reference path and return it in the given vector if possible. pub(crate) fn ref_contents(&self, name: &FullNameRef) -> io::Result>> { - let ref_path = self.reference_path(name); + let (base, relative_path) = self.reference_path_with_base(name); + if self.prohibit_windows_device_names + && relative_path + .components() + .filter_map(|c| gix_path::try_os_str_into_bstr(c.as_os_str().into()).ok()) + .any(|c| gix_validate::path::component_is_windows_device(c.as_ref())) + { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Illegal use of reserved Windows device name in \"{}\"", name.as_bstr()), + )); + } + let ref_path = base.join(relative_path); match std::fs::File::open(&ref_path) { Ok(mut file) => { diff --git a/gix-ref/src/store/file/loose/mod.rs b/gix-ref/src/store/file/loose/mod.rs index f1fd8735163..57c1317d8dd 100644 --- a/gix-ref/src/store/file/loose/mod.rs +++ b/gix-ref/src/store/file/loose/mod.rs @@ -35,15 +35,18 @@ mod init { impl file::Store { /// Create a new instance at the given `git_dir`, which commonly is a standard git repository with a /// `refs/` subdirectory. - /// The `object_hash` defines which kind of hash we should recognize. + /// Use [`Options`](crate::store::init::Options) to adjust settings. /// - /// Note that if `precompose_unicode` is set, the `git_dir` is also expected to use precomposed unicode, - /// or else some operations that strip prefixes will fail. + /// Note that if [`precompose_unicode`](crate::store::init::Options::precompose_unicode) is set in the options, + /// the `git_dir` is also expected to use precomposed unicode, or else some operations that strip prefixes will fail. pub fn at( git_dir: PathBuf, - write_reflog: file::WriteReflog, - object_hash: gix_hash::Kind, - precompose_unicode: bool, + crate::store::init::Options { + write_reflog, + object_hash, + precompose_unicode, + prohibit_windows_device_names, + }: crate::store::init::Options, ) -> Self { file::Store { git_dir, @@ -51,6 +54,7 @@ mod init { common_dir: None, write_reflog, namespace: None, + prohibit_windows_device_names, packed: gix_fs::SharedFileSnapshotMut::new().into(), object_hash, precompose_unicode, @@ -60,14 +64,17 @@ mod init { /// Like [`at()`][file::Store::at()], but for _linked_ work-trees which use `git_dir` as private ref store and `common_dir` for /// shared references. /// - /// Note that if `precompose_unicode` is set, the `git_dir` and `common_dir` are also expected to use precomposed unicode, - /// or else some operations that strip prefixes will fail. + /// Note that if [`precompose_unicode`](crate::store::init::Options::precompose_unicode) is set, the `git_dir` and + /// `common_dir` are also expected to use precomposed unicode, or else some operations that strip prefixes will fail. pub fn for_linked_worktree( git_dir: PathBuf, common_dir: PathBuf, - write_reflog: file::WriteReflog, - object_hash: gix_hash::Kind, - precompose_unicode: bool, + crate::store::init::Options { + write_reflog, + object_hash, + precompose_unicode, + prohibit_windows_device_names, + }: crate::store::init::Options, ) -> Self { file::Store { git_dir, @@ -75,6 +82,7 @@ mod init { common_dir: Some(common_dir), write_reflog, namespace: None, + prohibit_windows_device_names, packed: gix_fs::SharedFileSnapshotMut::new().into(), object_hash, precompose_unicode, diff --git a/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs b/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs index c2b487ad9e2..45d74cead09 100644 --- a/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs +++ b/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs @@ -14,7 +14,13 @@ fn hex_to_id(hex: &str) -> gix_hash::ObjectId { fn empty_store(writemode: WriteReflog) -> Result<(TempDir, file::Store)> { let dir = TempDir::new()?; - let store = file::Store::at(dir.path().into(), writemode, gix_hash::Kind::Sha1, false); + let store = file::Store::at( + dir.path().into(), + crate::store::init::Options { + write_reflog: writemode, + ..Default::default() + }, + ); Ok((dir, store)) } diff --git a/gix-ref/src/store/file/mod.rs b/gix-ref/src/store/file/mod.rs index 19296b0af3f..ec7c7fc624c 100644 --- a/gix-ref/src/store/file/mod.rs +++ b/gix-ref/src/store/file/mod.rs @@ -27,6 +27,9 @@ pub struct Store { pub write_reflog: WriteReflog, /// The namespace to use for edits and reads pub namespace: Option, + /// This is only useful on Windows, which may have 'virtual' devices on each level of a path so that + /// reading or writing `refs/heads/CON` for example would read from the console, or write to it. + pub prohibit_windows_device_names: bool, /// If set, we will convert decomposed unicode like `a\u308` into precomposed unicode like `รค` when reading /// ref names from disk. /// Note that this is an internal operation that isn't observable on the outside, but it's needed for lookups diff --git a/gix-ref/src/store/general/init.rs b/gix-ref/src/store/general/init.rs index 6b5ee9e87f2..efe5dacfaf5 100644 --- a/gix-ref/src/store/general/init.rs +++ b/gix-ref/src/store/general/init.rs @@ -1,7 +1,5 @@ use std::path::PathBuf; -use crate::store::WriteReflog; - mod error { /// The error returned by [`crate::Store::at()`]. #[derive(Debug, thiserror::Error)] @@ -19,23 +17,16 @@ use crate::file; #[allow(dead_code)] impl crate::Store { /// Create a new store at the given location, typically the `.git/` directory. + /// Use [`opts`](crate::store::init::Options) to adjust settings. /// - /// `object_hash` defines the kind of hash to assume when dealing with refs. - /// `precompose_unicode` is used to set to the value of [`crate::file::Store::precompose_unicode]. - /// - /// Note that if `precompose_unicode` is set, the `git_dir` is also expected to use precomposed unicode, - /// or else some operations that strip prefixes will fail. - pub fn at( - git_dir: PathBuf, - reflog_mode: WriteReflog, - object_hash: gix_hash::Kind, - precompose_unicode: bool, - ) -> Result { + /// Note that if [`precompose_unicode`](crate::store::init::Options::precompose_unicode) is set in the options, + /// the `git_dir` is also expected to use precomposed unicode, or else some operations that strip prefixes will fail. + pub fn at(git_dir: PathBuf, opts: crate::store::init::Options) -> Result { // for now, just try to read the directory - later we will do that naturally as we have to figure out if it's a ref-table or not. std::fs::read_dir(&git_dir)?; Ok(crate::Store { inner: crate::store::State::Loose { - store: file::Store::at(git_dir, reflog_mode, object_hash, precompose_unicode), + store: file::Store::at(git_dir, opts), }, }) } diff --git a/gix-ref/tests/file/mod.rs b/gix-ref/tests/file/mod.rs index 3f932075af8..9c2aa843418 100644 --- a/gix-ref/tests/file/mod.rs +++ b/gix-ref/tests/file/mod.rs @@ -15,26 +15,13 @@ pub fn store_with_packed_refs() -> crate::Result { pub fn store_at(name: &str) -> crate::Result { let path = gix_testtools::scripted_fixture_read_only_standalone(name)?; - Ok(Store::at( - path.join(".git"), - gix_ref::store::WriteReflog::Normal, - gix_hash::Kind::Sha1, - false, - )) + Ok(Store::at(path.join(".git"), Default::default())) } fn store_writable(name: &str) -> crate::Result<(gix_testtools::tempfile::TempDir, Store)> { let dir = gix_testtools::scripted_fixture_writable_standalone(name)?; let git_dir = dir.path().join(".git"); - Ok(( - dir, - Store::at( - git_dir, - gix_ref::store::WriteReflog::Normal, - gix_hash::Kind::Sha1, - false, - ), - )) + Ok((dir, Store::at(git_dir, Default::default()))) } struct EmptyCommit; diff --git a/gix-ref/tests/file/store/mod.rs b/gix-ref/tests/file/store/mod.rs index a3e7db39164..4cfd6120b03 100644 --- a/gix-ref/tests/file/store/mod.rs +++ b/gix-ref/tests/file/store/mod.rs @@ -21,9 +21,11 @@ fn precompose_unicode_journey() -> crate::Result { let store_decomposed = gix_ref::file::Store::at( root, - WriteReflog::Always, - gix_hash::Kind::Sha1, - false, /* precompose_unicode */ + gix_ref::store::init::Options { + write_reflog: WriteReflog::Always, + precompose_unicode: false, + ..Default::default() + }, ); assert!(!store_decomposed.precompose_unicode); @@ -46,9 +48,11 @@ fn precompose_unicode_journey() -> crate::Result { let store_precomposed = gix_ref::file::Store::at( tmp.path().join(precomposed_a), // it's important that root paths are also precomposed then. - WriteReflog::Always, - gix_hash::Kind::Sha1, - true, /* precompose_unicode */ + gix_ref::store::init::Options { + write_reflog: WriteReflog::Always, + precompose_unicode: true, + ..Default::default() + }, ); let precomposed_ref = format!("refs/heads/{precomposed_a}"); diff --git a/gix-ref/tests/file/store/reflog.rs b/gix-ref/tests/file/store/reflog.rs index 27ffdd82630..a0a2ad3b124 100644 --- a/gix-ref/tests/file/store/reflog.rs +++ b/gix-ref/tests/file/store/reflog.rs @@ -1,9 +1,10 @@ fn store() -> crate::Result { Ok(crate::file::Store::at( gix_testtools::scripted_fixture_read_only_standalone("make_repo_for_reflog.sh")?.join(".git"), - gix_ref::store::WriteReflog::Disable, - gix_hash::Kind::Sha1, - false, + gix_ref::store::init::Options { + write_reflog: gix_ref::store::WriteReflog::Disable, + ..Default::default() + }, )) } diff --git a/gix-ref/tests/file/transaction/mod.rs b/gix-ref/tests/file/transaction/mod.rs index e7f8d344e38..348f76df6e7 100644 --- a/gix-ref/tests/file/transaction/mod.rs +++ b/gix-ref/tests/file/transaction/mod.rs @@ -22,12 +22,7 @@ pub(crate) mod prepare_and_commit { pub(crate) fn empty_store() -> crate::Result<(gix_testtools::tempfile::TempDir, file::Store)> { let dir = gix_testtools::tempfile::TempDir::new().unwrap(); - let store = file::Store::at( - dir.path().into(), - gix_ref::store::WriteReflog::Normal, - gix_hash::Kind::Sha1, - false, - ); + let store = file::Store::at(dir.path().into(), Default::default()); Ok((dir, store)) } diff --git a/gix-ref/tests/file/transaction/prepare_and_commit/create_or_update/mod.rs b/gix-ref/tests/file/transaction/prepare_and_commit/create_or_update/mod.rs index 99ec59d673d..6c71b98ca76 100644 --- a/gix-ref/tests/file/transaction/prepare_and_commit/create_or_update/mod.rs +++ b/gix-ref/tests/file/transaction/prepare_and_commit/create_or_update/mod.rs @@ -10,6 +10,7 @@ use gix_ref::{ transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog}, Target, }; +use std::error::Error; use crate::{ file::{ @@ -430,6 +431,63 @@ fn symbolic_reference_writes_reflog_if_previous_value_is_set() -> crate::Result Ok(()) } +#[test] +fn windows_device_name_is_illegal_with_enabled_windows_protections() -> crate::Result { + let (_keep, mut store) = empty_store()?; + store.prohibit_windows_device_names = true; + let log_ignored = LogChange { + mode: RefLog::AndReference, + force_create_reflog: false, + message: "ignored".into(), + }; + + let new = Target::Peeled(hex_to_id("28ce6a8b26aa170e1de65536fe8abe1832bd3242")); + for invalid_name in ["refs/heads/CON", "refs/CON/still-invalid"] { + let err = store + .transaction() + .prepare( + Some(RefEdit { + change: Change::Update { + log: log_ignored.clone(), + new: new.clone(), + expected: PreviousValue::Any, + }, + name: invalid_name.try_into()?, + deref: false, + }), + Fail::Immediately, + Fail::Immediately, + ) + .unwrap_err(); + assert_eq!( + err.source().expect("inner").to_string(), + format!("Illegal use of reserved Windows device name in \"{invalid_name}\""), + "it's notable that the check also kicks in when the previous value doesn't matter - we expect a 'read' to happen anyway \ + - it can't be optimized away as the previous value is stored in the transaction result right now." + ); + } + + #[cfg(not(windows))] + { + store.prohibit_windows_device_names = false; + let _prepared_transaction = store.transaction().prepare( + Some(RefEdit { + change: Change::Update { + log: log_ignored.clone(), + new, + expected: PreviousValue::Any, + }, + name: "refs/heads/CON".try_into()?, + deref: false, + }), + Fail::Immediately, + Fail::Immediately, + )?; + } + + Ok(()) +} + #[test] fn symbolic_head_missing_referent_then_update_referent() -> crate::Result { for reflog_writemode in &[WriteReflog::Normal, WriteReflog::Disable, WriteReflog::Always] { diff --git a/gix-ref/tests/file/worktree.rs b/gix-ref/tests/file/worktree.rs index 196ac3d1260..7b723eca107 100644 --- a/gix-ref/tests/file/worktree.rs +++ b/gix-ref/tests/file/worktree.rs @@ -29,7 +29,7 @@ fn main_store( let (dir, tmp) = dir(packed, writable)?; let git_dir = dir.join("repo").join(".git"); Ok(( - gix_ref::file::Store::at(git_dir.clone(), Default::default(), Default::default(), false), + gix_ref::file::Store::at(git_dir.clone(), Default::default()), gix_odb::at(git_dir.join("objects"))?, tmp, )) @@ -50,13 +50,7 @@ fn worktree_store( .into_repository_and_work_tree_directories(); let common_dir = git_dir.join("../.."); Ok(( - gix_ref::file::Store::for_linked_worktree( - git_dir, - common_dir.clone(), - Default::default(), - Default::default(), - false, - ), + gix_ref::file::Store::for_linked_worktree(git_dir, common_dir.clone(), Default::default()), gix_odb::at(common_dir.join("objects"))?, tmp, )) From d2ae9d5f11be9f2561f6799d88804d0d8eae33ef Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 21 May 2024 21:24:56 +0200 Subject: [PATCH 46/50] adapt to changes in `gix-ref` --- Cargo.lock | 259 +++++++++++++++++++++++++++- gix-discover/src/is.rs | 8 +- gix-negotiate/tests/baseline/mod.rs | 7 +- gix/Cargo.toml | 1 + gix/src/config/cache/incubate.rs | 13 +- gix/src/config/cache/init.rs | 1 + gix/src/open/repository.rs | 18 +- 7 files changed, 285 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 09058e67861..0c2d0d472d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -589,6 +589,26 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "config" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + [[package]] name = "conpty" version = "0.5.1" @@ -598,6 +618,35 @@ dependencies = [ "windows 0.44.0", ] +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -790,6 +839,12 @@ dependencies = [ "tui-react", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -875,6 +930,15 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "document-features" version = "0.2.8" @@ -1280,6 +1344,7 @@ version = "0.62.0" dependencies = [ "anyhow", "async-std", + "config", "document-features", "gix-actor 0.31.1", "gix-archive", @@ -2925,6 +2990,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + [[package]] name = "hashbrown" version = "0.14.3" @@ -3231,6 +3302,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "jwalk" version = "0.8.1" @@ -3312,6 +3394,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -3425,6 +3513,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -3477,6 +3571,16 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "ntapi" version = "0.4.1" @@ -3621,6 +3725,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +dependencies = [ + "dlv-list", + "hashbrown 0.13.2", +] + [[package]] name = "overload" version = "0.1.1" @@ -3674,6 +3788,51 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "pest_meta" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "pin-project" version = "1.1.5" @@ -3804,7 +3963,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.15", ] [[package]] @@ -4018,6 +4177,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.5", + "bitflags 2.4.1", + "serde", + "serde_derive", +] + [[package]] name = "rusqlite" version = "0.30.0" @@ -4032,6 +4203,16 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-ini" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -4241,6 +4422,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4305,6 +4495,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -4622,6 +4823,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -4698,11 +4908,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.8.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.13", +] + [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -4715,6 +4940,19 @@ dependencies = [ "winnow 0.5.40", ] +[[package]] +name = "toml_edit" +version = "0.22.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.0", +] + [[package]] name = "tower" version = "0.4.13" @@ -4853,6 +5091,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "uluru" version = "3.0.0" @@ -5385,6 +5629,15 @@ dependencies = [ "lzma-sys", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yansi" version = "0.5.1" diff --git a/gix-discover/src/is.rs b/gix-discover/src/is.rs index 055a68e67cd..c0172ef9a06 100644 --- a/gix-discover/src/is.rs +++ b/gix-discover/src/is.rs @@ -172,13 +172,7 @@ pub(crate) fn git_with_metadata( // We expect to be able to parse any ref-hash, so we shouldn't have to know the repos hash here. // With ref-table, the has is probably stored as part of the ref-db itself, so we can handle it from there. // In other words, it's important not to fail on detached heads here because we guessed the hash kind wrongly. - let object_hash_should_not_matter_here = gix_hash::Kind::Sha1; - let refs = gix_ref::file::Store::at( - dot_git.as_ref().into(), - gix_ref::store::WriteReflog::Normal, - object_hash_should_not_matter_here, - false, - ); + let refs = gix_ref::file::Store::at(dot_git.as_ref().into(), Default::default()); let head = refs.find_loose("HEAD")?; if head.name.as_bstr() != "HEAD" { return Err(crate::is_git::Error::MisplacedHead { diff --git a/gix-negotiate/tests/baseline/mod.rs b/gix-negotiate/tests/baseline/mod.rs index 158d70a4be3..f416097f428 100644 --- a/gix-negotiate/tests/baseline/mod.rs +++ b/gix-negotiate/tests/baseline/mod.rs @@ -26,9 +26,10 @@ fn run() -> crate::Result { let store = gix_odb::at(base.join("client").join(".git/objects"))?; let refs = gix_ref::file::Store::at( base.join("client").join(".git"), - WriteReflog::Disable, - gix_hash::Kind::Sha1, - false, + gix_ref::store::init::Options { + write_reflog: WriteReflog::Disable, + ..Default::default() + }, ); let lookup_names = |names: &[&str]| -> Vec { names diff --git a/gix/Cargo.toml b/gix/Cargo.toml index 556b64f2112..0e83deaaf28 100644 --- a/gix/Cargo.toml +++ b/gix/Cargo.toml @@ -375,6 +375,7 @@ regex = { version = "1.6.0", optional = true, default-features = false, features parking_lot = "0.12.1" document-features = { version = "0.2.0", optional = true } +config = "0.14.0" [dev-dependencies] pretty_assertions = "1.4.0" diff --git a/gix/src/config/cache/incubate.rs b/gix/src/config/cache/incubate.rs index 0bd0a3b5df4..505bbfd0640 100644 --- a/gix/src/config/cache/incubate.rs +++ b/gix/src/config/cache/incubate.rs @@ -1,7 +1,7 @@ #![allow(clippy::result_large_err)] use super::{util, Error}; -use crate::config::cache::util::ApplyLeniency; +use crate::config::cache::util::{ApplyLeniency, ApplyLeniencyDefaultValue}; use crate::config::tree::{Core, Extensions, Key}; /// A utility to deal with the cyclic dependency between the ref store and the configuration. The ref-store needs the @@ -15,6 +15,7 @@ pub(crate) struct StageOne { pub object_hash: gix_hash::Kind, pub reflog: Option, pub precompose_unicode: bool, + pub protect_windows: bool, } /// Initialization @@ -80,6 +81,15 @@ impl StageOne { .map_err(Error::ConfigBoolean)? .unwrap_or_default(); + const IS_WINDOWS: bool = cfg!(windows); + let protect_windows = crate::config::tree::gitoxide::Core::PROTECT_WINDOWS + .enrich_error( + config + .boolean("gitoxide", Some("core".into()), "protectWindows") + .unwrap_or(Ok(IS_WINDOWS)), + ) + .with_lenient_default_value(lenient, IS_WINDOWS)?; + let reflog = util::query_refupdates(&config, lenient)?; Ok(StageOne { git_dir_config: config, @@ -89,6 +99,7 @@ impl StageOne { object_hash, reflog, precompose_unicode, + protect_windows, }) } } diff --git a/gix/src/config/cache/init.rs b/gix/src/config/cache/init.rs index 76e6dd81e41..56eb50e7059 100644 --- a/gix/src/config/cache/init.rs +++ b/gix/src/config/cache/init.rs @@ -28,6 +28,7 @@ impl Cache { object_hash, reflog: _, precompose_unicode: _, + protect_windows: _, }: StageOne, git_dir: &std::path::Path, branch_name: Option<&gix_ref::FullNameRef>, diff --git a/gix/src/open/repository.rs b/gix/src/open/repository.rs index 7c5b065bd78..e5d475c8009 100644 --- a/gix/src/open/repository.rs +++ b/gix/src/open/repository.rs @@ -216,15 +216,17 @@ impl ThreadSafeRepository { let mut refs = { let reflog = repo_config.reflog.unwrap_or(gix_ref::store::WriteReflog::Disable); let object_hash = repo_config.object_hash; + let ref_store_init_opts = gix_ref::store::init::Options { + write_reflog: reflog, + object_hash, + precompose_unicode: repo_config.precompose_unicode, + prohibit_windows_device_names: repo_config.protect_windows, + }; match &common_dir { - Some(common_dir) => crate::RefStore::for_linked_worktree( - git_dir.to_owned(), - common_dir.into(), - reflog, - object_hash, - repo_config.precompose_unicode, - ), - None => crate::RefStore::at(git_dir.to_owned(), reflog, object_hash, repo_config.precompose_unicode), + Some(common_dir) => { + crate::RefStore::for_linked_worktree(git_dir.to_owned(), common_dir.into(), ref_store_init_opts) + } + None => crate::RefStore::at(git_dir.to_owned(), ref_store_init_opts), } }; let head = refs.find("HEAD").ok(); From 1242151079004ae99fae7b80966de151961a6159 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 22 May 2024 07:22:01 +0200 Subject: [PATCH 47/50] Apply suggestions from code review Co-authored-by: Eliah Kagan --- gix-fs/tests/stack/mod.rs | 4 ++-- gix-ref/src/store/file/mod.rs | 2 +- gix-validate/tests/path/mod.rs | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gix-fs/tests/stack/mod.rs b/gix-fs/tests/stack/mod.rs index 7e50d40898e..c79db87dbf9 100644 --- a/gix-fs/tests/stack/mod.rs +++ b/gix-fs/tests/stack/mod.rs @@ -59,7 +59,7 @@ fn path_join_handling() { assert_eq!( p("c:").join("relative"), p("c:relative"), - "absolute + relative = strange joined result with missing backslash, but it's a valid path that works just like `c:\relative`" + "drive + relative = strange joined result with missing backslash, but it's a valid path that works just like `c:\relative`" ); assert_eq!( p("c:\\").join("relative"), @@ -102,7 +102,7 @@ fn path_join_handling() { assert_eq!( p("c:\\").join("\\\\.\\"), p("\\\\.\\"), - "d-drive-with-bs + device-namespace-unc = device-namespace-unc" + "c-drive-with-bs + device-namespace-unc = device-namespace-unc" ); assert_eq!( p("/").join("C:/"), diff --git a/gix-ref/src/store/file/mod.rs b/gix-ref/src/store/file/mod.rs index ec7c7fc624c..c01c9deb4d8 100644 --- a/gix-ref/src/store/file/mod.rs +++ b/gix-ref/src/store/file/mod.rs @@ -27,7 +27,7 @@ pub struct Store { pub write_reflog: WriteReflog, /// The namespace to use for edits and reads pub namespace: Option, - /// This is only useful on Windows, which may have 'virtual' devices on each level of a path so that + /// This is only needed on Windows, where some device names are reserved at any level of a path, so that /// reading or writing `refs/heads/CON` for example would read from the console, or write to it. pub prohibit_windows_device_names: bool, /// If set, we will convert decomposed unicode like `a\u308` into precomposed unicode like `รค` when reading diff --git a/gix-validate/tests/path/mod.rs b/gix-validate/tests/path/mod.rs index 0257270ad23..e9aa7a6ec88 100644 --- a/gix-validate/tests/path/mod.rs +++ b/gix-validate/tests/path/mod.rs @@ -66,7 +66,7 @@ mod component { mktest!(dot_gitmodules_as_file, b".gitmodules", UNIX_OPTS); mktest!( starts_with_dot_git_with_backslashes_on_linux, - b".git\\hooks\\precommit", + b".git\\hooks\\pre-commit", UNIX_OPTS ); mktest!(not_dot_git_shorter, b".gi", NO_OPTS); @@ -136,7 +136,7 @@ mod component { mktest!(dot_git_upper, b".GIT", Error::DotGitDir, NO_OPTS); mktest!( starts_with_dot_git_with_backslashes_on_windows, - b".git\\hooks\\precommit", + b".git\\hooks\\pre-commit", Error::PathSeparator ); mktest!(dot_git_upper_hfs, ".GIT\u{200e}".as_bytes(), Error::DotGitDir); @@ -199,12 +199,12 @@ mod component { Error::WindowsIllegalCharacter ); mktest!( - ntfs_stream_default_explicit, - b"file:$ANYTHING_REALLY:$DATA", + ntfs_stream_explicit, + b"file:ANYTHING_REALLY:$DATA", Error::WindowsIllegalCharacter ); mktest!( - dot_gitmodules_lower_ntfs_stream_default_explicit, + dot_gitmodules_lower_ntfs_stream, b".gitmodules:$DATA:$DATA", Error::SymlinkedGitModules, Symlink, From 6f55f2abd13078f94e8c4e10922806f195ae0d8b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 22 May 2024 10:19:29 +0200 Subject: [PATCH 48/50] fix-CI --- Cargo.lock | 255 +----------------- deny.toml | 23 -- gix-ref/src/lib.rs | 2 +- gix-ref/src/store/file/find.rs | 2 +- gix-ref/src/store/file/transaction/prepare.rs | 4 +- gix/Cargo.toml | 1 - 6 files changed, 4 insertions(+), 283 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c2d0d472d8..481394bb111 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -589,26 +589,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "config" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" -dependencies = [ - "async-trait", - "convert_case", - "json5", - "lazy_static", - "nom", - "pathdiff", - "ron", - "rust-ini", - "serde", - "serde_json", - "toml", - "yaml-rust", -] - [[package]] name = "conpty" version = "0.5.1" @@ -618,35 +598,6 @@ dependencies = [ "windows 0.44.0", ] -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom", - "once_cell", - "tiny-keccak", -] - -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -839,12 +790,6 @@ dependencies = [ "tui-react", ] -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - [[package]] name = "crypto-common" version = "0.1.6" @@ -930,15 +875,6 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "dlv-list" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = [ - "const-random", -] - [[package]] name = "document-features" version = "0.2.8" @@ -1344,7 +1280,6 @@ version = "0.62.0" dependencies = [ "anyhow", "async-std", - "config", "document-features", "gix-actor 0.31.1", "gix-archive", @@ -2990,12 +2925,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" - [[package]] name = "hashbrown" version = "0.14.3" @@ -3302,17 +3231,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - [[package]] name = "jwalk" version = "0.8.1" @@ -3394,12 +3312,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -3513,12 +3425,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.7.1" @@ -3571,16 +3477,6 @@ dependencies = [ "pin-utils", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "ntapi" version = "0.4.1" @@ -3725,16 +3621,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "ordered-multimap" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" -dependencies = [ - "dlv-list", - "hashbrown 0.13.2", -] - [[package]] name = "overload" version = "0.1.1" @@ -3788,51 +3674,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pest" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" -dependencies = [ - "memchr", - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.47", -] - -[[package]] -name = "pest_meta" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" -dependencies = [ - "once_cell", - "pest", - "sha2", -] - [[package]] name = "pin-project" version = "1.1.5" @@ -3963,7 +3804,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit 0.19.15", + "toml_edit", ] [[package]] @@ -4177,18 +4018,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "ron" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" -dependencies = [ - "base64 0.21.5", - "bitflags 2.4.1", - "serde", - "serde_derive", -] - [[package]] name = "rusqlite" version = "0.30.0" @@ -4203,16 +4032,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "rust-ini" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - [[package]] name = "rustc-demangle" version = "0.1.23" @@ -4422,15 +4241,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_spanned" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" -dependencies = [ - "serde", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4495,17 +4305,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -4823,15 +4622,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - [[package]] name = "tinytemplate" version = "1.2.1" @@ -4908,26 +4698,11 @@ dependencies = [ "tracing", ] -[[package]] -name = "toml" -version = "0.8.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit 0.22.13", -] - [[package]] name = "toml_datetime" version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" -dependencies = [ - "serde", -] [[package]] name = "toml_edit" @@ -4940,19 +4715,6 @@ dependencies = [ "winnow 0.5.40", ] -[[package]] -name = "toml_edit" -version = "0.22.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow 0.6.0", -] - [[package]] name = "tower" version = "0.4.13" @@ -5091,12 +4853,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "ucd-trie" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" - [[package]] name = "uluru" version = "3.0.0" @@ -5629,15 +5385,6 @@ dependencies = [ "lzma-sys", ] -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "yansi" version = "0.5.1" diff --git a/deny.toml b/deny.toml index 1e8ed3f49e0..d0b421c9da3 100644 --- a/deny.toml +++ b/deny.toml @@ -8,20 +8,6 @@ # More documentation for the advisories section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html [advisories] -# The path where the advisory database is cloned/fetched into -db-path = "~/.cargo/advisory-db" -# The url(s) of the advisory databases to use -db-urls = ["https://github.com/rustsec/advisory-db"] -# The lint level for security vulnerabilities -vulnerability = "deny" -# The lint level for unmaintained crates -unmaintained = "warn" -# The lint level for crates that have been yanked from their source registry -yanked = "warn" -# The lint level for crates with security notices. Note that as of -# 2019-12-17 there are no security notice advisories in -# https://github.com/rustsec/advisory-db -notice = "warn" ignore = [ # this is `rustls@0.20.9` coming in with `curl`, which doesn't have an update yet. It's only active optionally, not by default. "RUSTSEC-2024-0336", @@ -33,8 +19,6 @@ ignore = [ # More documentation for the licenses section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html [licenses] -# The lint level for crates which do not have a detectable license -unlicensed = "deny" # List of explicitly allowed licenses # See https://spdx.org/licenses/ for list of possible licenses # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. @@ -48,13 +32,6 @@ allow = [ "LicenseRef-ring", "Zlib" ] -# Lint level for licenses considered copyleft -copyleft = "allow" -# Lint level used when no other predicates are matched -# 1. License isn't in the allow or deny lists -# 2. License isn't copyleft -# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" -default = "deny" # The confidence threshold for detecting a license from license text. # The higher the value, the more closely the license text must be to the # canonical license text of a valid SPDX license file. diff --git a/gix-ref/src/lib.rs b/gix-ref/src/lib.rs index 4aeae1b037e..29028b38d62 100644 --- a/gix-ref/src/lib.rs +++ b/gix-ref/src/lib.rs @@ -66,7 +66,7 @@ pub mod store { #[allow(clippy::empty_docs)] pub mod init { - /// Options for use during [initialization](crate::Store::at). + /// Options for use during [initialization](crate::file::Store::at). #[derive(Debug, Copy, Clone, Default)] pub struct Options { /// How to write the ref-log. diff --git a/gix-ref/src/store/file/find.rs b/gix-ref/src/store/file/find.rs index c84eeda6ecc..b148d3f2e73 100644 --- a/gix-ref/src/store/file/find.rs +++ b/gix-ref/src/store/file/find.rs @@ -263,8 +263,8 @@ impl file::Store { format!("Illegal use of reserved Windows device name in \"{}\"", name.as_bstr()), )); } - let ref_path = base.join(relative_path); + let ref_path = base.join(relative_path); match std::fs::File::open(&ref_path) { Ok(mut file) => { let mut buf = Vec::with_capacity(128); diff --git a/gix-ref/src/store/file/transaction/prepare.rs b/gix-ref/src/store/file/transaction/prepare.rs index afb1dd21489..79d86c6cbd5 100644 --- a/gix-ref/src/store/file/transaction/prepare.rs +++ b/gix-ref/src/store/file/transaction/prepare.rs @@ -51,7 +51,7 @@ impl<'s, 'p> Transaction<'s, 'p> { .map_err(Error::from), (None, None) => Ok(None), (maybe_loose, _) => Ok(maybe_loose), - }); + })?; let lock = match &mut change.update.change { Change::Delete { expected, .. } => { let (base, relative_path) = store.reference_path_with_base(change.update.name.as_ref()); @@ -70,7 +70,6 @@ impl<'s, 'p> Transaction<'s, 'p> { .into() }; - let existing_ref = existing_ref?; match (&expected, &existing_ref) { (PreviousValue::MustNotExist, _) => { panic!("BUG: MustNotExist constraint makes no sense if references are to be deleted") @@ -120,7 +119,6 @@ impl<'s, 'p> Transaction<'s, 'p> { }; let mut lock = (!has_global_lock).then(obtain_lock).transpose()?; - let existing_ref = existing_ref?; match (&expected, &existing_ref) { (PreviousValue::Any, _) | (PreviousValue::MustExist, Some(_)) diff --git a/gix/Cargo.toml b/gix/Cargo.toml index 0e83deaaf28..556b64f2112 100644 --- a/gix/Cargo.toml +++ b/gix/Cargo.toml @@ -375,7 +375,6 @@ regex = { version = "1.6.0", optional = true, default-features = false, features parking_lot = "0.12.1" document-features = { version = "0.2.0", optional = true } -config = "0.14.0" [dev-dependencies] pretty_assertions = "1.4.0" From cd4de8327fc195eb862ab6e138f2315a87374f85 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 22 May 2024 12:08:51 +0200 Subject: [PATCH 49/50] update dependencies --- Cargo.lock | 78 +++++++++++++++++++++++++------------ Cargo.toml | 14 +++---- gitoxide-core/Cargo.toml | 4 +- gix-config/tests/Cargo.toml | 6 +-- gix-discover/Cargo.toml | 4 +- gix-filter/Cargo.toml | 2 +- gix-pathspec/Cargo.toml | 4 +- gix-prompt/Cargo.toml | 4 +- gix-transport/Cargo.toml | 2 +- gix/Cargo.toml | 2 +- 10 files changed, 73 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 481394bb111..a6fe354cb97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,23 +62,24 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.5" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" @@ -348,6 +349,12 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "basic-toml" version = "0.1.7" @@ -846,9 +853,9 @@ dependencies = [ [[package]] name = "defer" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "647605a6345d5e89c3950a36a638c56478af9b414c55c6f2477c73b115f9acde" +checksum = "930c7171c8df9fb1782bdf9b918ed9ed2d33d1d22300abb754f9085bc48bf8e8" [[package]] name = "deranged" @@ -907,9 +914,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" dependencies = [ "humantime", "is-terminal", @@ -2639,7 +2646,7 @@ version = "0.42.0" dependencies = [ "async-std", "async-trait", - "base64 0.21.5", + "base64 0.22.1", "blocking", "bstr", "curl", @@ -2937,9 +2944,9 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.8.4" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ "hashbrown 0.14.3", ] @@ -3198,6 +3205,12 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.10.5" @@ -3280,9 +3293,9 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libsqlite3-sys" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" dependencies = [ "cc", "pkg-config", @@ -3342,9 +3355,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" dependencies = [ "value-bag", ] @@ -4020,9 +4033,9 @@ dependencies = [ [[package]] name = "rusqlite" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" dependencies = [ "bitflags 2.4.1", "fallible-iterator", @@ -4162,6 +4175,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scc" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ad2bbb0ae5100a07b7a6f2ed7ab5fd0045551a4c507989b7a620046ea3efdc" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.23" @@ -4187,6 +4209,12 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "sdd" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d" + [[package]] name = "security-framework" version = "2.9.2" @@ -4255,23 +4283,23 @@ dependencies = [ [[package]] name = "serial_test" -version = "2.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" +checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" dependencies = [ - "dashmap", "futures", - "lazy_static", "log", + "once_cell", "parking_lot", + "scc", "serial_test_derive", ] [[package]] name = "serial_test_derive" -version = "2.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" +checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" dependencies = [ "proc-macro2", "quote", @@ -4938,9 +4966,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.4.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a72e1902dde2bd6441347de2b70b7f5d59bf157c6c62f0c44572607a1d55bbe" +checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" [[package]] name = "vcpkg" diff --git a/Cargo.toml b/Cargo.toml index e5e7d42d1d6..4c950e5b68b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,22 +42,22 @@ max = ["max-control", "fast", "gitoxide-core-blocking-client", "http-client-curl ## transports as it uses Rust's HTTP implementation. ## ## As fast as possible, with TUI progress, progress line rendering with auto-configuration, all transports available but less mature pure Rust HTTP implementation, all `ein` tools, CLI colors and local-time support, JSON output, regex support for rev-specs. -max-pure = ["max-control", "gix-features/rustsha1", "gix-features/zlib-rust-backend", "http-client-reqwest", "gitoxide-core-blocking-client" ] +max-pure = ["max-control", "gix-features/rustsha1", "gix-features/zlib-rust-backend", "http-client-reqwest", "gitoxide-core-blocking-client"] ## Like `max`, but with more control for configuration. See the *Package Maintainers* headline for more information. -max-control = ["tracing", "fast-safe", "pretty-cli", "gitoxide-core-tools-query", "gitoxide-core-tools-corpus", "gitoxide-core-tools", "prodash-render-line", "prodash-render-tui", "prodash/render-line-autoconfigure", "gix/revparse-regex" ] +max-control = ["tracing", "fast-safe", "pretty-cli", "gitoxide-core-tools-query", "gitoxide-core-tools-corpus", "gitoxide-core-tools", "prodash-render-line", "prodash-render-tui", "prodash/render-line-autoconfigure", "gix/revparse-regex"] ## All of the good stuff, with less fanciness for smaller binaries. ## ## As fast as possible, progress line rendering, all transports based on their most mature implementation (HTTP), all `ein` tools, CLI colors and local-time support, JSON output. -lean = ["fast", "tracing", "pretty-cli", "http-client-curl", "gitoxide-core-tools-query", "gitoxide-core-tools-corpus", "gitoxide-core-tools", "gitoxide-core-blocking-client", "prodash-render-line" ] +lean = ["fast", "tracing", "pretty-cli", "http-client-curl", "gitoxide-core-tools-query", "gitoxide-core-tools-corpus", "gitoxide-core-tools", "gitoxide-core-blocking-client", "prodash-render-line"] ## The smallest possible build, best suitable for small single-core machines. ## ## This build is essentially limited to local operations without any fanciness. ## ## Optimized for size, no parallelism thus much slower, progress line rendering. -small = ["pretty-cli", "gix-features/rustsha1", "gix-features/zlib-rust-backend", "prodash-render-line", "is-terminal" ] +small = ["pretty-cli", "gix-features/rustsha1", "gix-features/zlib-rust-backend", "prodash-render-line", "is-terminal"] ## Like lean, but uses Rusts async implementations for networking. ## @@ -107,12 +107,12 @@ fast = ["gix/max-performance", "gix/comfort"] fast-safe = ["gix/max-performance-safe", "gix/comfort"] ## Enable tracing in `gitoxide-core`. -tracing = ["dep:tracing-forest", "dep:tracing-subscriber", "dep:tracing", "gix-features/tracing", "gix-features/tracing-detail" ] +tracing = ["dep:tracing-forest", "dep:tracing-subscriber", "dep:tracing", "gix-features/tracing", "gix-features/tracing-detail"] ## Use `clap` 3.0 to build the prettiest, best documented and most user-friendly CLI at the expense of binary size. ## Provides a terminal user interface for detailed and exhaustive progress. ## Provides a line renderer for leaner progress display, without the need for a full-blown TUI. -pretty-cli = [ "gitoxide-core/serde", "prodash/progress-tree", "prodash/progress-tree-log", "prodash/local-time", "env_logger/humantime", "env_logger/color", "env_logger/auto-color" ] +pretty-cli = ["gitoxide-core/serde", "prodash/progress-tree", "prodash/progress-tree-log", "prodash/local-time", "env_logger/humantime", "env_logger/color", "env_logger/auto-color"] ## The `--verbose` flag will be powered by an interactive progress mechanism that doubles as log as well as interactive progress ## that appears after a short duration. @@ -285,9 +285,7 @@ members = [ "gix-worktree-stream", "gix-revwalk", "gix-fsck", - "tests/tools", - "gix-diff/tests", "gix-pack/tests", "gix-odb/tests", diff --git a/gitoxide-core/Cargo.toml b/gitoxide-core/Cargo.toml index aa53e19e5c9..3cc9a67fbf0 100644 --- a/gitoxide-core/Cargo.toml +++ b/gitoxide-core/Cargo.toml @@ -23,7 +23,7 @@ estimate-hours = ["dep:fs-err", "dep:crossbeam-channel", "dep:smallvec"] query = ["dep:rusqlite"] ## Run algorithms on a corpus of repositories and store their results for later comparison and intelligence gathering. ## *Note that* `organize` we need for finding git repositories fast. -corpus = [ "dep:rusqlite", "dep:sysinfo", "organize", "dep:crossbeam-channel", "dep:serde_json", "dep:tracing-forest", "dep:tracing-subscriber", "tracing", "dep:parking_lot" ] +corpus = ["dep:rusqlite", "dep:sysinfo", "organize", "dep:crossbeam-channel", "dep:serde_json", "dep:tracing-forest", "dep:tracing-subscriber", "tracing", "dep:parking_lot"] ## The ability to create archives from virtual worktrees, similar to `git archive`. archive = ["dep:gix-archive-for-configuration-only", "gix/worktree-archive"] @@ -77,7 +77,7 @@ crossbeam-channel = { version = "0.5.6", optional = true } smallvec = { version = "1.10.0", optional = true } # for 'query' and 'corpus' -rusqlite = { version = "0.30.0", optional = true, features = ["bundled"] } +rusqlite = { version = "0.31.0", optional = true, features = ["bundled"] } # for 'corpus' parking_lot = { version = "0.12.1", optional = true } diff --git a/gix-config/tests/Cargo.toml b/gix-config/tests/Cargo.toml index 992589ad820..0448769115c 100644 --- a/gix-config/tests/Cargo.toml +++ b/gix-config/tests/Cargo.toml @@ -19,13 +19,13 @@ name = "mem" path = "mem.rs" [dev-dependencies] -gix-config = { path = ".."} -gix-testtools = { path = "../../tests/tools"} +gix-config = { path = ".." } +gix-testtools = { path = "../../tests/tools" } gix = { path = "../../gix", default-features = false } gix-ref = { path = "../../gix-ref" } gix-path = { path = "../../gix-path" } gix-sec = { path = "../../gix-sec" } -serial_test = { version = "2.0.0", default-features = false } +serial_test = { version = "3.1.0", default-features = false } bstr = { version = "1.3.0", default-features = false, features = ["std"] } bytesize = "1.3.0" diff --git a/gix-discover/Cargo.toml b/gix-discover/Cargo.toml index 43c5ad26f63..a474d56e652 100644 --- a/gix-discover/Cargo.toml +++ b/gix-discover/Cargo.toml @@ -27,11 +27,11 @@ dunce = "1.0.3" [dev-dependencies] gix-testtools = { path = "../tests/tools" } -serial_test = { version = "2.0.0", default-features = false } +serial_test = { version = "3.1.0", default-features = false } is_ci = "1.1.1" [target.'cfg(target_os = "macos")'.dev-dependencies] -defer = "0.1.0" +defer = "0.2.1" [target.'cfg(any(unix, windows))'.dev-dependencies] tempfile = "3.2.0" diff --git a/gix-filter/Cargo.toml b/gix-filter/Cargo.toml index dfaa2d85115..766dbadc209 100644 --- a/gix-filter/Cargo.toml +++ b/gix-filter/Cargo.toml @@ -30,6 +30,6 @@ smallvec = "1.10.0" [dev-dependencies] -serial_test = { version = "2.0.0", default-features = false } +serial_test = { version = "3.1.0", default-features = false } gix-testtools = { path = "../tests/tools" } gix-worktree = { path = "../gix-worktree", default-features = false, features = ["attributes"] } diff --git a/gix-pathspec/Cargo.toml b/gix-pathspec/Cargo.toml index 08f8d700d57..5fe702c59cd 100644 --- a/gix-pathspec/Cargo.toml +++ b/gix-pathspec/Cargo.toml @@ -18,11 +18,11 @@ gix-path = { version = "^0.10.7", path = "../gix-path" } gix-attributes = { version = "^0.22.2", path = "../gix-attributes" } gix-config-value = { version = "^0.14.6", path = "../gix-config-value" } -bstr = { version = "1.3.0", default-features = false, features = ["std"]} +bstr = { version = "1.3.0", default-features = false, features = ["std"] } bitflags = "2" thiserror = "1.0.26" [dev-dependencies] gix-testtools = { path = "../tests/tools" } once_cell = "1.12.0" -serial_test = "2.0.0" +serial_test = "3.1.1" diff --git a/gix-prompt/Cargo.toml b/gix-prompt/Cargo.toml index 66f76893dd2..a6ac0893d06 100644 --- a/gix-prompt/Cargo.toml +++ b/gix-prompt/Cargo.toml @@ -23,6 +23,6 @@ parking_lot = "0.12.1" rustix = { version = "0.38.4", features = ["termios"] } [dev-dependencies] -gix-testtools = { path = "../tests/tools"} -serial_test = { version = "2.0.0", default-features = false } +gix-testtools = { path = "../tests/tools" } +serial_test = { version = "3.1.0", default-features = false } expectrl = "0.7.0" diff --git a/gix-transport/Cargo.toml b/gix-transport/Cargo.toml index 2e907e10f40..abecff15d49 100644 --- a/gix-transport/Cargo.toml +++ b/gix-transport/Cargo.toml @@ -102,7 +102,7 @@ futures-lite = { workspace = true, optional = true } pin-project-lite = { version = "0.2.6", optional = true } # for http-client -base64 = { version = "0.21.0", optional = true } +base64 = { version = "0.22.1", optional = true } # for http-client-curl. Additional configuration should be performed on higher levels of the dependency tree. curl = { workspace = true, optional = true } diff --git a/gix/Cargo.toml b/gix/Cargo.toml index 556b64f2112..0eb88d0648b 100644 --- a/gix/Cargo.toml +++ b/gix/Cargo.toml @@ -382,7 +382,7 @@ gix-testtools = { path = "../tests/tools" } is_ci = "1.1.1" anyhow = "1" walkdir = "2.3.2" -serial_test = { version = "2.0.0", default-features = false } +serial_test = { version = "3.1.0", default-features = false } async-std = { version = "1.12.0", features = ["attributes"] } [package.metadata.docs.rs] From e955770c0b5c06a6c8518a06df4aa0cc3b506f16 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 22 May 2024 12:24:18 +0200 Subject: [PATCH 50/50] fix: symlink support for `zip` archives This started working with the upgradde of the `zip` crate. --- Cargo.lock | 104 +++++++++++++++++++++++++++-------- deny.toml | 1 + gix-archive/Cargo.toml | 16 +++--- gix-archive/src/write.rs | 6 +- gix-archive/tests/archive.rs | 6 +- 5 files changed, 99 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a6fe354cb97..e43a165752f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,6 +115,15 @@ version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9d19de80eff169429ac1e9f48fffb163916b448a44e8e046186232046d9e1f9" +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arc-swap" version = "1.6.0" @@ -426,15 +435,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytes" @@ -647,9 +650,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -748,12 +751,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.18" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" -dependencies = [ - "cfg-if", -] +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" @@ -866,6 +866,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + [[package]] name = "diff" version = "0.1.13" @@ -882,6 +893,17 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + [[package]] name = "document-features" version = "0.2.8" @@ -3353,6 +3375,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" version = "0.4.21" @@ -3509,6 +3537,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.17" @@ -4378,6 +4412,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.9" @@ -4621,13 +4661,14 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", "libc", + "num-conv", "num_threads", "powerfmt", "serde", @@ -4643,10 +4684,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ + "num-conv", "time-core", ] @@ -5447,13 +5489,31 @@ checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" [[package]] name = "zip" -version = "0.6.6" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +checksum = "1b7a5a9285bd4ee13bdeb3f8a4917eb46557e53f270c783849db8bef37b0ad00" dependencies = [ - "byteorder", + "arbitrary", "crc32fast", "crossbeam-utils", + "displaydoc", "flate2", + "indexmap", + "thiserror", "time", + "zopfli", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", ] diff --git a/deny.toml b/deny.toml index d0b421c9da3..01d4cb436f6 100644 --- a/deny.toml +++ b/deny.toml @@ -25,6 +25,7 @@ ignore = [ allow = [ "Apache-2.0", "BSD-3-Clause", + "BSL-1.0", "MIT", "MIT-0", "ISC", diff --git a/gix-archive/Cargo.toml b/gix-archive/Cargo.toml index 81477eafc07..c08fc5a8264 100644 --- a/gix-archive/Cargo.toml +++ b/gix-archive/Cargo.toml @@ -31,7 +31,7 @@ gix-path = { version = "^0.10.7", path = "../gix-path", optional = true } gix-date = { version = "^0.8.6", path = "../gix-date" } flate2 = { version = "1.0.26", optional = true } -zip = { version = "0.6.6", optional = true, default-features = false, features = ["deflate", "time"] } +zip = { version = "1.3.1", optional = true, default-features = false, features = ["deflate", "time"] } time = { version = "0.3.23", optional = true, default-features = false, features = ["std"] } thiserror = "1.0.26" @@ -42,13 +42,13 @@ tar = { version = "0.4.38", optional = true } document-features = { version = "0.2.0", optional = true } [dev-dependencies] -gix-testtools = { path = "../tests/tools"} -gix-odb = { path = "../gix-odb"} -gix-worktree = { path = "../gix-worktree", default-features = false, features = ["attributes"]} -gix-hash = { path = "../gix-hash"} -gix-attributes = { path = "../gix-attributes"} -gix-object = { path = "../gix-object"} -gix-filter = { path = "../gix-filter"} +gix-testtools = { path = "../tests/tools" } +gix-odb = { path = "../gix-odb" } +gix-worktree = { path = "../gix-worktree", default-features = false, features = ["attributes"] } +gix-hash = { path = "../gix-hash" } +gix-attributes = { path = "../gix-attributes" } +gix-object = { path = "../gix-object" } +gix-filter = { path = "../gix-filter" } [package.metadata.docs.rs] all-features = true diff --git a/gix-archive/src/write.rs b/gix-archive/src/write.rs index 5c5dc885eca..244b40554e0 100644 --- a/gix-archive/src/write.rs +++ b/gix-archive/src/write.rs @@ -126,7 +126,7 @@ where NextFn: FnMut(&mut Stream) -> Result>, gix_worktree_stream::entry::Error>, { let compression_level = match opts.format { - Format::Zip { compression_level } => compression_level.map(|lvl| lvl as i32), + Format::Zip { compression_level } => compression_level.map(|lvl| lvl as i64), _other => return write_stream(stream, next_entry, out, opts), }; @@ -161,10 +161,10 @@ fn append_zip_entry( mut entry: gix_worktree_stream::Entry<'_>, buf: &mut Vec, mtime: zip::DateTime, - compression_level: Option, + compression_level: Option, tree_prefix: Option<&bstr::BString>, ) -> Result<(), Error> { - let file_opts = zip::write::FileOptions::default() + let file_opts = zip::write::FileOptions::<'_, ()>::default() .compression_method(zip::CompressionMethod::Deflated) .compression_level(compression_level) .large_file(entry.bytes_remaining().map_or(true, |len| len > u32::MAX as usize)) diff --git a/gix-archive/tests/archive.rs b/gix-archive/tests/archive.rs index a1b9920bb61..125d3cd2826 100644 --- a/gix-archive/tests/archive.rs +++ b/gix-archive/tests/archive.rs @@ -208,7 +208,11 @@ mod from_tree { ); let mut link = ar.by_name("prefix/symlink-to-a")?; assert!(!link.is_dir()); - assert!(link.is_file(), "no symlink differentiation"); + assert_eq!( + link.is_symlink(), + cfg!(not(windows)), + "symlinks are supported as well, but only on Unix" + ); assert_eq!( link.unix_mode(), Some(if cfg!(windows) { 0o100644 } else { 0o120644 }),