1use bstr::{BStr, BString, ByteSlice};
2
3pub mod name {
5 use bstr::BString;
6
7 #[derive(Debug)]
9 #[expect(missing_docs)]
10 #[non_exhaustive]
11 pub enum Error {
12 InvalidByte { byte: BString },
13 StartsWithSlash,
14 RepeatedSlash,
15 RepeatedDot,
16 LockFileSuffix,
17 ReflogPortion,
18 Asterisk,
19 StartsWithDot,
20 EndsWithDot,
21 EndsWithSlash,
22 Empty,
23 }
24
25 impl std::fmt::Display for Error {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 match self {
28 Error::InvalidByte { byte } => write!(
29 f,
30 "A ref must not contain invalid bytes or ascii control characters: {byte:?}"
31 ),
32 Error::StartsWithSlash => write!(f, "A reference name must not start with a slash '/'"),
33 Error::RepeatedSlash => write!(
34 f,
35 "Multiple slashes in a row are not allowed as they may change the reference's meaning"
36 ),
37 Error::RepeatedDot => write!(f, "A ref must not contain '..' as it may be mistaken for a range"),
38 Error::LockFileSuffix => write!(f, "A ref must not end with '.lock'"),
39 Error::ReflogPortion => write!(f, "A ref must not contain '@{{' which is a part of a ref-log"),
40 Error::Asterisk => write!(f, "A ref must not contain '*' character"),
41 Error::StartsWithDot => write!(f, "A ref must not start with a '.'"),
42 Error::EndsWithDot => write!(f, "A ref must not end with a '.'"),
43 Error::EndsWithSlash => write!(f, "A ref must not end with a '/'"),
44 Error::Empty => write!(f, "A ref must not be empty"),
45 }
46 }
47 }
48
49 impl std::error::Error for Error {}
50}
51
52pub fn name(input: &BStr) -> Result<&BStr, name::Error> {
55 match name_inner(input, Mode::Validate)? {
56 None => Ok(input),
57 Some(_) => {
58 unreachable!("When validating, the input isn't changed")
59 }
60 }
61}
62
63#[derive(Eq, PartialEq)]
64pub(crate) enum Mode {
65 Sanitize,
66 Validate,
67}
68
69pub(crate) fn name_inner(input: &BStr, mode: Mode) -> Result<Option<BString>, name::Error> {
74 let mut out: Option<BString> =
75 matches!(mode, Mode::Sanitize).then(|| BString::from(Vec::with_capacity(input.len())));
76 if input.is_empty() {
77 return if let Some(mut out) = out {
78 out.push(b'-');
79 Ok(Some(out))
80 } else {
81 Err(name::Error::Empty)
82 };
83 }
84 if *input.last().expect("non-empty") == b'/' && out.is_none() {
85 return Err(name::Error::EndsWithSlash);
86 }
87 if input.first() == Some(&b'/') && out.is_none() {
88 return Err(name::Error::StartsWithSlash);
89 }
90
91 let mut previous = 0;
92 let mut component_start;
93 let mut component_end = 0;
94 let last = input.len() - 1;
95 for (byte_pos, byte) in input.iter().enumerate() {
96 match byte {
97 b'\\' | b'^' | b':' | b'[' | b'?' | b' ' | b'~' | b'\0'..=b'\x1F' | b'\x7F' => {
98 if let Some(out) = out.as_mut() {
99 out.push(b'-');
100 } else {
101 return Err(name::Error::InvalidByte {
102 byte: (&[*byte][..]).into(),
103 });
104 }
105 }
106 b'*' => {
107 if let Some(out) = out.as_mut() {
108 out.push(b'-');
109 } else {
110 return Err(name::Error::Asterisk);
111 }
112 }
113
114 b'.' if previous == b'.' => {
115 if out.is_none() {
116 return Err(name::Error::RepeatedDot);
117 }
118 }
119 b'.' if previous == b'/' => {
120 if let Some(out) = out.as_mut() {
121 out.push(b'-');
122 } else {
123 return Err(name::Error::StartsWithDot);
124 }
125 }
126 b'{' if previous == b'@' => {
127 if let Some(out) = out.as_mut() {
128 out.push(b'-');
129 } else {
130 return Err(name::Error::ReflogPortion);
131 }
132 }
133 b'/' if previous == b'/' => {
134 if out.is_none() {
135 return Err(name::Error::RepeatedSlash);
136 }
137 }
138 c => {
139 if *c == b'/' {
140 component_start = component_end;
141 component_end = byte_pos;
142
143 if input[component_start..component_end].ends_with_str(".lock") {
144 if let Some(out) = out.as_mut() {
145 while out.ends_with(b".lock") {
146 let len_without_suffix = out.len() - b".lock".len();
147 out.truncate(len_without_suffix);
148 }
149 } else {
150 return Err(name::Error::LockFileSuffix);
151 }
152 }
153 }
154
155 if let Some(out) = out.as_mut() {
156 out.push(*c);
157 }
158
159 if byte_pos == last && input[component_end + 1..].ends_with_str(".lock") {
160 if let Some(out) = out.as_mut() {
161 while out.ends_with(b".lock") {
162 let len_without_suffix = out.len() - b".lock".len();
163 out.truncate(len_without_suffix);
164 }
165 } else {
166 return Err(name::Error::LockFileSuffix);
167 }
168 }
169 }
170 }
171 previous = *byte;
172 }
173
174 if let Some(out) = out.as_mut() {
175 while out.last() == Some(&b'/') {
176 out.pop();
177 }
178 while out.first() == Some(&b'/') {
179 out.remove(0);
180 }
181 }
182 if out.as_ref().map_or(input, |b| b.as_bstr())[0] == b'.' {
183 if let Some(out) = out.as_mut() {
184 out[0] = b'-';
185 } else {
186 return Err(name::Error::StartsWithDot);
187 }
188 }
189 let last = out.as_ref().map_or(input, |b| b.as_bstr()).len() - 1;
190 if out.as_ref().map_or(input, |b| b.as_bstr())[last] == b'.' {
191 if let Some(out) = out.as_mut() {
192 let last = out.len() - 1;
193 out[last] = b'-';
194 } else {
195 return Err(name::Error::EndsWithDot);
196 }
197 }
198 Ok(out)
199}