use std::fmt;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct RefnameFormat {
pub allow_onelevel: bool,
pub refspec_pattern: bool,
}
impl RefnameFormat {
pub const STRICT: Self = Self {
allow_onelevel: false,
refspec_pattern: false,
};
pub const ALLOW_ONELEVEL: Self = Self {
allow_onelevel: true,
refspec_pattern: false,
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RefnameFormatError {
LoneAt,
EmptyComponent,
ComponentStartsWithDot,
ComponentEndsWithLock,
DoubleDot,
AtBrace,
ForbiddenByte(u8),
Asterisk,
EndsWithDot,
OneLevel,
}
impl fmt::Display for RefnameFormatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::LoneAt => f.write_str("ref name must not be the single character '@'"),
Self::EmptyComponent => f.write_str(
"ref name must not be empty or have a leading, trailing, or consecutive slash",
),
Self::ComponentStartsWithDot => {
f.write_str("ref name component must not start with '.'")
}
Self::ComponentEndsWithLock => {
f.write_str("ref name component must not end with '.lock'")
}
Self::DoubleDot => f.write_str("ref name must not contain '..'"),
Self::AtBrace => f.write_str("ref name must not contain '@{'"),
Self::ForbiddenByte(b' ') => f.write_str("ref name must not contain a space"),
Self::ForbiddenByte(byte) if byte.is_ascii_control() => write!(
f,
"ref name must not contain control character 0x{byte:02x}"
),
Self::ForbiddenByte(byte) => {
write!(f, "ref name must not contain '{}'", char::from(*byte))
}
Self::Asterisk => {
f.write_str("ref name must not contain '*' (a refspec pattern allows exactly one)")
}
Self::EndsWithDot => f.write_str("ref name must not end with '.'"),
Self::OneLevel => f.write_str("ref name must have at least two components"),
}
}
}
impl std::error::Error for RefnameFormatError {}
pub fn check_refname_format(name: &[u8], format: RefnameFormat) -> Result<(), RefnameFormatError> {
if name == b"@" {
return Err(RefnameFormatError::LoneAt);
}
let mut pattern_allowed = format.refspec_pattern;
let mut components = 0usize;
for component in name.split(|&byte| byte == b'/') {
check_refname_component(component, &mut pattern_allowed)?;
components += 1;
}
if name.last() == Some(&b'.') {
return Err(RefnameFormatError::EndsWithDot);
}
if !format.allow_onelevel && components < 2 {
return Err(RefnameFormatError::OneLevel);
}
Ok(())
}
fn check_refname_component(
component: &[u8],
pattern_allowed: &mut bool,
) -> Result<(), RefnameFormatError> {
let mut last = 0u8;
for &byte in component {
match byte {
b'.' if last == b'.' => return Err(RefnameFormatError::DoubleDot),
b'{' if last == b'@' => return Err(RefnameFormatError::AtBrace),
b'*' => {
if !*pattern_allowed {
return Err(RefnameFormatError::Asterisk);
}
*pattern_allowed = false;
}
0x00..=0x20 | 0x7f | b'~' | b'^' | b':' | b'?' | b'[' | b'\\' => {
return Err(RefnameFormatError::ForbiddenByte(byte));
}
_ => {}
}
last = byte;
}
if component.is_empty() {
return Err(RefnameFormatError::EmptyComponent);
}
if component.first() == Some(&b'.') {
return Err(RefnameFormatError::ComponentStartsWithDot);
}
if component.ends_with(b".lock") {
return Err(RefnameFormatError::ComponentEndsWithLock);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn check(name: &[u8]) -> Result<(), RefnameFormatError> {
check_refname_format(name, RefnameFormat::STRICT)
}
#[test]
fn non_ascii_whitespace_is_an_ordinary_byte() {
assert_eq!(check("refs/heads/\u{00A0}edge\u{00A0}".as_bytes()), Ok(()));
assert_eq!(check("\u{3000}refs/heads/x\u{2028}".as_bytes()), Ok(()));
assert_eq!(
check(b"refs/heads/ edge"),
Err(RefnameFormatError::ForbiddenByte(b' '))
);
}
#[test]
fn non_utf8_bytes_are_ordinary() {
assert_eq!(check(b"refs/heads/\xff\xfe"), Ok(()));
}
#[test]
fn nul_is_rejected_as_a_control_byte() {
assert_eq!(
check(b"refs/heads/a\0b"),
Err(RefnameFormatError::ForbiddenByte(0))
);
}
#[test]
fn structural_rules() {
assert_eq!(
check_refname_format(b"@", RefnameFormat::ALLOW_ONELEVEL),
Err(RefnameFormatError::LoneAt)
);
assert_eq!(check(b""), Err(RefnameFormatError::EmptyComponent));
assert_eq!(check(b"refs//a"), Err(RefnameFormatError::EmptyComponent));
assert_eq!(check(b"refs/a/"), Err(RefnameFormatError::EmptyComponent));
assert_eq!(
check(b"refs/.a"),
Err(RefnameFormatError::ComponentStartsWithDot)
);
assert_eq!(
check(b"refs/a.lock/b"),
Err(RefnameFormatError::ComponentEndsWithLock)
);
assert_eq!(check(b"refs/a..b"), Err(RefnameFormatError::DoubleDot));
assert_eq!(check(b"refs/a@{b"), Err(RefnameFormatError::AtBrace));
assert_eq!(check(b"refs/a."), Err(RefnameFormatError::EndsWithDot));
assert_eq!(check(b"refs/a./b"), Ok(()));
assert_eq!(check(b"HEAD"), Err(RefnameFormatError::OneLevel));
assert_eq!(
check_refname_format(b"HEAD", RefnameFormat::ALLOW_ONELEVEL),
Ok(())
);
}
#[test]
fn refspec_pattern_allows_exactly_one_asterisk() {
let pattern = RefnameFormat {
allow_onelevel: false,
refspec_pattern: true,
};
assert_eq!(check(b"refs/heads/*"), Err(RefnameFormatError::Asterisk));
assert_eq!(check_refname_format(b"refs/heads/*", pattern), Ok(()));
assert_eq!(
check_refname_format(b"refs/*/a*", pattern),
Err(RefnameFormatError::Asterisk)
);
}
}