mcvm_shared/
versions.rs

1use std::fmt::Display;
2
3#[cfg(feature = "schema")]
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7/// Pattern matching for the version of Minecraft, a package, etc.
8#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord)]
9#[cfg_attr(feature = "schema", derive(JsonSchema))]
10pub enum VersionPattern {
11	/// Matches a single version
12	Single(String),
13	/// Matches the latest version in the list
14	Latest(Option<String>),
15	/// Matches any version that is <= a version
16	Before(String),
17	/// Matches any version that is >= a version
18	After(String),
19	/// Matches any versions between an inclusive range
20	Range(String, String),
21	/// Matches any version
22	Any,
23}
24
25impl VersionPattern {
26	/// Finds all match in a list of versions
27	pub fn get_matches(&self, versions: &[String]) -> Vec<String> {
28		match self {
29			Self::Single(version) => match versions.contains(version) {
30				true => vec![version.to_string()],
31				false => vec![],
32			},
33			Self::Latest(found) => match found {
34				Some(found) => vec![found.clone()],
35				None => match versions.get(versions.len()).cloned() {
36					Some(version) => vec![version],
37					None => vec![],
38				},
39			},
40			Self::Before(version) => match versions.iter().position(|e| e == version) {
41				Some(pos) => versions[..=pos].to_vec(),
42				None => vec![],
43			},
44			Self::After(version) => match versions.iter().position(|e| e == version) {
45				Some(pos) => versions[pos..].to_vec(),
46				None => vec![],
47			},
48			Self::Range(start, end) => match versions.iter().position(|e| e == start) {
49				Some(start_pos) => match versions.iter().position(|e| e == end) {
50					Some(end_pos) => versions[start_pos..=end_pos].to_vec(),
51					None => vec![],
52				},
53				None => vec![],
54			},
55			Self::Any => versions.to_vec(),
56		}
57	}
58
59	/// Finds the newest match in a list of versions
60	pub fn get_match(&self, versions: &[String]) -> Option<String> {
61		self.get_matches(versions).last().cloned()
62	}
63
64	/// Compares this pattern to a single string.
65	/// For some pattern types, this may return false if it is unable to deduce an
66	/// answer from the list of versions provided.
67	pub fn matches_single(&self, version: &str, versions: &[String]) -> bool {
68		match self {
69			Self::Single(vers) => version == vers,
70			Self::Latest(cached) => match cached {
71				Some(vers) => version == vers,
72				None => {
73					if let Some(latest) = versions.last() {
74						version == latest
75					} else {
76						false
77					}
78				}
79			},
80			Self::Before(vers) => {
81				if let Some(vers_pos) = versions.iter().position(|x| x == vers) {
82					if let Some(version_pos) = versions.iter().position(|x| x == version) {
83						version_pos <= vers_pos
84					} else {
85						false
86					}
87				} else {
88					false
89				}
90			}
91			Self::After(vers) => {
92				if let Some(vers_pos) = versions.iter().position(|x| x == vers) {
93					if let Some(version_pos) = versions.iter().position(|x| x == version) {
94						version_pos >= vers_pos
95					} else {
96						false
97					}
98				} else {
99					false
100				}
101			}
102			Self::Range(start, end) => {
103				if let Some(start_pos) = versions.iter().position(|x| x == start) {
104					if let Some(end_pos) = versions.iter().position(|x| x == end) {
105						if let Some(version_pos) = versions.iter().position(|x| x == version) {
106							(version_pos >= start_pos) && (version_pos <= end_pos)
107						} else {
108							false
109						}
110					} else {
111						false
112					}
113				} else {
114					false
115				}
116			}
117			Self::Any => versions.contains(&version.to_string()),
118		}
119	}
120
121	/// Compares this pattern to a version supplied in a VersionInfo
122	pub fn matches_info(&self, version_info: &VersionInfo) -> bool {
123		self.matches_single(&version_info.version, &version_info.versions)
124	}
125
126	/// Returns the union of matches for multiple patterns
127	pub fn match_union(&self, other: &Self, versions: &[String]) -> Vec<String> {
128		self.get_matches(versions)
129			.iter()
130			.zip(other.get_matches(versions))
131			.filter_map(
132				|(left, right)| {
133					if *left == right {
134						Some(right)
135					} else {
136						None
137					}
138				},
139			)
140			.collect()
141	}
142
143	/// Creates a version pattern by parsing a string
144	pub fn from(text: &str) -> Self {
145		match text {
146			"latest" => Self::Latest(None),
147			"*" => Self::Any,
148			text => {
149				if let Some(last) = text.chars().last() {
150					// Check for escape
151					if !text.chars().nth(text.len() - 2).is_some_and(|x| x == '\\') {
152						match last {
153							'-' => return Self::Before(text[..text.len() - 1].to_string()),
154							'+' => return Self::After(text[..text.len() - 1].to_string()),
155							_ => {}
156						}
157					}
158				}
159
160				let range_split: Vec<_> = text.split("..").collect();
161				if range_split.len() == 2 {
162					let start = range_split
163						.first()
164						.expect("First element in range split should exist");
165					// Check for escape
166					if !start.ends_with('\\') {
167						let end = range_split
168							.get(1)
169							.expect("Second element in range split should exist");
170						return Self::Range(start.to_string(), end.to_string());
171					}
172				}
173
174				Self::Single(text.replace('\\', ""))
175			}
176		}
177	}
178
179	/// Checks that a string contains no pattern-special characters
180	#[cfg(test)]
181	pub fn validate(text: &str) -> bool {
182		if text.contains('*') || text.contains("..") || text == "latest" {
183			return false;
184		}
185		if let Some(last) = text.chars().last() {
186			if last == '-' || last == '+' {
187				return false;
188			}
189		}
190		true
191	}
192}
193
194impl Display for VersionPattern {
195	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196		write!(
197			f,
198			"{}",
199			match self {
200				Self::Single(version) => version.to_string(),
201				Self::Latest(..) => "latest".into(),
202				Self::Before(version) => version.to_string() + "-",
203				Self::After(version) => version.to_string() + "+",
204				Self::Range(start, end) => start.to_string() + ".." + end,
205				Self::Any => "*".into(),
206			}
207		)
208	}
209}
210
211impl<'de> Deserialize<'de> for VersionPattern {
212	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
213	where
214		D: serde::Deserializer<'de>,
215	{
216		let string = String::deserialize(deserializer)?;
217		Ok(Self::from(&string))
218	}
219}
220
221impl Serialize for VersionPattern {
222	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
223	where
224		S: serde::Serializer,
225	{
226		let ser = self.to_string().serialize(serializer)?;
227		Ok(ser)
228	}
229}
230
231/// Utility struct that contains the version and version list
232#[derive(Debug, Default, Deserialize, Serialize, Clone)]
233pub struct VersionInfo {
234	/// The version
235	pub version: String,
236	/// The list of available versions to use for comparisons
237	pub versions: Vec<String>,
238}
239
240#[cfg(test)]
241mod tests {
242	use super::*;
243
244	#[test]
245	fn test_version_pattern() {
246		let versions = vec![
247			"1.16.5".to_string(),
248			"1.17".to_string(),
249			"1.18".to_string(),
250			"1.19.3".to_string(),
251		];
252
253		assert_eq!(
254			VersionPattern::Single("1.19.3".into()).get_match(&versions),
255			Some("1.19.3".into())
256		);
257		assert_eq!(
258			VersionPattern::Single("1.18".into()).get_match(&versions),
259			Some("1.18".into())
260		);
261		assert_eq!(
262			VersionPattern::Single(String::new()).get_match(&versions),
263			None
264		);
265		assert_eq!(
266			VersionPattern::Before("1.18".into()).get_match(&versions),
267			Some("1.18".into())
268		);
269		assert_eq!(
270			VersionPattern::After("1.16.5".into()).get_match(&versions),
271			Some("1.19.3".into())
272		);
273
274		assert_eq!(
275			VersionPattern::Before("1.17".into()).get_matches(&versions),
276			vec!["1.16.5".to_string(), "1.17".to_string()]
277		);
278		assert_eq!(
279			VersionPattern::After("1.17".into()).get_matches(&versions),
280			vec!["1.17".to_string(), "1.18".to_string(), "1.19.3".to_string()]
281		);
282		assert_eq!(
283			VersionPattern::Range("1.16.5".into(), "1.18".into()).get_matches(&versions),
284			vec!["1.16.5".to_string(), "1.17".to_string(), "1.18".to_string()]
285		);
286
287		assert!(VersionPattern::Before("1.18".into()).matches_single("1.16.5", &versions));
288		assert!(VersionPattern::After("1.18".into()).matches_single("1.19.3", &versions));
289		assert!(VersionPattern::Latest(None).matches_single("1.19.3", &versions));
290	}
291
292	#[test]
293	fn test_version_pattern_parse() {
294		assert_eq!(
295			VersionPattern::from("+1.19.5"),
296			VersionPattern::Single("+1.19.5".into())
297		);
298		assert_eq!(VersionPattern::from("latest"), VersionPattern::Latest(None));
299		assert_eq!(
300			VersionPattern::from("1.19.5-"),
301			VersionPattern::Before("1.19.5".into())
302		);
303		assert_eq!(
304			VersionPattern::from("1.19.5+"),
305			VersionPattern::After("1.19.5".into())
306		);
307		assert_eq!(
308			VersionPattern::from("1.17.1..1.19.3"),
309			VersionPattern::Range("1.17.1".into(), "1.19.3".into())
310		);
311	}
312
313	#[test]
314	fn test_version_pattern_parse_escape() {
315		assert_eq!(
316			VersionPattern::from("1.19.5\\+"),
317			VersionPattern::Single("1.19.5+".into())
318		);
319		assert_eq!(
320			VersionPattern::from("1.17.1\\..1.19.3"),
321			VersionPattern::Single("1.17.1..1.19.3".into())
322		);
323	}
324
325	#[test]
326	fn test_version_pattern_validation() {
327		assert!(VersionPattern::validate("hello"));
328		assert!(!VersionPattern::validate("latest"));
329		assert!(!VersionPattern::validate("foo-"));
330		assert!(!VersionPattern::validate("foo+"));
331		assert!(!VersionPattern::validate("f*o"));
332		assert!(!VersionPattern::validate("f..o"));
333	}
334}