1use super::{Anchor, ByteSet, Finder, RunClass, RunRule};
2use std::ops::Range;
3
4#[derive(Default)]
5pub struct Semver {}
6
7impl Semver {
8 fn is_boundary_before(input: &[u8], pos: usize) -> bool {
9 if pos == 0 {
10 return true;
11 }
12 let b = input[pos - 1];
13 !b.is_ascii_alphanumeric() && b != b'.'
14 }
15
16 fn is_boundary_after(input: &[u8], pos: usize) -> bool {
17 if pos >= input.len() {
18 return true;
19 }
20 let b = input[pos];
21 if b.is_ascii_alphanumeric() {
22 return false;
23 }
24 if b == b'.' {
28 return !(pos + 1 < input.len() && input[pos + 1].is_ascii_digit());
29 }
30 true
34 }
35
36 fn parse_digits(input: &[u8], pos: usize) -> Option<usize> {
37 if pos >= input.len() || !input[pos].is_ascii_digit() {
38 return None;
39 }
40 let mut end = pos + 1;
41 while end < input.len() && input[end].is_ascii_digit() {
42 end += 1;
43 }
44 Some(end)
45 }
46
47 fn parse_prerelease_or_build(input: &[u8], pos: usize) -> usize {
48 let mut end = pos;
49 while end < input.len()
50 && (input[end].is_ascii_alphanumeric() || input[end] == b'-' || input[end] == b'.')
51 {
52 end += 1;
53 }
54 while end > pos && input[end - 1] == b'.' {
56 end -= 1;
57 }
58 end
59 }
60}
61
62impl Finder for Semver {
63 fn line_agnostic(&self) -> bool {
64 true
67 }
68
69 fn id(&self) -> &'static str {
70 "semver"
71 }
72
73 fn dispatchable(&self) -> bool {
74 true
75 }
76
77 fn could_start_at(&self, byte: u8) -> bool {
78 byte.is_ascii_digit() || byte == b'v' || byte == b'V'
79 }
80
81 fn could_start_after(&self, prev: u8, _cur: u8) -> bool {
82 !prev.is_ascii_alphanumeric() && prev != b'.'
83 }
84
85 fn could_continue_with(&self, cur: u8, next: u8) -> bool {
86 if cur.is_ascii_digit() {
87 next.is_ascii_digit() || next == b'.'
88 } else {
89 next.is_ascii_digit()
90 }
91 }
92
93 fn anchor(&self) -> Option<Anchor> {
94 Some(Anchor::new(
95 ByteSet::from_bytes(b"."),
96 ByteSet::from_fn(|b| b.is_ascii_digit() || b == b'v' || b == b'V'),
97 ))
98 }
99
100 fn run_rules(&self) -> Vec<RunRule> {
101 vec![
103 RunRule::new(RunClass::Digit, 1, crate::RUN_CAP)
104 .followed_by(b".")
105 .then(RunClass::Digit, 1, crate::RUN_CAP)
106 .followed_by(b"."),
107 ]
108 }
109
110 fn try_at(&self, input: &[u8], pos: usize) -> Option<Range<usize>> {
111 let start = pos;
112 let mut p = pos;
113
114 if p < input.len() && (input[p] == b'v' || input[p] == b'V') {
115 if p + 1 < input.len() && input[p + 1].is_ascii_digit() {
116 p += 1;
117 } else {
118 return None;
119 }
120 } else if p < input.len() && input[p].is_ascii_digit() {
121 } else {
123 return None;
124 }
125
126 if !Self::is_boundary_before(input, start) {
127 return None;
128 }
129
130 let major_end = Self::parse_digits(input, p)?;
131 if major_end >= input.len() || input[major_end] != b'.' {
132 return None;
133 }
134 let minor_end = Self::parse_digits(input, major_end + 1)?;
135 if minor_end >= input.len() || input[minor_end] != b'.' {
136 return None;
137 }
138 let patch_end = Self::parse_digits(input, minor_end + 1)?;
139
140 let mut end = patch_end;
141 if end < input.len() && input[end] == b'-' {
142 let pre_end = Self::parse_prerelease_or_build(input, end + 1);
143 if pre_end > end + 1 {
144 end = pre_end;
145 }
146 }
147 if end < input.len() && input[end] == b'+' {
148 let build_end = Self::parse_prerelease_or_build(input, end + 1);
149 if build_end > end + 1 {
150 end = build_end;
151 }
152 }
153
154 if Self::is_boundary_after(input, end) {
155 Some(start..end)
156 } else {
157 None
158 }
159 }
160
161 fn find(&self, s: &str) -> Option<Range<usize>> {
162 let input = s.as_bytes();
165 (0..input.len())
166 .filter(|&idx| self.could_start_at(input[idx]))
167 .find_map(|idx| self.try_at(input, idx))
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn id_should_return_semver() {
177 let finder = Semver::default();
178 assert_eq!("semver", finder.id());
179 }
180
181 #[test]
182 fn find_should_extract_simple_version() {
183 let finder = Semver::default();
184 let input = "version 1.0.0 released";
185 let range = finder.find(input).unwrap();
186 assert_eq!("1.0.0", &input[range]);
187 }
188
189 #[test]
190 fn find_should_extract_version_with_v_prefix() {
191 let finder = Semver::default();
192 let input = "tag v2.3.1 created";
193 let range = finder.find(input).unwrap();
194 assert_eq!("v2.3.1", &input[range]);
195 }
196
197 #[test]
198 fn find_should_extract_version_with_uppercase_v() {
199 let finder = Semver::default();
200 let input = "V1.0.0";
201 let range = finder.find(input).unwrap();
202 assert_eq!("V1.0.0", &input[range]);
203 }
204
205 #[test]
206 fn find_should_extract_version_with_prerelease() {
207 let finder = Semver::default();
208 let input = "v1.0.0-rc.1 is out";
209 let range = finder.find(input).unwrap();
210 assert_eq!("v1.0.0-rc.1", &input[range]);
211 }
212
213 #[test]
214 fn find_should_extract_version_with_build_meta() {
215 let finder = Semver::default();
216 let input = "1.0.0+build.42";
217 let range = finder.find(input).unwrap();
218 assert_eq!("1.0.0+build.42", &input[range]);
219 }
220
221 #[test]
222 fn find_should_extract_version_with_prerelease_and_build() {
223 let finder = Semver::default();
224 let input = "v2.0.0-alpha.1+build.123";
225 let range = finder.find(input).unwrap();
226 assert_eq!("v2.0.0-alpha.1+build.123", &input[range]);
227 }
228
229 #[test]
230 fn find_should_extract_version_with_large_numbers() {
231 let finder = Semver::default();
232 let input = "100.200.300";
233 let range = finder.find(input).unwrap();
234 assert_eq!("100.200.300", &input[range]);
235 }
236
237 #[test]
238 fn find_should_extract_version_with_zeros() {
239 let finder = Semver::default();
240 let input = "0.0.0";
241 let range = finder.find(input).unwrap();
242 assert_eq!("0.0.0", &input[range]);
243 }
244
245 #[test]
246 fn find_should_reject_two_components() {
247 let finder = Semver::default();
248 assert!(finder.find("1.0 only").is_none());
249 }
250
251 #[test]
252 fn find_should_reject_four_components() {
253 let finder = Semver::default();
254 assert!(finder.find("1.0.0.0 too many").is_none());
255 }
256
257 #[test]
258 fn find_should_reject_preceded_by_alphanumeric() {
259 let finder = Semver::default();
260 assert!(finder.find("a1.0.0").is_none());
261 }
262
263 #[test]
264 fn find_should_reject_preceded_by_dot() {
265 let finder = Semver::default();
266 assert!(finder.find(".1.0.0").is_none());
267 }
268
269 #[test]
270 fn find_should_handle_empty_input() {
271 let finder = Semver::default();
272 assert!(finder.find("").is_none());
273 }
274
275 #[test]
276 fn find_should_extract_version_at_start() {
277 let finder = Semver::default();
278 let input = "1.2.3 is the version";
279 let range = finder.find(input).unwrap();
280 assert_eq!("1.2.3", &input[range]);
281 }
282
283 #[test]
284 fn find_should_extract_version_at_end() {
285 let finder = Semver::default();
286 let input = "version is 1.2.3";
287 let range = finder.find(input).unwrap();
288 assert_eq!("1.2.3", &input[range]);
289 }
290
291 #[test]
292 fn find_should_extract_multiple_versions_iteratively() {
293 let finder = Semver::default();
294 let input = "from 1.0.0 to 2.0.0";
295
296 let mut results = Vec::new();
297 let mut idx = 0;
298 while idx < input.len() {
299 if let Some(range) = finder.find(&input[idx..]) {
300 results.push(&input[idx + range.start..idx + range.end]);
301 idx += range.end;
302 } else {
303 break;
304 }
305 }
306
307 assert_eq!(vec!["1.0.0", "2.0.0"], results);
308 }
309
310 #[test]
311 fn find_should_extract_version_with_hyphen_prerelease() {
312 let finder = Semver::default();
313 let input = "1.0.0-beta-2";
314 let range = finder.find(input).unwrap();
315 assert_eq!("1.0.0-beta-2", &input[range]);
316 }
317
318 #[test]
319 fn find_should_extract_version_in_parentheses() {
320 let finder = Semver::default();
321 let input = "(v1.2.3)";
322 let range = finder.find(input).unwrap();
323 assert_eq!("v1.2.3", &input[range]);
324 }
325
326 #[test]
327 fn find_should_not_match_ip_address() {
328 let finder = Semver::default();
329 assert!(finder.find("192.168.1.1").is_none());
330 }
331
332 #[test]
333 fn try_at_simple() {
334 let finder = Semver::default();
335 let input = b"1.2.3 rest";
336 assert_eq!(finder.try_at(input, 0), Some(0..5));
337 }
338
339 #[test]
340 fn try_at_with_v_prefix() {
341 let finder = Semver::default();
342 let input = b"v1.2.3 rest";
343 assert_eq!(finder.try_at(input, 0), Some(0..6));
344 }
345
346 #[test]
347 fn try_at_preceded_by_alpha() {
348 let finder = Semver::default();
349 let input = b"a1.2.3";
350 assert!(finder.try_at(input, 1).is_none());
351 }
352
353 #[test]
354 fn try_at_preceded_by_dot() {
355 let finder = Semver::default();
356 let input = b".1.2.3";
357 assert!(finder.try_at(input, 1).is_none());
358 }
359
360 #[test]
361 fn try_at_v_without_digit() {
362 let finder = Semver::default();
363 let input = b"vx";
364 assert!(finder.try_at(input, 0).is_none());
365 }
366
367 #[test]
368 fn try_at_single_digit() {
369 let finder = Semver::default();
370 let input = b"1";
371 assert!(finder.try_at(input, 0).is_none());
372 }
373
374 #[test]
375 fn find_prerelease_with_dots() {
376 let finder = Semver::default();
377 let input = "1.0.0-alpha.1.2";
378 let range = finder.find(input).unwrap();
379 assert_eq!("1.0.0-alpha.1.2", &input[range]);
380 }
381
382 #[test]
383 fn find_build_with_dots() {
384 let finder = Semver::default();
385 let input = "1.0.0+build.1.2";
386 let range = finder.find(input).unwrap();
387 assert_eq!("1.0.0+build.1.2", &input[range]);
388 }
389
390 #[test]
391 fn find_backtracks_trailing_dot_in_prerelease() {
392 let finder = Semver::default();
394 let input = "1.0.0-beta.";
395 let range = finder.find(input).unwrap();
396 assert_eq!("1.0.0-beta", &input[range]);
397 }
398
399 #[test]
400 fn find_prerelease_with_trailing_dot_in_text() {
401 let finder = Semver::default();
402 let input = "use 1.0.0-beta. done";
403 let range = finder.find(input).unwrap();
404 assert_eq!("1.0.0-beta", &input[range]);
405 }
406}