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
2 changes: 1 addition & 1 deletion gix-ref/tests/namespace/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ mod expand {
fn repeated_slashes_are_invalid() {
assert!(matches!(
gix_ref::namespace::expand("foo//bar").expect_err("empty invalid"),
gix_validate::reference::name::Error::RepeatedSlash
gix_validate::reference::name::Error::Tag(gix_validate::tag::name::Error::RepeatedSlash)
));
}
}
4 changes: 3 additions & 1 deletion gix-refspec/tests/parse/invalid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ fn empty() {
fn empty_component() {
assert!(matches!(
try_parse("refs/heads/test:refs/remotes//test", Operation::Fetch).unwrap_err(),
Error::ReferenceName(gix_validate::reference::name::Error::RepeatedSlash)
Error::ReferenceName(gix_validate::reference::name::Error::Tag(
gix_validate::tag::name::Error::RepeatedSlash
))
));
}

Expand Down
70 changes: 38 additions & 32 deletions gix-validate/src/reference.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use bstr::{BStr, BString, ByteSlice};

///
#[allow(clippy::empty_docs)]
pub mod name {
Expand All @@ -11,12 +13,6 @@ pub mod name {
Tag(#[from] crate::tag::name::Error),
#[error("Standalone references must be all uppercased, like 'HEAD'")]
SomeLowercase,
#[error("A reference name must not start with a slash '/'")]
StartsWithSlash,
#[error("Multiple slashes in a row are not allowed as they may change the reference's meaning")]
RepeatedSlash,
#[error("Path components must not start with '.'")]
StartsWithDot,
}

impl From<Infallible> for Error {
Expand All @@ -26,49 +22,59 @@ pub mod name {
}
}

use bstr::BStr;

/// Validate a reference name running all the tests in the book. This disallows lower-case references like `lower`, but also allows
/// ones like `HEAD`, and `refs/lower`.
pub fn name(path: &BStr) -> Result<&BStr, name::Error> {
validate(path, Mode::Complete)
match validate(path, Mode::Complete)? {
None => Ok(path),
Some(_) => {
unreachable!("Without sanitization, there is no chance a sanitized version is returned.")
}
}
}

/// Validate a partial reference name. As it is assumed to be partial, names like `some-name` is allowed
/// even though these would be disallowed with when using [`name()`].
pub fn name_partial(path: &BStr) -> Result<&BStr, name::Error> {
validate(path, Mode::Partial)
match validate(path, Mode::Partial)? {
None => Ok(path),
Some(_) => {
unreachable!("Without sanitization, there is no chance a sanitized version is returned.")
}
}
}

/// The infallible version of [`name_partial()`] which instead of failing, alters `path` and returns it to be a valid
/// partial name, which would also pass [`name_partial()`].
///
/// Note that an empty `path` is replaced with a `-` in order to be valid.
pub fn name_partial_or_sanitize(path: &BStr) -> BString {
validate(path, Mode::PartialSanitize)
.expect("BUG: errors cannot happen as any issue is fixed instantly")
.expect("we always rebuild the path")
}

enum Mode {
Complete,
Partial,
/// like Partial, but instead of failing, a sanitized version is returned.
PartialSanitize,
}

fn validate(path: &BStr, mode: Mode) -> Result<&BStr, name::Error> {
crate::tag::name(path)?;
if path[0] == b'/' {
return Err(name::Error::StartsWithSlash);
}
let mut previous = 0;
let mut saw_slash = false;
for byte in path.iter() {
match *byte {
b'/' if previous == b'/' => return Err(name::Error::RepeatedSlash),
b'.' if previous == b'/' => return Err(name::Error::StartsWithDot),
_ => {}
}

if *byte == b'/' {
saw_slash = true;
}
previous = *byte;
}

fn validate(path: &BStr, mode: Mode) -> Result<Option<BString>, name::Error> {
let out = crate::tag::name_inner(
path,
match mode {
Mode::Complete | Mode::Partial => crate::tag::Mode::Validate,
Mode::PartialSanitize => crate::tag::Mode::Sanitize,
},
)?;
if let Mode::Complete = mode {
if !saw_slash && !path.iter().all(|c| c.is_ascii_uppercase() || *c == b'_') {
let input = out.as_ref().map_or(path, |b| b.as_bstr());
let saw_slash = input.find_byte(b'/').is_some();
if !saw_slash && !input.iter().all(|c| c.is_ascii_uppercase() || *c == b'_') {
return Err(name::Error::SomeLowercase);
}
}
Ok(path)
Ok(out)
}
128 changes: 113 additions & 15 deletions gix-validate/src/tag.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use bstr::BStr;
use bstr::{BStr, BString};

///
#[allow(clippy::empty_docs)]
Expand All @@ -11,8 +11,12 @@ pub mod name {
pub enum Error {
#[error("A ref must not contain invalid bytes or ascii control characters: {byte:?}")]
InvalidByte { byte: BString },
#[error("A reference name must not start with a slash '/'")]
StartsWithSlash,
#[error("Multiple slashes in a row are not allowed as they may change the reference's meaning")]
RepeatedSlash,
#[error("A ref must not contain '..' as it may be mistaken for a range")]
DoubleDot,
RepeatedDot,
#[error("A ref must not end with '.lock'")]
LockFileSuffix,
#[error("A ref must not contain '@{{' which is a part of a ref-log")]
Expand All @@ -33,36 +37,130 @@ pub mod name {
/// Assure the given `input` resemble a valid git tag name, which is returned unchanged on success.
/// Tag names are provided as names, lik` v1.0` or `alpha-1`, without paths.
pub fn name(input: &BStr) -> Result<&BStr, name::Error> {
match name_inner(input, Mode::Validate)? {
None => Ok(input),
Some(_) => {
unreachable!("When validating, the input isn't changed")
}
}
}

#[derive(Eq, PartialEq)]
pub(crate) enum Mode {
Sanitize,
Validate,
}

pub(crate) fn name_inner(input: &BStr, mode: Mode) -> Result<Option<BString>, name::Error> {
let mut out: Option<BString> =
matches!(mode, Mode::Sanitize).then(|| BString::from(Vec::with_capacity(input.len())));
if input.is_empty() {
return Err(name::Error::Empty);
return if let Some(mut out) = out {
out.push(b'-');
Ok(Some(out))
} else {
Err(name::Error::Empty)
};
}
if *input.last().expect("non-empty") == b'/' {
if *input.last().expect("non-empty") == b'/' && out.is_none() {
return Err(name::Error::EndsWithSlash);
}
if input.first() == Some(&b'/') && out.is_none() {
return Err(name::Error::StartsWithSlash);
}

let mut previous = 0;
for byte in input.iter() {
match byte {
b'\\' | b'^' | b':' | b'[' | b'?' | b' ' | b'~' | b'\0'..=b'\x1F' | b'\x7F' => {
return Err(name::Error::InvalidByte {
byte: (&[*byte][..]).into(),
})
if let Some(out) = out.as_mut() {
out.push(b'-');
} else {
return Err(name::Error::InvalidByte {
byte: (&[*byte][..]).into(),
});
}
}
b'*' => {
if let Some(out) = out.as_mut() {
out.push(b'-');
} else {
return Err(name::Error::Asterisk);
}
}

b'.' if previous == b'.' => {
if out.is_none() {
return Err(name::Error::RepeatedDot);
}
}
b'.' if previous == b'/' => {
if let Some(out) = out.as_mut() {
out.push(b'-');
} else {
return Err(name::Error::StartsWithDot);
}
}
b'{' if previous == b'@' => {
if let Some(out) = out.as_mut() {
out.push(b'-');
} else {
return Err(name::Error::ReflogPortion);
}
}
b'/' if previous == b'/' => {
if out.is_none() {
return Err(name::Error::RepeatedSlash);
}
}
b'.' if previous == b'/' => {
if let Some(out) = out.as_mut() {
out.push(b'-');
} else {
return Err(name::Error::StartsWithDot);
}
}
c => {
if let Some(out) = out.as_mut() {
out.push(*c)
}
}
b'*' => return Err(name::Error::Asterisk),
b'.' if previous == b'.' => return Err(name::Error::DoubleDot),
b'{' if previous == b'@' => return Err(name::Error::ReflogPortion),
_ => {}
}
previous = *byte;
}

if let Some(out) = out.as_mut() {
while out.last() == Some(&b'/') {
out.pop();
}
while out.first() == Some(&b'/') {
out.remove(0);
}
}
if input[0] == b'.' {
return Err(name::Error::StartsWithDot);
if let Some(out) = out.as_mut() {
out[0] = b'-';
} else {
return Err(name::Error::StartsWithDot);
}
}
if input[input.len() - 1] == b'.' {
return Err(name::Error::EndsWithDot);
if let Some(out) = out.as_mut() {
let last = out.len() - 1;
out[last] = b'-';
} else {
return Err(name::Error::EndsWithDot);
}
}
if input.ends_with(b".lock") {
return Err(name::Error::LockFileSuffix);
if let Some(out) = out.as_mut() {
while out.ends_with(b".lock") {
let len_without_suffix = out.len() - b".lock".len();
out.truncate(len_without_suffix);
}
} else {
return Err(name::Error::LockFileSuffix);
}
}
Ok(input)
Ok(out)
}
Loading