1use std::{fs, path::Path};
2
3use crate::{GitError, HashKind, ObjectId, Result, error::invalid};
4
5#[derive(Clone, Debug, Eq, PartialEq)]
6pub struct Reference {
7 pub name: String,
8 pub target: ObjectId,
9}
10
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct Head {
13 pub symbolic: Option<String>,
14 pub target: Option<ObjectId>,
15}
16
17pub(crate) fn validate_name(name: &str) -> Result<()> {
18 let invalid_part = name.is_empty()
19 || name.starts_with('/')
20 || name.ends_with('/')
21 || name.contains("..")
22 || name.contains("@{")
23 || name.contains('\\')
24 || name
25 .bytes()
26 .any(|byte| byte <= b' ' || byte == 0x7f || b"~^:?*[".contains(&byte))
27 || name.split('/').any(|part| {
28 part.is_empty() || part.starts_with('.') || part.to_ascii_lowercase().ends_with(".lock")
29 });
30 if invalid_part {
31 return Err(invalid(format!("invalid reference name {name:?}")));
32 }
33 Ok(())
34}
35
36pub(crate) fn read_text(path: &Path) -> Result<Option<String>> {
37 match fs::read_to_string(path) {
38 Ok(value) => Ok(Some(value.trim().to_owned())),
39 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
40 Err(error) => Err(error.into()),
41 }
42}
43
44pub(crate) fn parse_target(value: &str, hash: HashKind) -> Result<RefTarget> {
45 if let Some(name) = value.strip_prefix("ref: ") {
46 validate_name(name)?;
47 return Ok(RefTarget::Symbolic(name.to_owned()));
48 }
49 Ok(RefTarget::Direct(ObjectId::from_hex_for(value, hash)?))
50}
51
52pub(crate) fn packed_target(path: &Path, name: &str, hash: HashKind) -> Result<Option<ObjectId>> {
53 let Some(contents) = read_text(path)? else {
54 return Ok(None);
55 };
56 for line in contents.lines() {
57 if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
58 continue;
59 }
60 let Some((hex, candidate)) = line.split_once(' ') else {
61 return Err(invalid("malformed packed-refs entry"));
62 };
63 if candidate == name {
64 return ObjectId::from_hex_for(hex, hash).map(Some);
65 }
66 }
67 Ok(None)
68}
69
70#[derive(Clone, Debug, Eq, PartialEq)]
71pub(crate) enum RefTarget {
72 Symbolic(String),
73 Direct(ObjectId),
74}
75
76pub(crate) fn missing_ref(name: &str) -> GitError {
77 GitError::NotFound(format!("reference {name}"))
78}
79
80#[cfg(test)]
81mod tests {
82 use super::validate_name;
83
84 #[test]
85 fn validates_reference_names() {
86 assert!(validate_name("refs/heads/main").is_ok());
87 for invalid in ["", "../HEAD", "refs//main", "refs/a.lock", "refs/a b"] {
88 assert!(validate_name(invalid).is_err(), "{invalid}");
89 }
90 }
91}