Skip to main content

gix_validate/
reference.rs

1use bstr::{BStr, BString, ByteSlice};
2
3///
4pub mod name {
5    use bstr::BString;
6    use std::convert::Infallible;
7
8    /// The error used in [name()][super::name()] and [`name_partial()`][super::name_partial()]
9    #[derive(Debug)]
10    #[expect(missing_docs)]
11    #[non_exhaustive]
12    pub enum Error {
13        InvalidByte { byte: BString },
14        StartsWithSlash,
15        RepeatedSlash,
16        RepeatedDot,
17        LockFileSuffix,
18        ReflogPortion,
19        Asterisk,
20        StartsWithDot,
21        EndsWithDot,
22        EndsWithSlash,
23        Empty,
24        SomeLowercase,
25        Reserved { name: BString },
26    }
27
28    impl std::fmt::Display for Error {
29        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30            match self {
31                Error::InvalidByte { byte } => write!(f, "Reference name contains invalid byte: {byte:?}"),
32                Error::StartsWithSlash => write!(f, "Reference name cannot start with a slash"),
33                Error::RepeatedSlash => write!(f, "Reference name cannot contain repeated slashes"),
34                Error::RepeatedDot => write!(f, "Reference name cannot contain repeated dots"),
35                Error::LockFileSuffix => write!(f, "Reference name cannot end with '.lock'"),
36                Error::ReflogPortion => write!(f, "Reference name cannot contain '@{{'"),
37                Error::Asterisk => write!(f, "Reference name cannot contain '*'"),
38                Error::StartsWithDot => write!(f, "Reference name cannot start with a dot"),
39                Error::EndsWithDot => write!(f, "Reference name cannot end with a dot"),
40                Error::EndsWithSlash => write!(f, "Reference name cannot end with a slash"),
41                Error::Empty => write!(f, "Reference name cannot be empty"),
42                Error::SomeLowercase => write!(f, "Standalone references must be all uppercased, like 'HEAD'"),
43                Error::Reserved { name } => write!(f, "Reference name is reserved and cannot be used: {name:?}"),
44            }
45        }
46    }
47
48    impl std::error::Error for Error {
49        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
50            Some(const { &gix_error::ClassificationMarker::VALIDATION })
51        }
52    }
53
54    impl From<crate::tag::name::Error> for Error {
55        fn from(err: crate::tag::name::Error) -> Self {
56            match err {
57                crate::tag::name::Error::InvalidByte { byte } => Error::InvalidByte { byte },
58                crate::tag::name::Error::StartsWithSlash => Error::StartsWithSlash,
59                crate::tag::name::Error::RepeatedSlash => Error::RepeatedSlash,
60                crate::tag::name::Error::RepeatedDot => Error::RepeatedDot,
61                crate::tag::name::Error::LockFileSuffix => Error::LockFileSuffix,
62                crate::tag::name::Error::ReflogPortion => Error::ReflogPortion,
63                crate::tag::name::Error::Asterisk => Error::Asterisk,
64                crate::tag::name::Error::StartsWithDot => Error::StartsWithDot,
65                crate::tag::name::Error::EndsWithDot => Error::EndsWithDot,
66                crate::tag::name::Error::EndsWithSlash => Error::EndsWithSlash,
67                crate::tag::name::Error::Empty => Error::Empty,
68            }
69        }
70    }
71
72    impl From<Infallible> for Error {
73        fn from(_: Infallible) -> Self {
74            unreachable!("this impl is needed to allow passing a known valid partial path as parameter")
75        }
76    }
77}
78
79/// Validate a reference name running all the tests in the book. This disallows lower-case references like `lower`, but also allows
80/// ones like `HEAD`, and `refs/lower`.
81pub fn name(path: &BStr) -> Result<&BStr, name::Error> {
82    match validate(path, Mode::Complete)? {
83        None => Ok(path),
84        Some(_) => {
85            unreachable!("Without sanitization, there is no chance a sanitized version is returned.")
86        }
87    }
88}
89
90/// Validate a reference name for use as a local branch.
91///
92/// This is like [`name()`], but also rejects `refs/heads/HEAD`, matching Git's branch-specific validation.
93pub fn branch_name(path: &BStr) -> Result<&BStr, name::Error> {
94    let path = name(path)?;
95    if path == "refs/heads/HEAD" {
96        return Err(name::Error::Reserved { name: path.into() });
97    }
98    Ok(path)
99}
100
101/// Validate a partial reference name. As it is assumed to be partial, names like `some-name` is allowed
102/// even though these would be disallowed with when using [`name()`].
103pub fn name_partial(path: &BStr) -> Result<&BStr, name::Error> {
104    match validate(path, Mode::Partial)? {
105        None => Ok(path),
106        Some(_) => {
107            unreachable!("Without sanitization, there is no chance a sanitized version is returned.")
108        }
109    }
110}
111
112/// Sanitize `path` into a nonempty partial reference name which would also pass [`name_partial()`].
113///
114/// Return `-` if `path` is empty or sanitization leaves no name, as with input consisting only of `/` separators.
115/// This fallback is specific to Gitoxide: Git's sanitizer does not replace empty results. It ensures callers
116/// always receive a nonempty valid name without needing to handle an empty result themselves.
117pub fn name_partial_or_sanitize(path: &BStr) -> BString {
118    let mut name = validate(path, Mode::PartialSanitize)
119        .expect("sanitization mode never returns validation errors")
120        .expect("sanitization mode always rebuilds the name");
121    if name.is_empty() {
122        name.push(b'-');
123    }
124    name
125}
126
127enum Mode {
128    Complete,
129    Partial,
130    /// like Partial, but instead of failing, a sanitized version is returned.
131    PartialSanitize,
132}
133
134fn validate(path: &BStr, mode: Mode) -> Result<Option<BString>, name::Error> {
135    let mut out = crate::tag::name_inner(
136        path,
137        match mode {
138            Mode::Complete | Mode::Partial => crate::tag::Mode::Validate,
139            Mode::PartialSanitize => crate::tag::Mode::Sanitize,
140        },
141    )?;
142    // `@` alone is shorthand for `HEAD` in revspecs, so Git refuses it as a reference name.
143    // Git substitutes it with `-` when sanitizing it, which is what we do here too. Note that `@` stays
144    // valid as a component, which is why `refs/heads/@` and a tag named `@` are both fine.
145    if out.as_ref().map_or(path, |b| b.as_bstr()) == "@" {
146        match out.as_mut() {
147            Some(out) => out[0] = b'-',
148            None => return Err(name::Error::Reserved { name: path.into() }),
149        }
150    }
151    if let Mode::Complete = mode {
152        let input = out.as_ref().map_or(path, |b| b.as_bstr());
153        let saw_slash = input.find_byte(b'/').is_some();
154        if !saw_slash && !input.iter().all(|c| c.is_ascii_uppercase() || *c == b'_') {
155            return Err(name::Error::SomeLowercase);
156        }
157    }
158    Ok(out)
159}