r_description/
relations.rs

1//! Parsing of Debian relations strings.
2use crate::version::Version;
3use std::borrow::Cow;
4use std::iter::Peekable;
5use std::str::Chars;
6
7/// Constraint on a Debian package version.
8#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
9pub enum VersionConstraint {
10    /// <<
11    LessThan, // <<
12    /// <=
13    LessThanEqual, // <=
14    /// =
15    Equal, // =
16    /// >>
17    GreaterThan, // >>
18    /// >=
19    GreaterThanEqual, // >=
20}
21
22impl std::str::FromStr for VersionConstraint {
23    type Err = String;
24
25    fn from_str(s: &str) -> Result<Self, Self::Err> {
26        match s {
27            ">=" => Ok(VersionConstraint::GreaterThanEqual),
28            "<=" => Ok(VersionConstraint::LessThanEqual),
29            "=" => Ok(VersionConstraint::Equal),
30            ">>" => Ok(VersionConstraint::GreaterThan),
31            "<<" => Ok(VersionConstraint::LessThan),
32            _ => Err(format!("Invalid version constraint: {s}")),
33        }
34    }
35}
36
37impl std::fmt::Display for VersionConstraint {
38    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
39        match self {
40            VersionConstraint::GreaterThanEqual => f.write_str(">="),
41            VersionConstraint::LessThanEqual => f.write_str("<="),
42            VersionConstraint::Equal => f.write_str("="),
43            VersionConstraint::GreaterThan => f.write_str(">>"),
44            VersionConstraint::LessThan => f.write_str("<<"),
45        }
46    }
47}
48
49/// Let's start with defining all kinds of tokens and
50/// composite nodes.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
52#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
53#[repr(u16)]
54#[allow(missing_docs)]
55pub(crate) enum SyntaxKind {
56    IDENT = 0,  // package name
57    COMMA,      // ,
58    L_PARENS,   // (
59    R_PARENS,   // )
60    L_ANGLE,    // <
61    R_ANGLE,    // >
62    EQUAL,      // =
63    WHITESPACE, // whitespace
64    NEWLINE,    // newline
65    ERROR,      // as well as errors
66
67    // composite nodes
68    ROOT,       // The entire file
69    RELATION,   // An alternative in a dependency
70    VERSION,    // A version constraint
71    CONSTRAINT, // (">=", "<=", "=", ">>", "<<")
72}
73
74/// Convert our `SyntaxKind` into the rowan `SyntaxKind`.
75impl From<SyntaxKind> for rowan::SyntaxKind {
76    fn from(kind: SyntaxKind) -> Self {
77        Self(kind as u16)
78    }
79}
80
81/// A lexer for relations strings.
82pub(crate) struct Lexer<'a> {
83    input: Peekable<Chars<'a>>,
84}
85
86impl<'a> Lexer<'a> {
87    /// Create a new lexer for the given input.
88    pub fn new(input: &'a str) -> Self {
89        Lexer {
90            input: input.chars().peekable(),
91        }
92    }
93
94    fn is_whitespace(c: char) -> bool {
95        c == ' ' || c == '\t' || c == '\r'
96    }
97
98    fn is_valid_ident_char(c: char) -> bool {
99        c.is_ascii_alphanumeric() || c == '-' || c == '.'
100    }
101
102    fn read_while<F>(&mut self, predicate: F) -> String
103    where
104        F: Fn(char) -> bool,
105    {
106        let mut result = String::new();
107        while let Some(&c) = self.input.peek() {
108            if predicate(c) {
109                result.push(c);
110                self.input.next();
111            } else {
112                break;
113            }
114        }
115        result
116    }
117
118    fn next_token(&mut self) -> Option<(SyntaxKind, String)> {
119        if let Some(&c) = self.input.peek() {
120            match c {
121                ',' => {
122                    self.input.next();
123                    Some((SyntaxKind::COMMA, ",".to_owned()))
124                }
125                '(' => {
126                    self.input.next();
127                    Some((SyntaxKind::L_PARENS, "(".to_owned()))
128                }
129                ')' => {
130                    self.input.next();
131                    Some((SyntaxKind::R_PARENS, ")".to_owned()))
132                }
133                '<' => {
134                    self.input.next();
135                    Some((SyntaxKind::L_ANGLE, "<".to_owned()))
136                }
137                '>' => {
138                    self.input.next();
139                    Some((SyntaxKind::R_ANGLE, ">".to_owned()))
140                }
141                '=' => {
142                    self.input.next();
143                    Some((SyntaxKind::EQUAL, "=".to_owned()))
144                }
145                '\n' => {
146                    self.input.next();
147                    Some((SyntaxKind::NEWLINE, "\n".to_owned()))
148                }
149                _ if Self::is_whitespace(c) => {
150                    let whitespace = self.read_while(Self::is_whitespace);
151                    Some((SyntaxKind::WHITESPACE, whitespace))
152                }
153                // TODO: separate handling for package names and versions?
154                _ if Self::is_valid_ident_char(c) => {
155                    let key = self.read_while(Self::is_valid_ident_char);
156                    Some((SyntaxKind::IDENT, key))
157                }
158                _ => {
159                    self.input.next();
160                    Some((SyntaxKind::ERROR, c.to_string()))
161                }
162            }
163        } else {
164            None
165        }
166    }
167}
168
169impl Iterator for Lexer<'_> {
170    type Item = (SyntaxKind, String);
171
172    fn next(&mut self) -> Option<Self::Item> {
173        self.next_token()
174    }
175}
176
177pub(crate) fn lex(input: &str) -> Vec<(SyntaxKind, String)> {
178    let mut lexer = Lexer::new(input);
179    lexer.by_ref().collect::<Vec<_>>()
180}
181
182/// A trait for looking up versions of packages.
183pub trait VersionLookup {
184    /// Look up the version of a package.
185    fn lookup_version<'a>(&'a self, package: &'_ str) -> Option<std::borrow::Cow<'a, Version>>;
186}
187
188impl VersionLookup for std::collections::HashMap<String, Version> {
189    fn lookup_version<'a>(&'a self, package: &str) -> Option<Cow<'a, Version>> {
190        self.get(package).map(Cow::Borrowed)
191    }
192}
193
194impl<F> VersionLookup for F
195where
196    F: Fn(&str) -> Option<Version>,
197{
198    fn lookup_version<'a>(&'a self, name: &str) -> Option<Cow<'a, Version>> {
199        self(name).map(Cow::Owned)
200    }
201}
202
203impl VersionLookup for (String, Version) {
204    fn lookup_version<'a>(&'a self, name: &str) -> Option<Cow<'a, Version>> {
205        if name == self.0 {
206            Some(Cow::Borrowed(&self.1))
207        } else {
208            None
209        }
210    }
211}