use bstr::ByteSlice;
pub const FUNNY_REFNAME_REASON: &str = "funny refname";
pub const RESERVED_REF_NAMESPACES: &[&str] = &["refs/alkgit"];
pub const LOCK_REFNAME_SUFFIX: &str = ".lock";
pub fn validate_ref_name(name: &str) -> Result<(), String> {
let rejected = gix_validate::reference::name(name.as_bytes().as_bstr()).is_err()
|| name.ends_with(LOCK_REFNAME_SUFFIX)
|| RESERVED_REF_NAMESPACES.iter().any(|prefix| {
name.len() >= prefix.len()
&& name.starts_with(prefix)
&& (name.len() == prefix.len() || name.as_bytes()[prefix.len()] == b'/')
});
if rejected {
Err(FUNNY_REFNAME_REASON.to_string())
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn is_allowed(name: &str) -> bool {
validate_ref_name(name).is_ok()
}
#[test]
fn ordinary_refs_are_accepted() {
for name in [
"refs/heads/main",
"refs/tags/v1.0",
"refs/heads/feature/deep",
"refs/remotes/origin/main",
"refs/notes/mirror/notes/x",
] {
assert!(is_allowed(name), "{name} should pass");
}
}
#[test]
fn git_refname_invalid_names_are_rejected() {
for name in [
"",
"refs/heads/../evil",
"refs/heads/foo bar",
"refs/heads/foo..bar",
"@",
"refs/heads/foo@{x}",
"refs/heads/.starts-with-dot",
"refs/heads/ends-with-dot.",
"refs/heads/ends-with-slash/",
"//no-leading-slash",
"refs//repeated-slash",
"refs/heads/asterisk*",
] {
assert!(!is_allowed(name), "{name:?} should be rejected");
}
}
#[test]
fn the_lock_suffix_rule_is_enforced() {
assert!(!is_allowed("refs/heads/foo.lock"));
assert!(!is_allowed("refs/heads/main.lock"));
assert!(!is_allowed("refs/heads/x/.lock"));
}
#[test]
fn the_reserved_namespace_rule_is_enforced() {
assert!(!is_allowed("refs/alkgit"));
assert!(!is_allowed("refs/alkgit/"));
assert!(!is_allowed("refs/alkgit/any/thing"));
assert!(is_allowed("refs/heads/alkgit"));
assert!(is_allowed("refs/tags/alkgit/x"));
}
#[test]
fn every_rejection_carries_the_pinned_reason() {
for name in [
"refs/heads/../evil",
"refs/heads/foo.lock",
"refs/alkgit/x",
"",
] {
assert_eq!(
validate_ref_name(name),
Err(FUNNY_REFNAME_REASON.to_string()),
"{name:?}"
);
}
}
#[test]
fn the_pin_is_the_observed_upstream_text() {
assert_eq!(FUNNY_REFNAME_REASON, "funny refname");
}
#[test]
fn the_deny_list_constant_holds_the_rules() {
assert!(RESERVED_REF_NAMESPACES
.iter()
.all(|p| p.starts_with("refs/")));
assert_eq!(LOCK_REFNAME_SUFFIX, ".lock");
}
#[test]
fn near_miss_names_are_rejected_not_cleaned() {
for name in [" refs/heads/main", "refs/heads/main ", "refs/heads/ma..in"] {
assert_eq!(
validate_ref_name(name),
Err(FUNNY_REFNAME_REASON.to_string()),
"{name:?} is a near miss of a valid ref — rejected verbatim, never repaired"
);
}
}
}