use anyhow::{Result, anyhow};
pub fn ensure_safe_refname_fragment(name: &str, context: &str) -> Result<()> {
if name.is_empty() {
return Err(anyhow!("{context}: ref name is empty"));
}
if name.starts_with('-') {
return Err(anyhow!(
"{context}: ref name '{name}' starts with '-', which some git versions \
may misinterpret as a command-line flag. Rename it."
));
}
for ch in name.chars() {
if ch.is_control() {
return Err(anyhow!(
"{context}: ref name '{name}' contains a disallowed control character (\
U+{:04X}). Rename it.",
ch as u32
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_empty() {
assert!(ensure_safe_refname_fragment("", "tag").is_err());
}
#[test]
fn rejects_leading_dash() {
let err = ensure_safe_refname_fragment("--exec=ls", "tag")
.expect_err("should reject leading dash");
assert!(format!("{err:?}").contains("starts with '-'"));
}
#[test]
fn rejects_newline() {
assert!(ensure_safe_refname_fragment("foo\nbar", "tag").is_err());
assert!(ensure_safe_refname_fragment("foo\r", "tag").is_err());
}
#[test]
fn rejects_null_byte() {
assert!(ensure_safe_refname_fragment("foo\0bar", "tag").is_err());
}
#[test]
fn rejects_control_chars() {
assert!(ensure_safe_refname_fragment("foo\x01bar", "tag").is_err());
assert!(ensure_safe_refname_fragment("foo\x7fbar", "tag").is_err());
}
#[test]
fn every_rejected_ascii_control_char_is_one_git_also_refuses() {
for bad in &[
"foo\tbar",
"foo\nbar",
"foo\rbar",
"foo\x01bar",
"foo\x7fbar",
] {
assert!(
ensure_safe_refname_fragment(bad, "tag").is_err(),
"should reject {bad:?}"
);
let accepted = std::process::Command::new("git")
.arg("check-ref-format")
.arg(format!("refs/tags/{bad}"))
.status()
.expect("git should be on PATH")
.success();
assert!(
!accepted,
"git accepts {bad:?} in a ref name, so rejecting it here is wrong"
);
}
}
#[test]
fn allows_normal_names() {
for ok in &[
"v1.0.0",
"release/v1.0.0",
"api@v1.0.0",
"pkg/v1.2.3",
"feature-branch_2",
"1.x",
"v0.0.1-beta.42",
"我的分支",
] {
assert!(
ensure_safe_refname_fragment(ok, "test").is_ok(),
"should allow '{ok}'"
);
}
}
#[test]
fn rejects_tab() {
let err = ensure_safe_refname_fragment("foo\tbar", "tag").expect_err("should reject tab");
assert!(format!("{err:?}").contains("U+0009"), "{err:?}");
}
}