feature-check 2.0.0

Query a program for supported features
Documentation
//! Parse query expressions using a Nom parser combinator.
/*
 * Copyright (c) 2022  Peter Pentchev <roam@ringlet.net>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

use std::collections::HashMap;
use std::iter;

use anyhow::Context;
use nom::{
    branch as nbranch,
    bytes::complete as nbytesc,
    character::complete as ncharc,
    combinator as ncomb,
    error::{Error as NError, ErrorKind as NErrorKind},
    multi as nmulti, sequence as nseq, Err as NErr, IResult,
};

use crate::defs::{Mode, ParseError};
use crate::expr::{BoolOp, BoolOpKind, FeatureOp, VersionOp};
use crate::version::{ParseError as VParseError, Version, VersionComponent};

/// Utility function for building up a Nom failure error.
#[inline]
fn err_fail(input: &str) -> NErr<NError<&str>> {
    NErr::Failure(NError::new(input, NErrorKind::Fail))
}

/// Make a `nom` error suitable for using as an `anyhow` error.
fn clone_err_input(err: NErr<NError<&str>>) -> NErr<NError<String>> {
    err.map_input(std::borrow::ToOwned::to_owned)
}

/// Parse the numerical part of a version component into an unsigned integer.
///
/// # Errors
///
/// Standard Nom parser errors; also, [`nom::Err::Failure`] on (hopefully impossible)
/// failure to convert the already-validated characters to a number.
#[allow(clippy::map_err_ignore)]
#[inline]
fn v_num(input: &str) -> IResult<&str, u32> {
    let (r_input, digits) = nbytesc::take_while1(|c: char| c.is_ascii_digit())(input)?;
    Ok((r_input, digits.parse::<u32>().map_err(|_| err_fail(input))?))
}

/// Parse the freeform string part of a version component into a string.
///
/// # Errors
///
/// Standard Nom parser errors; also, [`nom::Err::Failure`] on (hopefully impossible)
/// failure to split the already-validated characters up.
#[inline]
fn v_rest(input: &str) -> IResult<&str, &str> {
    let (f_input, _) = ncharc::one_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrwxyz~+")(input)?;
    let (r_input, _) = nbytesc::take_while(|c| {
        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~+".contains(c)
    })(f_input)?;
    Ok((
        r_input,
        input.strip_suffix(r_input).ok_or_else(|| err_fail(input))?,
    ))
}

/// Parse a version component that contains a numerical part and
/// an optional freeform one.
///
/// # Errors
///
/// Standard Nom parser errors.
#[inline]
fn v_comp_with_num(input: &str) -> IResult<&str, VersionComponent> {
    let (r_input, (num, rest)) = nseq::pair(v_num, ncomb::opt(v_rest))(input)?;
    Ok((
        r_input,
        VersionComponent {
            num: Some(num),
            rest: rest.map_or_else(String::new, str::to_owned),
        },
    ))
}

/// Parse a version component that only contains the freeform string part.
///
/// # Errors
///
/// Standard Nom parser errors.
#[inline]
fn v_comp_rest_only(input: &str) -> IResult<&str, VersionComponent> {
    let (r_input, rest) = v_rest(input)?;
    Ok((
        r_input,
        VersionComponent {
            num: None,
            rest: rest.to_owned(),
        },
    ))
}

/// Parse a dot-separated list of version components into a vector.
///
/// # Errors
///
/// Standard Nom parser errors.
#[inline]
fn v_components(input: &str) -> IResult<&str, Vec<VersionComponent>> {
    let (r_input, (first, arr)) = nseq::pair(
        nbranch::alt((v_comp_with_num, v_comp_rest_only)),
        ncomb::opt(nmulti::many0(nseq::pair(
            nbytesc::tag("."),
            nbranch::alt((v_comp_with_num, v_comp_rest_only)),
        ))),
    )(input)?;
    if let Some(comps) = arr {
        Ok((
            r_input,
            iter::once(first)
                .chain(comps.into_iter().map(|(_dot, comp)| comp))
                .collect(),
        ))
    } else {
        Ok((r_input, vec![first]))
    }
}

/// Parse a version string into a [`Version`] struct.
///
/// # Errors
///
/// Standard Nom parser errors; also, [`nom::Err::Failure`] on (hopefully impossible)
/// failure to split the already-validated characters up.
#[inline]
fn p_version(input: &str) -> IResult<&str, Version> {
    let (r_input, comps) = v_components(input)?;
    let v_chars = input.strip_suffix(r_input).ok_or_else(|| err_fail(input))?;
    Ok((r_input, Version::new(String::from(v_chars), comps)))
}

/// Parse a version string.
///
/// # Errors
///
/// Returns an error if the version string is invalid.
#[inline]
pub fn parse_version(s: &str) -> Result<Version, VParseError> {
    let (left, res) = p_version(s)
        .map_err(clone_err_input)
        .context("Could not parse a version string")
        .map_err(|err| VParseError::ParseFailure(s.to_owned(), err))?;
    if left.is_empty() {
        Ok(res)
    } else {
        Err(VParseError::ParseLeftovers(s.to_owned(), left.len()))
    }
}

/// Parse a feature name to a string slice.
///
/// Errors:
///
/// Standard Nom parser errors.
#[inline]
fn p_feature(input: &str) -> IResult<&str, &str> {
    let (r_input, name) =
        nbytesc::is_a("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-")(input)?;
    Ok((r_input, name))
}

/// Parse a comparison operator sign ("<", ">=", etc).
///
/// Errors:
///
/// Standard Nom parser errors; also, [`nom::Err::Failure`] on (hopefully impossible)
/// failure to convert the already-validated characters to a [`BoolOpKind`] value.
#[allow(clippy::map_err_ignore)]
#[inline]
fn p_op_sign(input: &str) -> IResult<&str, BoolOpKind> {
    let (r_input, res) = nbranch::alt((
        nbytesc::tag(BoolOpKind::LE),
        nbytesc::tag(BoolOpKind::LT),
        nbytesc::tag(BoolOpKind::EQ),
        nbytesc::tag(BoolOpKind::GE),
        nbytesc::tag(BoolOpKind::GT),
    ))(input)?;
    Ok((r_input, res.parse().map_err(|_| err_fail(input))?))
}

/// Parse a comparison operator word ("lt", "ge", etc).
///
/// Errors:
///
/// Standard Nom parser errors; also, [`nom::Err::Failure`] on (hopefully impossible)
/// failure to convert the already-validated characters to a [`BoolOpKind`] value.
#[allow(clippy::map_err_ignore)]
#[inline]
fn p_op_word(input: &str) -> IResult<&str, BoolOpKind> {
    let (r_input, res) = nbranch::alt((
        nbytesc::tag(BoolOpKind::LT_S),
        nbytesc::tag(BoolOpKind::LE_S),
        nbytesc::tag(BoolOpKind::EQ_S),
        nbytesc::tag(BoolOpKind::GE_S),
        nbytesc::tag(BoolOpKind::GT_S),
    ))(input)?;
    Ok((r_input, res.parse().map_err(|_| err_fail(input))?))
}

/// Parse a comparison operator sign ("<", ">=", etc) and a version string.
///
/// Errors:
///
/// Standard Nom parser errors.
#[inline]
fn p_op_sign_and_version(input: &str) -> IResult<&str, (BoolOpKind, Version)> {
    let (r_input, res) = nseq::tuple((
        ncharc::multispace0,
        p_op_sign,
        ncharc::multispace0,
        p_version,
        ncharc::multispace0,
    ))(input)?;
    Ok((r_input, (res.1, res.3)))
}

/// Parse a comparison operator word ("lt", "ge", etc) and a version string.
///
/// Errors:
///
/// Standard Nom parser errors.
#[inline]
fn p_op_word_and_version(input: &str) -> IResult<&str, (BoolOpKind, Version)> {
    let (r_input, res) = nseq::tuple((
        ncharc::multispace1,
        p_op_word,
        ncharc::multispace1,
        p_version,
        ncharc::multispace0,
    ))(input)?;
    Ok((r_input, (res.1, res.3)))
}

/// Parse a comparison operator ("<", "ge", etc) and a version string.
///
/// # Errors
///
/// Standard Nom parser errors.
#[inline]
fn p_op_and_version(input: &str) -> IResult<&str, (BoolOpKind, Version)> {
    nbranch::alt((p_op_sign_and_version, p_op_word_and_version))(input)
}

/// Parse a single feature name or a simple expression.
///
/// Errors:
///
/// Standard Nom parser errors.
#[inline]
fn p_expr(input: &str) -> IResult<&str, Mode> {
    let (r_input, (feature, op_ver)) = nseq::pair(p_feature, ncomb::opt(p_op_and_version))(input)?;
    if let Some((op, ver)) = op_ver {
        Ok((
            r_input,
            Mode::Simple(Box::new(BoolOp::new(
                op,
                Box::new(FeatureOp::new(feature)),
                Box::new(VersionOp::from_version(ver)),
            ))),
        ))
    } else {
        Ok((r_input, Mode::Single(Box::new(FeatureOp::new(feature)))))
    }
}

/// Parse a single `feature[=version]` pair with a "1.0" version default.
///
/// # Errors
///
/// Standard Nom parser errors; also, [`nom::Err::Failure`] on (hopefully impossible)
/// failure to convert a "1.0" string to a [`Version`] struct.
#[allow(clippy::map_err_ignore)]
#[inline]
fn p_feature_version(input: &str) -> IResult<&str, (String, Version)> {
    let (r_input, (feature, version)) = nseq::pair(
        p_feature,
        ncomb::opt(nseq::pair(nbytesc::tag("="), p_version)),
    )(input)?;
    Ok((
        r_input,
        (
            feature.to_owned(),
            version.map_or_else(
                || parse_version("1.0").map_err(|_| err_fail(input)),
                |(_, ver)| Ok(ver),
            )?,
        ),
    ))
}

/// Parse a `feature=[version] feature[=version]...` line into a map.
///
/// # Errors
///
/// Standard Nom parser errors.
#[inline]
fn p_features_line(input: &str) -> IResult<&str, HashMap<String, Version>> {
    let (r_input, (_, first, rest, _)) = nseq::tuple((
        ncharc::multispace0,
        p_feature_version,
        nmulti::many0(nseq::pair(ncharc::multispace1, p_feature_version)),
        ncharc::multispace0,
    ))(input)?;
    Ok((
        r_input,
        iter::once(first)
            .chain(rest.into_iter().map(|(_, pair)| pair))
            .collect(),
    ))
}

/// Parse a feature name or a "feature op version" expression.
///
/// # Errors
///
/// Returns an error if the expression is invalid.
#[inline]
pub fn parse_expr(expr: &str) -> Result<Mode, ParseError> {
    let (left, mode) = p_expr(expr)
        .map_err(clone_err_input)
        .context("Could not parse a test expression")
        .map_err(|err| ParseError::ParseFailure(expr.to_owned(), err))?;
    if left.is_empty() {
        Ok(mode)
    } else {
        Err(ParseError::ParseLeftovers(expr.to_owned(), left.len()))
    }
}

/// Parse a line of `feature[=version]` pairs.
///
/// # Errors
///
/// Returns an error if the feature names or version strings are invalid.
#[inline]
pub fn parse_features_line(s: &str) -> Result<HashMap<String, Version>, ParseError> {
    let (left, res) = p_features_line(s)
        .map_err(clone_err_input)
        .context("Could not parse the program's features line")
        .map_err(|err| ParseError::ParseFailure(s.to_owned(), err))?;
    if left.is_empty() {
        Ok(res)
    } else {
        Err(ParseError::ParseLeftovers(s.to_owned(), left.len()))
    }
}