Skip to main content

weavatrix_git/
refs.rs

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}