gix_validate/
tag.rs

1use bstr::{BStr, BString, ByteSlice};
2
3///
4pub mod name {
5    use bstr::BString;
6
7    /// The error returned by [`name()`][super::name()].
8    #[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
36/// Assure the given `input` resemble a valid git tag name, which is returned unchanged on success.
37/// Tag names are provided as names, like `v1.0` or `alpha-1`, without paths.
38pub 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}