Skip to main content

oma_tum/
parser.rs

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]+")] // ignore whitespace and newlines
8pub 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, // invalid operator
96        }
97    }
98}
99
100// const ZERO_STRING: &'static str = "0";
101const 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    // convert infix notation to RPN
121    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            // since we use a very simplified expression format, we don't have a LHS in our "binary expression"
127            // we will push a dummy VERSION_PLACEHOLDER_TOKEN to the stack, and later replace it with the actual version
128            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                // drain all operators and push them back to the output stack
155                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                // convert to version number token, as in this context, hex strings are only used as sha256sum arguments
170                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    // drain all remaining operators and add them to the output stack
187    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}