Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 9 additions & 11 deletions gix-attributes/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ mod error {
AttributeName { line_number: usize, attribute: BString },
#[error("Macro in line {line_number} has an invalid name: {macro_name}")]
MacroName { line_number: usize, macro_name: BString },
#[error("Could not unquote attributes line")]
Unquote(#[from] gix_quote::ansi_c::undo::Error),
}
}
pub use error::Error;
Expand Down Expand Up @@ -122,16 +120,16 @@ fn parse_line(line: &BStr, line_number: usize) -> Option<Result<(Kind, Iter<'_>,
return None;
}

let (line, attrs): (Cow<'_, _>, _) = if line.starts_with(b"\"") {
let (unquoted, consumed) = match gix_quote::ansi_c::undo(line) {
Ok(res) => res,
Err(err) => return Some(Err(err.into())),
};
(unquoted, &line[consumed..])
} else {
line.find_byteset(BLANKS)
let unquoted = line
.starts_with(b"\"")
.then(|| gix_quote::ansi_c::undo(line).ok())
.flatten();
let (line, attrs): (Cow<'_, _>, _) = match unquoted {
Some((unquoted, consumed)) => (unquoted, &line[consumed..]),
None => line
.find_byteset(BLANKS)
.map(|pos| (line[..pos].as_bstr().into(), line[pos..].as_bstr()))
.unwrap_or((line.into(), [].as_bstr()))
.unwrap_or((line.into(), [].as_bstr())),
};

let kind_res = match line.strip_prefix(b"[attr]").filter(|name| !name.is_empty()) {
Expand Down
20 changes: 17 additions & 3 deletions gix-attributes/tests/attributes/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,23 @@ fn exclamation_marks_must_be_escaped_or_error_unlike_gitignore() {
}

#[test]
fn invalid_escapes_in_quotes_are_an_error() {
assert!(matches!(try_line(r#""\!hello""#), Err(parse::Error::Unquote(_))));
assert!(lenient_lines(r#""\!hello""#).is_empty());
fn broken_quoting_falls_back_to_the_raw_text() {
assert_eq!(
line(r#""\!hello""#),
(pattern(r#""\!hello""#, Mode::NO_SUB_DIR, Some(1)), vec![], 1),
"an invalid escape leaves the quotes in place, so the leading `!` no longer negates, \
and the backslash goes on to escape it for the matcher"
);
assert_eq!(
line(r#""abc"#),
(pattern(r#""abc"#, Mode::NO_SUB_DIR, None), vec![], 1),
"so does a quote that is never closed"
);
assert_eq!(
line(r#""abc def"#),
(pattern(r#""abc"#, Mode::NO_SUB_DIR, None), vec![set("def")], 1),
"the raw text is then split on blanks like any unquoted pattern"
);
}

#[test]
Expand Down
3 changes: 2 additions & 1 deletion gix-odb/src/alternate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use gix_path::realpath::MAX_SYMLINKS;

///
pub mod parse;
pub use parse::function::parse;

/// Returned by [`resolve()`]
#[derive(thiserror::Error, Debug)]
Expand Down Expand Up @@ -67,7 +68,7 @@ pub fn resolve(objects_directory: PathBuf, current_dir: &std::path::Path) -> Res
seen.push((dir_canonicalized, parent_idx));
match fs::read(dir.join("info").join("alternates")) {
Ok(input) => {
for path in parse::content(&input)?.into_iter().rev() {
for path in parse(&input)?.into_iter().rev() {
dirs.push((Some(idx), objects_directory.join(path)));
}
}
Expand Down
62 changes: 40 additions & 22 deletions gix-odb/src/alternate/parse.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,51 @@
use std::{borrow::Cow, path::PathBuf};

use gix_object::bstr::ByteSlice;

/// Returned as part of [`crate::alternate::Error::Parse`]
#[derive(thiserror::Error, Debug)]
#[expect(missing_docs)]
pub enum Error {
#[error("Could not obtain an object path for the alternate directory '{}'", String::from_utf8_lossy(.0))]
PathConversion(Vec<u8>),
#[error("Could not unquote alternate path")]
Unquote(#[from] gix_quote::ansi_c::undo::Error),
}

pub(crate) fn content(input: &[u8]) -> Result<Vec<PathBuf>, Error> {
let mut out = Vec::new();
for line in input.split(|b| *b == b'\n') {
let line = line.as_bstr();
if line.is_empty() || line.starts_with(b"#") {
continue;
}
out.push(
gix_path::try_from_bstr(if line.starts_with(b"\"") {
gix_quote::ansi_c::undo(line)?.0
pub(super) mod function {
use super::Error;
use std::{borrow::Cow, path::PathBuf};

use gix_object::bstr::ByteSlice;

/// Parse the raw contents of an `objects/info/alternates` file from `input` into paths.
///
/// Empty entries and comments are ignored. Entries beginning with `"` use Git's C-style quoting,
/// which permits literal newlines in paths. Invalid quoting falls back to the raw entry.
pub fn parse(mut input: &[u8]) -> Result<Vec<PathBuf>, Error> {
let mut out = Vec::new();
while !input.is_empty() {
let entry = input.as_bstr();
let end_of_line = || entry.find_byte(b'\n').unwrap_or(entry.len());
let (path, consumed) = if entry.starts_with(b"#") {
(None, end_of_line())
} else {
Cow::Borrowed(line)
})
.map_err(|_| Error::PathConversion(line.to_vec()))?
.into_owned(),
);
// Like Git, try unquoting before treating a newline as the next separator.
match entry.starts_with(b"\"").then(|| gix_quote::ansi_c::undo(entry)) {
Some(Ok((unquoted, consumed))) => (Some(unquoted), consumed),
_ => {
let consumed = end_of_line();
(Some(Cow::Borrowed(entry[..consumed].as_bstr())), consumed)
}
}
};
let original = &entry[..consumed];
let maybe_nl = usize::from(consumed < input.len());
input = &input[consumed + maybe_nl..];

let Some(path) = path.filter(|path| !path.is_empty()) else {
continue;
};
out.push(
gix_path::try_from_bstr(path)
.map_err(|_| Error::PathConversion(original.to_vec()))?
.into_owned(),
);
}
Ok(out)
}
Ok(out)
}
31 changes: 31 additions & 0 deletions gix-odb/tests/odb/alternate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,37 @@ use std::{

use gix_odb::alternate;

mod parse {
use std::path::PathBuf;

use gix_odb::alternate;

#[test]
fn a_quote_that_is_never_closed_is_used_as_a_literal_path() {
assert_eq!(
alternate::parse(br#""unterminated"#).expect("no path conversion issue"),
vec![PathBuf::from(r#""unterminated"#)],
"broken quoting falls back to the raw line"
);
}

#[test]
fn a_properly_quoted_path_is_unquoted() {
assert_eq!(
alternate::parse(br#""quoted\tpath""#).expect("no path conversion issue"),
vec![PathBuf::from("quoted\tpath")]
);
}

#[test]
fn a_quoted_path_may_contain_the_line_separator() {
assert_eq!(
alternate::parse(b"\"quoted\npath\"\nnext").expect("no path conversion issue"),
vec![PathBuf::from("quoted\npath"), PathBuf::from("next")],
"Git looks for a closing quote before treating a newline as the next separator"
);
}
}
pub fn alternate(
objects_at: impl Into<PathBuf>,
objects_to: impl Into<PathBuf>,
Expand Down
7 changes: 4 additions & 3 deletions gix-quote/src/ansi_c.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use gix_error::{ErrorExt, OptionExt, ResultExt, ValidationError};
/// quotation, otherwise a new unquoted string will always be allocated.
/// The amount of consumed bytes allow to pass strings that start with a quote, and skip all quoted text for additional processing
///
/// A quote that is never closed is an error.
/// See [the tests][tests] for quotation examples.
///
/// [tests]: https://github.com/GitoxideLabs/gitoxide/blob/64872690e60efdd9267d517f4d9971eecd3b875c/gix-quote/tests/quote.rs#L57-L74
Expand Down Expand Up @@ -93,9 +94,9 @@ pub fn undo(input: &BStr) -> Result<(Cow<'_, BStr>, usize), undo::Error> {
}
}
None => {
out.extend_from_slice(input);
consumed += input.len();
break;
return Err(
ValidationError::new_with_input("Missing closing quote in quoted string", original).raise(),
);
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions gix-quote/tests/quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ mod ansi_c {
assert_eq!(&input[consumed..], " out of quote");
}

#[test]
fn a_quote_that_is_never_closed_is_an_error() {
for unterminated in [r#"""#, r#""abc"#, r#""abc def"#, r#""abc\"#, r#""\""#] {
assert!(
ansi_c::undo(unterminated.into()).is_err(),
"{unterminated:?} should not parse"
);
}
}

#[test]
fn fuzzed() {
for invalid in ["\"\\", "\"Q\u{2}QT\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\0\0\\"] {
Expand Down
Loading