1const fn is_forbidden_char(byte: u8) -> bool {
8 matches!(byte, b'~' | b'^' | b':' | b'?' | b'*' | b'[' | b'\\')
9}
10
11pub const fn validate(input: &str) -> Result<(), RefFormatError> {
17 if input.is_empty() {
18 return Err(RefFormatError::Empty);
19 }
20
21 let bytes = input.as_bytes();
22
23 if bytes.len() == 1 && bytes[0] == b'@' {
24 return Err(RefFormatError::SingleAt);
25 }
26
27 if bytes[0] == b'-' {
28 return Err(RefFormatError::StartsWithDash);
29 }
30 if bytes[0] == b'.' {
31 return Err(RefFormatError::StartsWithDot);
32 }
33 if bytes[0] == b'/' {
34 return Err(RefFormatError::StartsWithSlash);
35 }
36
37 if bytes[bytes.len() - 1] == b'/' {
38 return Err(RefFormatError::EndsWithSlash);
39 }
40 if bytes[bytes.len() - 1] == b'.' {
41 return Err(RefFormatError::EndsWithDot);
42 }
43
44 if bytes.len() >= 5
46 && bytes[bytes.len() - 5] == b'.'
47 && bytes[bytes.len() - 4] == b'l'
48 && bytes[bytes.len() - 3] == b'o'
49 && bytes[bytes.len() - 2] == b'c'
50 && bytes[bytes.len() - 1] == b'k'
51 {
52 return Err(RefFormatError::EndsWithLock);
53 }
54
55 let mut index = 0;
56 while index < bytes.len() {
58 let byte = bytes[index];
59
60 if byte < 0x20 || byte == 0x7f {
61 return Err(RefFormatError::ContainsControlCharacter);
62 }
63
64 if byte == b' ' {
65 return Err(RefFormatError::ContainsSpace);
66 }
67
68 if is_forbidden_char(byte) {
69 return Err(RefFormatError::ContainsForbiddenCharacter);
70 }
71
72 if byte == b'.' && index + 1 < bytes.len() && bytes[index + 1] == b'.' {
73 return Err(RefFormatError::ContainsDoubleDot);
74 }
75
76 if byte == b'/' && index + 1 < bytes.len() && bytes[index + 1] == b'/' {
77 return Err(RefFormatError::ContainsDoubleSlash);
78 }
79
80 if byte == b'@' && index + 1 < bytes.len() && bytes[index + 1] == b'{' {
81 return Err(RefFormatError::ContainsAtBrace);
82 }
83
84 if byte == b'/' && index + 1 < bytes.len() && bytes[index + 1] == b'.' {
85 return Err(RefFormatError::ComponentStartsWithDot);
86 }
87
88 if byte == b'.'
89 && index + 5 < bytes.len()
90 && bytes[index + 1] == b'l'
91 && bytes[index + 2] == b'o'
92 && bytes[index + 3] == b'c'
93 && bytes[index + 4] == b'k'
94 && bytes[index + 5] == b'/'
95 {
96 return Err(RefFormatError::ComponentEndsWithLock);
97 }
98
99 index += 1;
100 }
101
102 Ok(())
103}
104
105#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
107pub enum RefFormatError {
108 #[error("git ref name cannot be empty")]
109 Empty,
110 #[error("git ref name cannot be single '@'")]
111 SingleAt,
112 #[error("git ref name cannot start with '-'")]
113 StartsWithDash,
114 #[error("git ref name cannot start with '.'")]
115 StartsWithDot,
116 #[error("git ref name cannot start with '/'")]
117 StartsWithSlash,
118 #[error("git ref name cannot end with '/'")]
119 EndsWithSlash,
120 #[error("git ref name cannot end with '.'")]
121 EndsWithDot,
122 #[error("git ref name cannot end with '.lock'")]
123 EndsWithLock,
124 #[error("git ref name cannot contain '..'")]
125 ContainsDoubleDot,
126 #[error("git ref name cannot contain '//'")]
127 ContainsDoubleSlash,
128 #[error("git ref name cannot contain '@{{'")]
129 ContainsAtBrace,
130 #[error("git ref component cannot start with '.'")]
131 ComponentStartsWithDot,
132 #[error("git ref component cannot end with '.lock'")]
133 ComponentEndsWithLock,
134 #[error("git ref name cannot contain control characters")]
135 ContainsControlCharacter,
136 #[error("git ref name cannot contain spaces")]
137 ContainsSpace,
138 #[error("git ref name cannot contain forbidden characters (~^:?*[\\)")]
139 ContainsForbiddenCharacter,
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn test_valid() {
148 assert!(validate("main").is_ok());
149 assert!(validate("v1.0.0").is_ok());
150 assert!(validate("feature/login").is_ok());
151 assert!(validate("a/b/c").is_ok());
152 }
153
154 #[test]
155 fn test_empty() {
156 assert_eq!(validate(""), Err(RefFormatError::Empty));
157 }
158
159 #[test]
160 fn test_single_at() {
161 assert_eq!(validate("@"), Err(RefFormatError::SingleAt));
162 }
163
164 #[test]
165 fn test_starts_with_dash() {
166 assert_eq!(validate("-x"), Err(RefFormatError::StartsWithDash));
167 }
168
169 #[test]
170 fn test_starts_with_dot() {
171 assert_eq!(validate(".x"), Err(RefFormatError::StartsWithDot));
172 }
173
174 #[test]
175 fn test_starts_with_slash() {
176 assert_eq!(validate("/x"), Err(RefFormatError::StartsWithSlash));
177 }
178
179 #[test]
180 fn test_ends_with_slash() {
181 assert_eq!(validate("x/"), Err(RefFormatError::EndsWithSlash));
182 }
183
184 #[test]
185 fn test_ends_with_dot() {
186 assert_eq!(validate("x."), Err(RefFormatError::EndsWithDot));
187 }
188
189 #[test]
190 fn test_ends_with_lock() {
191 assert_eq!(validate("x.lock"), Err(RefFormatError::EndsWithLock));
192 }
193
194 #[test]
195 fn test_contains_double_dot() {
196 assert_eq!(validate("a..b"), Err(RefFormatError::ContainsDoubleDot));
197 }
198
199 #[test]
200 fn test_contains_double_slash() {
201 assert_eq!(validate("a//b"), Err(RefFormatError::ContainsDoubleSlash));
202 }
203
204 #[test]
205 fn test_contains_at_brace() {
206 assert_eq!(validate("a@{b"), Err(RefFormatError::ContainsAtBrace));
207 }
208
209 #[test]
210 fn test_component_starts_with_dot() {
211 assert_eq!(
212 validate("a/.b"),
213 Err(RefFormatError::ComponentStartsWithDot)
214 );
215 assert_eq!(
216 validate("a/b/.c/d"),
217 Err(RefFormatError::ComponentStartsWithDot)
218 );
219 }
220
221 #[test]
222 fn test_component_ends_with_lock() {
223 assert_eq!(
224 validate("a/b.lock/c"),
225 Err(RefFormatError::ComponentEndsWithLock)
226 );
227 }
228
229 #[test]
230 fn test_contains_space() {
231 assert_eq!(validate("a b"), Err(RefFormatError::ContainsSpace));
232 }
233
234 #[test]
235 fn test_contains_control_character() {
236 assert_eq!(
237 validate("a\x00b"),
238 Err(RefFormatError::ContainsControlCharacter)
239 );
240 assert_eq!(
241 validate("a\tb"),
242 Err(RefFormatError::ContainsControlCharacter)
243 );
244 assert_eq!(
245 validate("a\x7fb"),
246 Err(RefFormatError::ContainsControlCharacter)
247 );
248 }
249
250 #[test]
251 fn test_contains_forbidden_characters() {
252 for bad in ["a~b", "a^b", "a:b", "a?b", "a*b", "a[b", "a\\b"] {
253 assert_eq!(
254 validate(bad),
255 Err(RefFormatError::ContainsForbiddenCharacter)
256 );
257 }
258 }
259}