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