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 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
51 Some(const { &gix_error::ClassificationMarker::VALIDATION })
52 }
53 }
54}
55
56pub fn name(input: &BStr) -> Result<&BStr, name::Error> {
59 match name_inner(input, Mode::Validate)? {
60 None => Ok(input),
61 Some(_) => {
62 unreachable!("When validating, the input isn't changed")
63 }
64 }
65}
66
67#[derive(Eq, PartialEq)]
68pub(crate) enum Mode {
69 Sanitize,
70 Validate,
71}
72
73pub(crate) fn name_inner(input: &BStr, mode: Mode) -> Result<Option<BString>, name::Error> {
78 let mut out: Option<BString> =
79 matches!(mode, Mode::Sanitize).then(|| BString::from(Vec::with_capacity(input.len())));
80 if input.is_empty() {
81 return if let Some(out) = out {
82 Ok(Some(out))
83 } else {
84 Err(name::Error::Empty)
85 };
86 }
87 if *input.last().expect("non-empty") == b'/' && out.is_none() {
88 return Err(name::Error::EndsWithSlash);
89 }
90 if input.first() == Some(&b'/') && out.is_none() {
91 return Err(name::Error::StartsWithSlash);
92 }
93
94 let mut previous = 0;
95 let mut component_start;
96 let mut component_end = 0;
97 let last = input.len() - 1;
98 for (byte_pos, byte) in input.iter().enumerate() {
99 match byte {
100 b'\\' | b'^' | b':' | b'[' | b'?' | b' ' | b'~' | b'\0'..=b'\x1F' | b'\x7F' => {
101 if let Some(out) = out.as_mut() {
102 out.push(b'-');
103 } else {
104 return Err(name::Error::InvalidByte {
105 byte: (&[*byte][..]).into(),
106 });
107 }
108 }
109 b'*' => {
110 if let Some(out) = out.as_mut() {
111 out.push(b'-');
112 } else {
113 return Err(name::Error::Asterisk);
114 }
115 }
116
117 b'.' if previous == b'.' => {
118 if out.is_none() {
119 return Err(name::Error::RepeatedDot);
120 }
121 }
122 b'.' if previous == b'/' => {
123 if let Some(out) = out.as_mut() {
124 out.push(b'-');
125 } else {
126 return Err(name::Error::StartsWithDot);
127 }
128 }
129 b'{' if previous == b'@' => {
130 if let Some(out) = out.as_mut() {
131 out.push(b'-');
132 } else {
133 return Err(name::Error::ReflogPortion);
134 }
135 }
136 b'/' if previous == b'/' => {
137 if out.is_none() {
138 return Err(name::Error::RepeatedSlash);
139 }
140 }
141 c => {
142 if *c == b'/' {
143 component_start = component_end;
144 component_end = byte_pos;
145
146 if input[component_start..component_end].ends_with_str(".lock") {
147 if let Some(out) = out.as_mut() {
148 while out.ends_with(b".lock") {
149 let len_without_suffix = out.len() - b".lock".len();
150 out.truncate(len_without_suffix);
151 }
152 } else {
153 return Err(name::Error::LockFileSuffix);
154 }
155 }
156 }
157
158 if let Some(out) = out.as_mut() {
159 out.push(if byte_pos == 0 && *c == b'.' { b'-' } else { *c });
161 }
162
163 if byte_pos == last && input[component_end + 1..].ends_with_str(".lock") {
164 if let Some(out) = out.as_mut() {
165 while out.ends_with(b".lock") {
166 let len_without_suffix = out.len() - b".lock".len();
167 out.truncate(len_without_suffix);
168 }
169 } else {
170 return Err(name::Error::LockFileSuffix);
171 }
172 }
173 }
174 }
175 previous = *byte;
176 }
177
178 if let Some(out) = out.as_mut() {
179 while out.last() == Some(&b'/') {
180 out.pop();
181 }
182 while out.first() == Some(&b'/') {
183 out.remove(0);
184 }
185 }
186 if out.as_ref().map_or(input, |b| b.as_bstr()).first() == Some(&b'.') {
187 if let Some(out) = out.as_mut() {
188 out[0] = b'-';
189 } else {
190 return Err(name::Error::StartsWithDot);
191 }
192 }
193 if out.as_ref().map_or(input, |b| b.as_bstr()).last() == Some(&b'.') {
194 if let Some(out) = out.as_mut() {
195 let last = out.len() - 1;
196 out[last] = b'-';
197 } else {
198 return Err(name::Error::EndsWithDot);
199 }
200 }
201 Ok(out)
202}