use crate::error::Refusal;
pub fn check_git_legal(name: &str) -> Result<(), Refusal> {
for segment in name.split('/') {
if segment.starts_with('.') {
return Err(refusal(name, "segment begins with '.'"));
}
if segment.ends_with('.') {
return Err(refusal(name, "segment ends with '.'"));
}
if segment.contains("..") {
return Err(refusal(name, "segment contains '..'"));
}
}
Ok(())
}
#[allow(clippy::case_sensitive_file_extension_comparisons)]
pub fn check_tag_name(name: &[u8]) -> Result<(), &'static str> {
if name.is_empty() {
return Err("empty");
}
if name.len() > usize::from(mkit_core::object::TAG_NAME_MAX_LEN) {
return Err("over the tag-name length cap");
}
let Ok(s) = std::str::from_utf8(name) else {
return Err("not UTF-8");
};
for b in s.bytes() {
if !(b.is_ascii_alphanumeric() || b == b'.' || b == b'_' || b == b'-') {
return Err("byte outside the mkit ref-segment grammar");
}
}
if s == "." || s == ".." {
return Err("'.' or '..' name");
}
if s == "HEAD" {
return Err("'HEAD' is reserved");
}
if s.ends_with(".lock") {
return Err("'.lock' suffix");
}
check_git_legal(s).map_err(|_| "git-illegal dot placement")
}
fn refusal(name: &str, reason: &'static str) -> Refusal {
Refusal::RefName {
name: name.to_owned(),
reason,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plain_names_pass() {
for n in ["refs/heads/main", "refs/tags/v1.0.0", "refs/heads/a-b_c.d"] {
assert!(check_git_legal(n).is_ok(), "{n}");
}
}
#[test]
fn git_illegal_dot_shapes_refused() {
for n in [
"refs/heads/.hidden",
"refs/heads/trailing.",
"refs/heads/a..b",
] {
assert!(check_git_legal(n).is_err(), "{n}");
}
}
#[test]
fn tag_names_checked() {
assert!(check_tag_name(b"v1.0.0").is_ok());
assert!(check_tag_name(b"with space").is_err());
assert!(check_tag_name(b".dot").is_err());
assert!(
check_tag_name(b"no/slash").is_err(),
"SPEC-OBJECTS 6a forbids '/'"
);
assert!(check_tag_name(b"HEAD").is_err());
assert!(check_tag_name(b"v1.lock").is_err());
assert!(check_tag_name("naïve".as_bytes()).is_err());
}
}