feature_check/expr/parser/
p_nom.rs

1//! Parse query expressions using a Nom parser combinator.
2// SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
3// SPDX-License-Identifier: BSD-2-Clause
4
5use std::collections::HashMap;
6use std::iter;
7
8use anyhow::Context as _;
9use nom::{
10    Err as NErr, IResult, Parser as _, branch as nbranch,
11    bytes::complete as nbytesc,
12    character::complete as ncharc,
13    combinator as ncomb,
14    error::{Error as NError, ErrorKind as NErrorKind},
15    multi as nmulti, sequence as nseq,
16};
17
18use crate::defs::{Mode, ParseError};
19use crate::expr::{BoolOp, BoolOpKind, FeatureOp, VersionOp};
20use crate::version::{ParseError as VParseError, Version, VersionComponent};
21
22/// Utility function for building up a Nom failure error.
23#[inline]
24fn err_fail(input: &str) -> NErr<NError<&str>> {
25    NErr::Failure(NError::new(input, NErrorKind::Fail))
26}
27
28/// Make a `nom` error suitable for using as an `anyhow` error.
29fn clone_err_input(err: NErr<NError<&str>>) -> NErr<NError<String>> {
30    err.map_input(ToOwned::to_owned)
31}
32
33/// Parse the numerical part of a version component into an unsigned integer.
34///
35/// # Errors
36///
37/// Standard Nom parser errors; also, [`nom::Err::Failure`] on (hopefully impossible)
38/// failure to convert the already-validated characters to a number.
39#[inline]
40fn v_num(input: &str) -> IResult<&str, u32> {
41    let (r_input, digits) = nbytesc::take_while1(|chr: char| chr.is_ascii_digit())(input)?;
42    #[expect(clippy::map_err_ignore, reason = "it really does not matter")]
43    Ok((r_input, digits.parse::<u32>().map_err(|_| err_fail(input))?))
44}
45
46/// Parse the freeform string part of a version component into a string.
47///
48/// # Errors
49///
50/// Standard Nom parser errors; also, [`nom::Err::Failure`] on (hopefully impossible)
51/// failure to split the already-validated characters up.
52#[inline]
53fn v_rest(input: &str) -> IResult<&str, &str> {
54    let (f_input, _) = ncharc::one_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrwxyz~+")(input)?;
55    let (r_input, _) = nbytesc::take_while(|chr| {
56        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~+".contains(chr)
57    })(f_input)?;
58    Ok((
59        r_input,
60        input.strip_suffix(r_input).ok_or_else(|| err_fail(input))?,
61    ))
62}
63
64/// Parse a version component that contains a numerical part and
65/// an optional freeform one.
66///
67/// # Errors
68///
69/// Standard Nom parser errors.
70#[inline]
71fn v_comp_with_num(input: &str) -> IResult<&str, VersionComponent> {
72    let (r_input, (num, rest)) = nseq::pair(v_num, ncomb::opt(v_rest)).parse(input)?;
73    Ok((
74        r_input,
75        VersionComponent {
76            num: Some(num),
77            rest: rest.map_or_else(String::new, str::to_owned),
78        },
79    ))
80}
81
82/// Parse a version component that only contains the freeform string part.
83///
84/// # Errors
85///
86/// Standard Nom parser errors.
87#[inline]
88fn v_comp_rest_only(input: &str) -> IResult<&str, VersionComponent> {
89    let (r_input, rest) = v_rest(input)?;
90    Ok((
91        r_input,
92        VersionComponent {
93            num: None,
94            rest: rest.to_owned(),
95        },
96    ))
97}
98
99/// Parse a dot-separated list of version components into a vector.
100///
101/// # Errors
102///
103/// Standard Nom parser errors.
104#[inline]
105fn v_components(input: &str) -> IResult<&str, Vec<VersionComponent>> {
106    let (r_input, (first, arr)) = nseq::pair(
107        nbranch::alt((v_comp_with_num, v_comp_rest_only)),
108        ncomb::opt(nmulti::many0(nseq::pair(
109            nbytesc::tag("."),
110            nbranch::alt((v_comp_with_num, v_comp_rest_only)),
111        ))),
112    )
113    .parse(input)?;
114    if let Some(comps) = arr {
115        Ok((
116            r_input,
117            iter::once(first)
118                .chain(comps.into_iter().map(|(_dot, comp)| comp))
119                .collect(),
120        ))
121    } else {
122        Ok((r_input, vec![first]))
123    }
124}
125
126/// Parse a version string into a [`Version`] struct.
127///
128/// # Errors
129///
130/// Standard Nom parser errors; also, [`nom::Err::Failure`] on (hopefully impossible)
131/// failure to split the already-validated characters up.
132#[inline]
133fn p_version(input: &str) -> IResult<&str, Version> {
134    let (r_input, comps) = v_components(input)?;
135    let v_chars = input.strip_suffix(r_input).ok_or_else(|| err_fail(input))?;
136    Ok((r_input, Version::new(String::from(v_chars), comps)))
137}
138
139/// Parse a version string.
140///
141/// # Errors
142///
143/// Returns an error if the version string is invalid.
144#[inline]
145pub fn parse_version(value: &str) -> Result<Version, VParseError> {
146    let (left, res) = p_version(value)
147        .map_err(clone_err_input)
148        .context("Could not parse a version string")
149        .map_err(|err| VParseError::ParseFailure(value.to_owned(), err))?;
150    if left.is_empty() {
151        Ok(res)
152    } else {
153        Err(VParseError::ParseLeftovers(value.to_owned(), left.len()))
154    }
155}
156
157/// Parse a feature name to a string slice.
158///
159/// Errors:
160///
161/// Standard Nom parser errors.
162#[inline]
163fn p_feature(input: &str) -> IResult<&str, &str> {
164    let (r_input, name) =
165        nbytesc::is_a("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-")(input)?;
166    Ok((r_input, name))
167}
168
169/// Parse a comparison operator sign ("<", ">=", etc).
170///
171/// Errors:
172///
173/// Standard Nom parser errors; also, [`nom::Err::Failure`] on (hopefully impossible)
174/// failure to convert the already-validated characters to a [`BoolOpKind`] value.
175#[inline]
176fn p_op_sign(input: &str) -> IResult<&str, BoolOpKind> {
177    let (r_input, res) = nbranch::alt((
178        nbytesc::tag(BoolOpKind::LE),
179        nbytesc::tag(BoolOpKind::LT),
180        nbytesc::tag(BoolOpKind::EQ),
181        nbytesc::tag(BoolOpKind::GE),
182        nbytesc::tag(BoolOpKind::GT),
183    ))
184    .parse(input)?;
185    #[expect(clippy::map_err_ignore, reason = "it really does not matter")]
186    Ok((r_input, res.parse().map_err(|_| err_fail(input))?))
187}
188
189/// Parse a comparison operator word ("lt", "ge", etc).
190///
191/// Errors:
192///
193/// Standard Nom parser errors; also, [`nom::Err::Failure`] on (hopefully impossible)
194/// failure to convert the already-validated characters to a [`BoolOpKind`] value.
195#[inline]
196fn p_op_word(input: &str) -> IResult<&str, BoolOpKind> {
197    let (r_input, res) = nbranch::alt((
198        nbytesc::tag(BoolOpKind::LT_S),
199        nbytesc::tag(BoolOpKind::LE_S),
200        nbytesc::tag(BoolOpKind::EQ_S),
201        nbytesc::tag(BoolOpKind::GE_S),
202        nbytesc::tag(BoolOpKind::GT_S),
203    ))
204    .parse(input)?;
205    #[expect(clippy::map_err_ignore, reason = "it really does not matter")]
206    Ok((r_input, res.parse().map_err(|_| err_fail(input))?))
207}
208
209/// Parse a comparison operator sign ("<", ">=", etc) and a version string.
210///
211/// Errors:
212///
213/// Standard Nom parser errors.
214#[inline]
215fn p_op_sign_and_version(input: &str) -> IResult<&str, (BoolOpKind, Version)> {
216    let (r_input, res) = (
217        ncharc::multispace0,
218        p_op_sign,
219        ncharc::multispace0,
220        p_version,
221        ncharc::multispace0,
222    )
223        .parse(input)?;
224    Ok((r_input, (res.1, res.3)))
225}
226
227/// Parse a comparison operator word ("lt", "ge", etc) and a version string.
228///
229/// Errors:
230///
231/// Standard Nom parser errors.
232#[inline]
233fn p_op_word_and_version(input: &str) -> IResult<&str, (BoolOpKind, Version)> {
234    let (r_input, res) = (
235        ncharc::multispace1,
236        p_op_word,
237        ncharc::multispace1,
238        p_version,
239        ncharc::multispace0,
240    )
241        .parse(input)?;
242    Ok((r_input, (res.1, res.3)))
243}
244
245/// Parse a comparison operator ("<", "ge", etc) and a version string.
246///
247/// # Errors
248///
249/// Standard Nom parser errors.
250#[inline]
251fn p_op_and_version(input: &str) -> IResult<&str, (BoolOpKind, Version)> {
252    nbranch::alt((p_op_sign_and_version, p_op_word_and_version)).parse(input)
253}
254
255/// Parse a single feature name or a simple expression.
256///
257/// Errors:
258///
259/// Standard Nom parser errors.
260#[inline]
261fn p_expr(input: &str) -> IResult<&str, Mode> {
262    let (r_input, (feature, op_ver)) =
263        nseq::pair(p_feature, ncomb::opt(p_op_and_version)).parse(input)?;
264    if let Some((op, ver)) = op_ver {
265        Ok((
266            r_input,
267            Mode::Simple(Box::new(BoolOp::new(
268                op,
269                Box::new(FeatureOp::new(feature)),
270                Box::new(VersionOp::from_version(ver)),
271            ))),
272        ))
273    } else {
274        Ok((r_input, Mode::Single(Box::new(FeatureOp::new(feature)))))
275    }
276}
277
278/// Parse a single `feature[=version]` pair with a "1.0" version default.
279///
280/// # Errors
281///
282/// Standard Nom parser errors; also, [`nom::Err::Failure`] on (hopefully impossible)
283/// failure to convert a "1.0" string to a [`Version`] struct.
284#[inline]
285fn p_feature_version(input: &str) -> IResult<&str, (String, Version)> {
286    let (r_input, (feature, version)) = nseq::pair(
287        p_feature,
288        ncomb::opt(nseq::pair(nbytesc::tag("="), p_version)),
289    )
290    .parse(input)?;
291    #[expect(clippy::map_err_ignore, reason = "it really does not matter")]
292    Ok((
293        r_input,
294        (
295            feature.to_owned(),
296            version.map_or_else(
297                || parse_version("1.0").map_err(|_| err_fail(input)),
298                |(_, ver)| Ok(ver),
299            )?,
300        ),
301    ))
302}
303
304/// Parse a `feature=[version] feature[=version]...` line into a map.
305///
306/// # Errors
307///
308/// Standard Nom parser errors.
309#[inline]
310fn p_features_line(input: &str) -> IResult<&str, HashMap<String, Version>> {
311    let (r_input, (_, first, rest, _)) = (
312        ncharc::multispace0,
313        p_feature_version,
314        nmulti::many0(nseq::pair(ncharc::multispace1, p_feature_version)),
315        ncharc::multispace0,
316    )
317        .parse(input)?;
318    Ok((
319        r_input,
320        iter::once(first)
321            .chain(rest.into_iter().map(|(_, pair)| pair))
322            .collect(),
323    ))
324}
325
326/// Parse a feature name or a "feature op version" expression.
327///
328/// # Errors
329///
330/// Returns an error if the expression is invalid.
331#[inline]
332pub fn parse_expr(expr: &str) -> Result<Mode, ParseError> {
333    let (left, mode) = p_expr(expr)
334        .map_err(clone_err_input)
335        .context("Could not parse a test expression")
336        .map_err(|err| ParseError::ParseFailure(expr.to_owned(), err))?;
337    if left.is_empty() {
338        Ok(mode)
339    } else {
340        Err(ParseError::ParseLeftovers(expr.to_owned(), left.len()))
341    }
342}
343
344/// Parse a line of `feature[=version]` pairs.
345///
346/// # Errors
347///
348/// Returns an error if the feature names or version strings are invalid.
349#[inline]
350pub fn parse_features_line(line: &str) -> Result<HashMap<String, Version>, ParseError> {
351    let (left, res) = p_features_line(line)
352        .map_err(clone_err_input)
353        .context("Could not parse the program's features line")
354        .map_err(|err| ParseError::ParseFailure(line.to_owned(), err))?;
355    if left.is_empty() {
356        Ok(res)
357    } else {
358        Err(ParseError::ParseLeftovers(line.to_owned(), left.len()))
359    }
360}