1use std::fmt::Display;
2
3use logos::{Logos, Span};
4use snafu::Snafu;
5
6#[derive(Logos, Copy, Clone, Debug, PartialEq)]
7#[logos(skip r"[ \t\n\f]+")] pub enum VersionToken<'source> {
9 #[token("=")]
10 Eq,
11 #[token("==")]
12 EqEq,
13 #[token("!=")]
14 NotEq,
15 #[token(">=")]
16 GtEq,
17 #[token("<=")]
18 LtEq,
19 #[token(">")]
20 Gt,
21 #[token("<")]
22 Lt,
23 #[token("||")]
24 Or,
25 #[token("&&")]
26 And,
27 #[token("(")]
28 LParen,
29 #[token(")")]
30 RParen,
31 #[regex(r"[a-fA-F0-9]+", priority = 3)]
32 Hexadecimal(&'source str),
33 #[regex(r"(\d+:)?[0-9][0-9A-Za-z.+\-~]*")]
34 VersionNumber(&'source str),
35}
36
37impl<'source> Display for VersionToken<'source> {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 VersionToken::Eq => write!(f, "="),
41 VersionToken::EqEq => write!(f, "=="),
42 VersionToken::NotEq => write!(f, "!="),
43 VersionToken::GtEq => write!(f, ">="),
44 VersionToken::LtEq => write!(f, "<="),
45 VersionToken::Gt => write!(f, ">"),
46 VersionToken::Lt => write!(f, "<"),
47 VersionToken::Or => write!(f, "||"),
48 VersionToken::And => write!(f, "&&"),
49 VersionToken::LParen => write!(f, "("),
50 VersionToken::RParen => write!(f, ")"),
51 VersionToken::Hexadecimal(hex) => write!(f, "{hex}"),
52 VersionToken::VersionNumber(version) => write!(f, "{version}"),
53 }
54 }
55}
56
57impl<'source> VersionToken<'source> {
58 pub fn is_op(&self) -> bool {
59 matches!(
60 self,
61 VersionToken::Eq
62 | VersionToken::EqEq
63 | VersionToken::NotEq
64 | VersionToken::GtEq
65 | VersionToken::LtEq
66 | VersionToken::Gt
67 | VersionToken::Lt
68 | VersionToken::Or
69 | VersionToken::And
70 )
71 }
72
73 pub fn is_cmp_op(&self) -> bool {
74 matches!(
75 self,
76 VersionToken::Eq
77 | VersionToken::EqEq
78 | VersionToken::NotEq
79 | VersionToken::GtEq
80 | VersionToken::LtEq
81 | VersionToken::Gt
82 | VersionToken::Lt
83 )
84 }
85
86 pub fn precedence(&self) -> u8 {
87 match self {
88 VersionToken::Eq
89 | VersionToken::GtEq
90 | VersionToken::LtEq
91 | VersionToken::Gt
92 | VersionToken::Lt
93 | VersionToken::NotEq => 10,
94 VersionToken::Or | VersionToken::And => 1,
95 _ => 0, }
97 }
98}
99
100const VERSION_PLACEHOLDER: &str = "$VER";
102const VERSION_PLACEHOLDER_TOKEN: VersionToken = VersionToken::VersionNumber(VERSION_PLACEHOLDER);
103
104#[derive(Debug, Snafu)]
105pub enum VersionParseError {
106 #[snafu(display("Invalid version expression at position {span:?}"))]
107 VersionExpr { span: Span },
108 #[snafu(display("Unexpected string '{s}' at position {span:?}"))]
109 UnexpectedString { s: String, span: Span },
110 #[snafu(display("Unmatched '(' at position {span:?}"))]
111 UnmatchedLeft { span: Span },
112}
113
114pub fn parse_version_expr(input: &str) -> Result<Vec<VersionToken<'_>>, VersionParseError> {
115 let mut lexer = VersionToken::lexer(input);
116 let mut stack: Vec<VersionToken> = Vec::with_capacity(8);
117 let mut operators: Vec<VersionToken> = Vec::with_capacity(8);
118 let mut prev_is_op = false;
119
120 while let Some(maybe_token) = lexer.next() {
122 let token =
123 maybe_token.map_err(|_| VersionParseError::VersionExpr { span: lexer.span() })?;
124
125 if token.is_cmp_op() {
126 stack.push(VERSION_PLACEHOLDER_TOKEN);
129 }
130
131 match token {
132 VersionToken::Eq
133 | VersionToken::EqEq
134 | VersionToken::NotEq
135 | VersionToken::GtEq
136 | VersionToken::LtEq
137 | VersionToken::Gt
138 | VersionToken::Lt
139 | VersionToken::Or
140 | VersionToken::And => {
141 if let Some(last_op) = operators.last()
142 && last_op.precedence() >= token.precedence()
143 {
144 let last = operators.pop().unwrap();
145 stack.push(last);
146 operators.push(token);
147 prev_is_op = token.is_op();
148 continue;
149 }
150 operators.push(token);
151 }
152 VersionToken::LParen => operators.push(token),
153 VersionToken::RParen => {
154 while let Some(op) = operators.pop() {
156 if op == VersionToken::LParen {
157 break;
158 }
159 stack.push(op);
160 }
161 }
162 VersionToken::Hexadecimal(v) => {
163 if !prev_is_op {
164 return Err(VersionParseError::UnexpectedString {
165 s: v.to_string(),
166 span: lexer.span(),
167 });
168 }
169 stack.push(VersionToken::VersionNumber(v));
171 }
172 VersionToken::VersionNumber(_) => {
173 if !prev_is_op {
174 return Err(VersionParseError::UnexpectedString {
175 s: token.to_string(),
176 span: lexer.span(),
177 });
178 }
179 stack.push(token);
180 }
181 }
182
183 prev_is_op = token.is_op();
184 }
185
186 while let Some(op) = operators.pop() {
188 if op == VersionToken::LParen {
189 return Err(VersionParseError::UnmatchedLeft { span: lexer.span() });
190 }
191 stack.push(op);
192 }
193
194 Ok(stack)
195}
196
197#[test]
198fn test_lexer() {
199 let input = "1.2.3+4-5";
200 let mut lexer = VersionToken::lexer(input);
201 let token = lexer.next().unwrap();
202 assert_eq!(token, Ok(VersionToken::VersionNumber(input)));
203 assert_eq!(lexer.slice(), "1.2.3+4-5");
204}
205
206#[test]
207fn test_parser_simple() {
208 let input_expr = "(=1.2.3 || =4.5.6) && <7.8.9 && > 20201010";
209 let tokens = parse_version_expr(input_expr).unwrap();
210 assert_eq!(
211 tokens,
212 vec![
213 VERSION_PLACEHOLDER_TOKEN,
214 VersionToken::VersionNumber("1.2.3"),
215 VersionToken::Eq,
216 VERSION_PLACEHOLDER_TOKEN,
217 VersionToken::VersionNumber("4.5.6"),
218 VersionToken::Eq,
219 VersionToken::Or,
220 VERSION_PLACEHOLDER_TOKEN,
221 VersionToken::VersionNumber("7.8.9"),
222 VersionToken::Lt,
223 VERSION_PLACEHOLDER_TOKEN,
224 VersionToken::VersionNumber("20201010"),
225 VersionToken::Gt,
226 VersionToken::And,
227 VersionToken::And,
228 ]
229 );
230}