git_ref_format_core/
check.rs

1use thiserror::Error;
2
3pub struct Options {
4    /// If `false`, the refname must contain at least one `/`.
5    pub allow_onelevel: bool,
6    /// If `true`, the refname may contain exactly one `*` character.
7    pub allow_pattern: bool,
8}
9
10#[derive(Debug, PartialEq, Eq, Error)]
11#[non_exhaustive]
12pub enum Error {
13    #[error("empty input")]
14    Empty,
15    #[error("lone '@' character")]
16    LoneAt,
17    #[error("consecutive or trailing slash")]
18    Slash,
19    #[error("ends with '.lock'")]
20    DotLock,
21    #[error("consecutive dots ('..')")]
22    DotDot,
23    #[error("at-open-brace ('@{{')")]
24    AtOpenBrace,
25    #[error("invalid character {0:?}")]
26    InvalidChar(char),
27    #[error("component starts with '.'")]
28    StartsDot,
29    #[error("component ends with '.'")]
30    EndsDot,
31    #[error("control character")]
32    Control,
33    #[error("whitespace")]
34    Space,
35    #[error("must contain at most one '*'")]
36    Pattern,
37    #[error("must contain at least one '/'")]
38    OneLevel,
39}
40
41/// Validate that a string slice is a valid refname.
42pub fn ref_format(opts: Options, s: &str) -> Result<(), Error> {
43    match s {
44        "" => Err(Error::Empty),
45        "@" => Err(Error::LoneAt),
46        "." => Err(Error::StartsDot),
47        _ => {
48            let mut globs = 0usize;
49            let mut parts = 0usize;
50
51            for x in s.split('/') {
52                if x.is_empty() {
53                    return Err(Error::Slash);
54                }
55
56                parts += 1;
57
58                if x.ends_with(".lock") {
59                    return Err(Error::DotLock);
60                }
61
62                let last_char = x.chars().count() - 1;
63                for (i, y) in x.chars().zip(x.chars().cycle().skip(1)).enumerate() {
64                    match y {
65                        ('.', '.') => return Err(Error::DotDot),
66                        ('@', '{') => return Err(Error::AtOpenBrace),
67
68                        ('\0', _) => return Err(Error::InvalidChar('\0')),
69                        ('\\', _) => return Err(Error::InvalidChar('\\')),
70                        ('~', _) => return Err(Error::InvalidChar('~')),
71                        ('^', _) => return Err(Error::InvalidChar('^')),
72                        (':', _) => return Err(Error::InvalidChar(':')),
73                        ('?', _) => return Err(Error::InvalidChar('?')),
74                        ('[', _) => return Err(Error::InvalidChar('[')),
75
76                        ('*', _) => globs += 1,
77
78                        ('.', _) if i == 0 => return Err(Error::StartsDot),
79                        ('.', _) if i == last_char => return Err(Error::EndsDot),
80
81                        (' ', _) => return Err(Error::Space),
82
83                        (z, _) if z.is_ascii_control() => return Err(Error::Control),
84
85                        _ => continue,
86                    }
87                }
88            }
89
90            if parts < 2 && !opts.allow_onelevel {
91                Err(Error::OneLevel)
92            } else if globs > 1 && opts.allow_pattern {
93                Err(Error::Pattern)
94            } else if globs > 0 && !opts.allow_pattern {
95                Err(Error::InvalidChar('*'))
96            } else {
97                Ok(())
98            }
99        }
100    }
101}