use std::{fs, path::Path};
use crate::{GitError, HashKind, ObjectId, Result, error::invalid};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Reference {
pub name: String,
pub target: ObjectId,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Head {
pub symbolic: Option<String>,
pub target: Option<ObjectId>,
}
pub(crate) fn validate_name(name: &str) -> Result<()> {
let invalid_part = name.is_empty()
|| name.starts_with('/')
|| name.ends_with('/')
|| name.contains("..")
|| name.contains("@{")
|| name.contains('\\')
|| name
.bytes()
.any(|byte| byte <= b' ' || byte == 0x7f || b"~^:?*[".contains(&byte))
|| name.split('/').any(|part| {
part.is_empty() || part.starts_with('.') || part.to_ascii_lowercase().ends_with(".lock")
});
if invalid_part {
return Err(invalid(format!("invalid reference name {name:?}")));
}
Ok(())
}
pub(crate) fn read_text(path: &Path) -> Result<Option<String>> {
match fs::read_to_string(path) {
Ok(value) => Ok(Some(value.trim().to_owned())),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error.into()),
}
}
pub(crate) fn parse_target(value: &str, hash: HashKind) -> Result<RefTarget> {
if let Some(name) = value.strip_prefix("ref: ") {
validate_name(name)?;
return Ok(RefTarget::Symbolic(name.to_owned()));
}
Ok(RefTarget::Direct(ObjectId::from_hex_for(value, hash)?))
}
pub(crate) fn packed_target(path: &Path, name: &str, hash: HashKind) -> Result<Option<ObjectId>> {
let Some(contents) = read_text(path)? else {
return Ok(None);
};
for line in contents.lines() {
if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
continue;
}
let Some((hex, candidate)) = line.split_once(' ') else {
return Err(invalid("malformed packed-refs entry"));
};
if candidate == name {
return ObjectId::from_hex_for(hex, hash).map(Some);
}
}
Ok(None)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum RefTarget {
Symbolic(String),
Direct(ObjectId),
}
pub(crate) fn missing_ref(name: &str) -> GitError {
GitError::NotFound(format!("reference {name}"))
}
#[cfg(test)]
mod tests {
use super::validate_name;
#[test]
fn validates_reference_names() {
assert!(validate_name("refs/heads/main").is_ok());
for invalid in ["", "../HEAD", "refs//main", "refs/a.lock", "refs/a b"] {
assert!(validate_name(invalid).is_err(), "{invalid}");
}
}
}