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