1use crate::version::compare_versions_for_range;
20use std::cmp::Ordering;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum VersionRange {
25 Exact(String),
27 Minimum {
29 version: String,
31 inclusive: bool,
33 },
34 Maximum {
36 version: String,
38 inclusive: bool,
40 },
41 Bounded {
43 min: String,
45 min_inclusive: bool,
47 max: String,
49 max_inclusive: bool,
51 },
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum BracketStyle {
63 Standard,
65 AllowReversed,
67}
68
69pub fn parse_interval(s: &str, style: BracketStyle) -> Option<VersionRange> {
80 let s = s.trim();
81 let first = s.chars().next()?;
82 let min_inclusive = match (first, style) {
83 ('[', _) => true,
84 ('(', _) => false,
85 (']', BracketStyle::AllowReversed) => false,
86 _ => return None,
87 };
88 let last = s.chars().next_back()?;
89 let max_inclusive = match (last, style) {
90 (']', _) => true,
91 (')', _) => false,
92 ('[', BracketStyle::AllowReversed) => false,
93 _ => return None,
94 };
95
96 if s.len() < first.len_utf8() + last.len_utf8() {
99 return None;
100 }
101
102 let inner = &s[first.len_utf8()..s.len() - last.len_utf8()];
103
104 if inner.contains(['[', ']', '(', ')']) {
105 return None;
106 }
107
108 if let Some((lo, hi)) = inner.split_once(',') {
109 if hi.contains(',') {
110 return None;
111 }
112 let lo = lo.trim();
113 let hi = hi.trim();
114 let min = (!lo.is_empty()).then(|| lo.to_string());
115 let max = (!hi.is_empty()).then(|| hi.to_string());
116 match (min, max) {
117 (Some(min), Some(max)) => Some(VersionRange::Bounded {
118 min,
119 min_inclusive,
120 max,
121 max_inclusive,
122 }),
123 (Some(version), None) => Some(VersionRange::Minimum {
124 version,
125 inclusive: min_inclusive,
126 }),
127 (None, Some(version)) => Some(VersionRange::Maximum {
128 version,
129 inclusive: max_inclusive,
130 }),
131 (None, None) => None,
132 }
133 } else {
134 let inner = inner.trim();
135 (!inner.is_empty() && min_inclusive && max_inclusive)
136 .then(|| VersionRange::Exact(inner.to_string()))
137 }
138}
139
140fn satisfies_min(v: &str, min: &str, inclusive: bool) -> bool {
141 let ord = compare_versions_for_range(v, min);
142 if inclusive {
143 ord != Ordering::Less
144 } else {
145 ord == Ordering::Greater
146 }
147}
148
149fn satisfies_max(v: &str, max: &str, inclusive: bool) -> bool {
150 let ord = compare_versions_for_range(v, max);
151 if inclusive {
152 ord != Ordering::Greater
153 } else {
154 ord == Ordering::Less
155 }
156}
157
158pub fn contains(version: &str, range: &VersionRange) -> bool {
160 match range {
161 VersionRange::Exact(target) => {
162 compare_versions_for_range(version, target) == Ordering::Equal
163 }
164 VersionRange::Minimum {
165 version: min,
166 inclusive,
167 } => satisfies_min(version, min, *inclusive),
168 VersionRange::Maximum {
169 version: max,
170 inclusive,
171 } => satisfies_max(version, max, *inclusive),
172 VersionRange::Bounded {
173 min,
174 min_inclusive,
175 max,
176 max_inclusive,
177 } => {
178 satisfies_min(version, min, *min_inclusive)
179 && satisfies_max(version, max, *max_inclusive)
180 }
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 fn parses(s: &str, style: BracketStyle) -> bool {
189 parse_interval(s, style).is_some()
190 }
191
192 #[test]
193 fn test_satisfies_exact_pin() {
194 for style in [BracketStyle::Standard, BracketStyle::AllowReversed] {
195 let range = parse_interval("[1.0]", style).unwrap();
196 assert!(contains("1.0", &range));
197 assert!(!contains("1.0.1", &range));
198 }
199 }
200
201 #[test]
202 fn test_satisfies_bounded_no_comma_vs_with_comma() {
203 for style in [BracketStyle::Standard, BracketStyle::AllowReversed] {
204 let exact = parse_interval("[1.0]", style).unwrap();
205 let bounded = parse_interval("[1.0,1.0]", style).unwrap();
206 assert!(contains("1.0", &exact));
207 assert!(contains("1.0", &bounded));
208 assert!(!contains("1.0.1", &bounded));
209 }
210 }
211
212 #[test]
213 fn test_satisfies_open_ended_minimum() {
214 for style in [BracketStyle::Standard, BracketStyle::AllowReversed] {
215 let range = parse_interval("[1.5,)", style).unwrap();
216 assert!(contains("1.5", &range));
217 assert!(contains("2.0", &range));
218 assert!(!contains("1.4", &range));
219 }
220 }
221
222 #[test]
223 fn test_satisfies_open_ended_maximum() {
224 for style in [BracketStyle::Standard, BracketStyle::AllowReversed] {
225 let inclusive = parse_interval("(,2.0]", style).unwrap();
226 assert!(contains("2.0", &inclusive));
227 assert!(!contains("2.0.1", &inclusive));
228 let exclusive = parse_interval("(,2.0)", style).unwrap();
229 assert!(contains("1.9", &exclusive));
230 assert!(!contains("2.0", &exclusive));
231 }
232 }
233
234 #[test]
235 fn test_satisfies_bounded_exclusive_inclusive_mix() {
236 for style in [BracketStyle::Standard, BracketStyle::AllowReversed] {
237 let range = parse_interval("[1.0,2.0)", style).unwrap();
238 assert!(contains("1.5", &range));
239 assert!(!contains("2.0", &range));
240 let range = parse_interval("(1.0,2.0)", style).unwrap();
241 assert!(!contains("1.0", &range));
242 assert!(contains("1.0.1", &range));
243 }
244 }
245
246 #[test]
247 fn test_satisfies_whitespace_inside_brackets() {
248 for style in [BracketStyle::Standard, BracketStyle::AllowReversed] {
249 let range = parse_interval("[ 1.0 , 2.0 )", style).unwrap();
250 assert!(contains("1.5", &range));
251 }
252 }
253
254 #[test]
255 fn test_satisfies_malformed_brackets_return_false() {
256 for style in [BracketStyle::Standard, BracketStyle::AllowReversed] {
257 assert!(!parses("[1.0,2.0", style));
258 assert!(!parses("1.0,2.0)", style));
259 assert!(!parses("(,)", style));
260 }
261 }
262
263 #[test]
264 fn test_satisfies_rejects_mismatched_no_comma_brackets() {
265 for style in [BracketStyle::Standard, BracketStyle::AllowReversed] {
269 assert!(!parses("[1.0)", style));
270 assert!(!parses("(1.0]", style));
271 assert!(!parses("(1.0)", style));
272 }
273 }
274
275 #[test]
276 fn test_satisfies_rejects_mismatched_no_comma_reversed_brackets() {
277 assert!(!parses("]1.0[", BracketStyle::AllowReversed));
281 assert!(!parses("]1.0]", BracketStyle::AllowReversed));
282 assert!(!parses("[1.0[", BracketStyle::AllowReversed));
283 }
284
285 #[test]
286 fn test_satisfies_rejects_stray_nested_brackets() {
287 for style in [BracketStyle::Standard, BracketStyle::AllowReversed] {
288 assert!(!parses("[[1.0,2.0)", style));
289 assert!(!parses("[1.0,2.0)]", style));
290 }
291 }
292
293 #[test]
294 fn test_satisfies_rejects_extra_component() {
295 for style in [BracketStyle::Standard, BracketStyle::AllowReversed] {
296 assert!(!parses("[1.0,2.0,3.0]", style));
297 }
298 }
299
300 #[test]
301 fn test_satisfies_qualifier_bearing_bounds() {
302 for style in [BracketStyle::Standard, BracketStyle::AllowReversed] {
303 let range = parse_interval("[1.0-beta,2.0-rc)", style).unwrap();
304 assert!(contains("1.0-milestone", &range));
305 assert!(!contains("1.0-alpha", &range));
306 assert!(!contains("2.0-rc", &range));
307 assert!(contains("2.0-milestone", &range));
308 }
309 }
310
311 #[test]
312 fn test_reversed_bracket_accepted_under_allow_reversed() {
313 let lower = parse_interval("]1.2,1.5]", BracketStyle::AllowReversed).unwrap();
314 assert!(!contains("1.2", &lower));
315 assert!(contains("1.3", &lower));
316 assert!(contains("1.5", &lower));
317 assert!(!contains("1.6", &lower));
318
319 let upper = parse_interval("[1.1,2.0[", BracketStyle::AllowReversed).unwrap();
320 assert!(contains("1.1", &upper));
321 assert!(contains("1.5", &upper));
322 assert!(!contains("2.0", &upper));
323 assert!(!contains("1.0", &upper));
324 }
325
326 #[test]
327 fn test_reversed_bracket_rejected_under_standard() {
328 assert!(!parses("]1.2,1.5]", BracketStyle::Standard));
329 assert!(!parses("[1.1,2.0[", BracketStyle::Standard));
330 }
331
332 #[test]
333 fn test_single_delimiter_is_rejected_not_panicking() {
334 for style in [BracketStyle::Standard, BracketStyle::AllowReversed] {
339 assert!(!parses("[", style));
340 assert!(!parses("]", style));
341 }
342 }
343}