#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RefName(Vec<u8>);
impl AsRef<[u8]> for RefName {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
impl RefName {
pub fn new(bytes: impl AsRef<[u8]>) -> Result<Self, InvalidRefName> {
let bytes = bytes.as_ref();
if bytes == b"HEAD" {
return Ok(Self(bytes.to_vec()));
}
if !bytes.starts_with(b"refs/")
|| bytes.ends_with(b".")
|| bytes.windows(2).any(|pair| pair == b".." || pair == b"@{")
|| bytes
.iter()
.any(|b| *b <= b' ' || *b == 127 || b"~^:?*[\\".contains(b))
|| bytes
.split(|b| *b == b'/')
.any(|part| part.is_empty() || part.starts_with(b".") || part.ends_with(b".lock"))
{
return Err(InvalidRefName);
}
Ok(Self(bytes.to_vec()))
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn into_bytes(self) -> Vec<u8> {
self.0
}
pub(crate) fn per_worktree(&self) -> bool {
self.0 == b"HEAD"
|| [
b"refs/bisect/".as_slice(),
b"refs/rewritten/",
b"refs/worktree/",
]
.iter()
.any(|prefix| self.0.starts_with(prefix))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
#[error("expected HEAD or a valid full refs/ reference name")]
pub struct InvalidRefName;
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[test]
fn consuming_name_preserves_non_utf8_buffer() {
let name = RefName::new(b"refs/heads/\xff").unwrap();
let pointer = name.as_bytes().as_ptr();
let bytes = name.into_bytes();
assert_eq!(bytes, b"refs/heads/\xff");
assert_eq!(bytes.as_ptr(), pointer);
}
#[rstest]
#[case::head(b"HEAD")]
#[case::branch(b"refs/heads/main")]
#[case::bytes(b"refs/tags/\xff")]
#[case::at(b"refs/heads/@")]
#[case::dot_component_end(b"refs/heads/a./b")]
fn accepts_names(#[case] bytes: &[u8]) {
assert_eq!(RefName::new(bytes).unwrap().as_bytes(), bytes);
}
#[rstest]
#[case::empty(b"")]
#[case::short(b"main")]
#[case::pseudo(b"MERGE_HEAD")]
#[case::alias(b"main-worktree/HEAD")]
#[case::double_slash(b"refs//a")]
#[case::trailing_slash(b"refs/a/")]
#[case::dot(b"refs/.a")]
#[case::dot_dot(b"refs/a..b")]
#[case::trailing_dot(b"refs/a.")]
#[case::lock(b"refs/a.lock/b")]
#[case::reflog(b"refs/a@{b")]
#[case::control(b"refs/a\0b")]
#[case::del(b"refs/a\x7f")]
#[case::space(b"refs/a b")]
#[case::tilde(b"refs/a~b")]
#[case::caret(b"refs/a^b")]
#[case::colon(b"refs/a:b")]
#[case::question(b"refs/a?b")]
#[case::glob(b"refs/a*b")]
#[case::bracket(b"refs/a[b")]
#[case::backslash(b"refs/a\\b")]
fn rejects_names(#[case] bytes: &[u8]) {
assert_eq!(RefName::new(bytes), Err(InvalidRefName));
}
}