use std::{
fmt::{Display, Formatter},
str::FromStr,
};
use alpm_parsers::{iter_char_context, iter_str_context};
use serde::{Deserialize, Serialize};
use strum::VariantNames;
use winnow::{
ModalResult,
Parser,
combinator::{alt, cut_err, eof, fail, opt, peek, repeat},
error::{
AddContext,
ContextError,
ErrMode,
ParserError,
StrContext,
StrContextValue::{self, *},
},
stream::Stream,
token::{one_of, rest, take_until},
};
use crate::{
Architecture,
FullVersion,
Name,
PackageFileName,
PackageRelation,
VersionComparison,
VersionRequirement,
error::Error,
};
fn option_bool_parser(input: &mut &str) -> ModalResult<bool> {
let alphanum = |c: char| c.is_ascii_alphanumeric();
let special_first_chars = ['-', '.', '_', '!'];
let valid_chars = one_of((alphanum, special_first_chars));
cut_err(peek(valid_chars))
.context(StrContext::Expected(CharLiteral('!')))
.context(StrContext::Expected(Description(
"ASCII alphanumeric character",
)))
.context_with(iter_char_context!(special_first_chars))
.parse_next(input)?;
Ok(opt('!').parse_next(input)?.is_none())
}
fn option_name_parser<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
let alphanum = |c: char| c.is_ascii_alphanumeric();
let special_chars = ['-', '.', '_'];
let valid_chars = one_of((alphanum, special_chars));
let name = repeat::<_, _, (), _, _>(0.., valid_chars)
.take()
.parse_next(input)?;
eof.context(StrContext::Label("character in makepkg option"))
.context(StrContext::Expected(Description(
"ASCII alphanumeric character",
)))
.context_with(iter_char_context!(special_chars))
.parse_next(input)?;
Ok(name)
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MakepkgOption {
BuildEnvironment(BuildEnvironmentOption),
Package(PackageOption),
}
impl MakepkgOption {
pub fn parser(input: &mut &str) -> ModalResult<Self> {
alt((
BuildEnvironmentOption::parser.map(MakepkgOption::BuildEnvironment),
PackageOption::parser.map(MakepkgOption::Package),
fail.context(StrContext::Label("packaging or build environment option"))
.context_with(iter_str_context!([
BuildEnvironmentOption::VARIANTS.to_vec(),
PackageOption::VARIANTS.to_vec()
])),
))
.parse_next(input)
}
}
impl FromStr for MakepkgOption {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::parser.parse(s)?)
}
}
impl Display for MakepkgOption {
fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
match self {
MakepkgOption::BuildEnvironment(option) => write!(fmt, "{option}"),
MakepkgOption::Package(option) => write!(fmt, "{option}"),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, VariantNames)]
#[serde(rename_all = "lowercase")]
pub enum BuildEnvironmentOption {
#[strum(serialize = "buildflags")]
BuildFlags(bool),
#[strum(serialize = "ccache")]
Ccache(bool),
#[strum(serialize = "check")]
Check(bool),
#[strum(serialize = "color")]
Color(bool),
#[strum(serialize = "distcc")]
Distcc(bool),
#[strum(serialize = "sign")]
Sign(bool),
#[strum(serialize = "makeflags")]
MakeFlags(bool),
}
impl BuildEnvironmentOption {
pub fn new(option: &str) -> Result<Self, Error> {
Self::from_str(option)
}
pub fn name(&self) -> &str {
match self {
Self::BuildFlags(_) => "buildflags",
Self::Ccache(_) => "ccache",
Self::Check(_) => "check",
Self::Color(_) => "color",
Self::Distcc(_) => "distcc",
Self::MakeFlags(_) => "makeflags",
Self::Sign(_) => "sign",
}
}
pub fn on(&self) -> bool {
match self {
Self::BuildFlags(on)
| Self::Ccache(on)
| Self::Check(on)
| Self::Color(on)
| Self::Distcc(on)
| Self::MakeFlags(on)
| Self::Sign(on) => *on,
}
}
pub fn parser(input: &mut &str) -> ModalResult<Self> {
let on = option_bool_parser.parse_next(input)?;
let mut name = option_name_parser.parse_next(input)?;
alt((
"buildflags".value(Self::BuildFlags(on)),
"ccache".value(Self::Ccache(on)),
"check".value(Self::Check(on)),
"color".value(Self::Color(on)),
"distcc".value(Self::Distcc(on)),
"makeflags".value(Self::MakeFlags(on)),
"sign".value(Self::Sign(on)),
fail.context(StrContext::Label("makepkg build environment option"))
.context_with(iter_str_context!([BuildEnvironmentOption::VARIANTS])),
))
.parse_next(&mut name)
}
}
impl FromStr for BuildEnvironmentOption {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::parser.parse(s)?)
}
}
impl Display for BuildEnvironmentOption {
fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
write!(fmt, "{}{}", if self.on() { "" } else { "!" }, self.name())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, VariantNames)]
#[serde(rename_all = "lowercase")]
pub enum PackageOption {
#[strum(serialize = "autodeps")]
AutoDeps(bool),
#[strum(serialize = "debug")]
Debug(bool),
#[strum(serialize = "docs")]
Docs(bool),
#[strum(serialize = "emptydirs")]
EmptyDirs(bool),
#[strum(serialize = "libtool")]
Libtool(bool),
#[strum(serialize = "lto")]
Lto(bool),
#[strum(serialize = "pestrip")]
PEStrip(bool),
#[strum(serialize = "purge")]
Purge(bool),
#[strum(serialize = "staticlibs")]
StaticLibs(bool),
#[strum(serialize = "strip")]
Strip(bool),
#[strum(serialize = "zipman")]
Zipman(bool),
}
impl PackageOption {
pub fn new(option: &str) -> Result<Self, Error> {
Self::from_str(option)
}
pub fn name(&self) -> &str {
match self {
Self::AutoDeps(_) => "autodeps",
Self::Debug(_) => "debug",
Self::Docs(_) => "docs",
Self::EmptyDirs(_) => "emptydirs",
Self::Libtool(_) => "libtool",
Self::Lto(_) => "lto",
Self::PEStrip(_) => "pestrip",
Self::Purge(_) => "purge",
Self::StaticLibs(_) => "staticlibs",
Self::Strip(_) => "strip",
Self::Zipman(_) => "zipman",
}
}
pub fn on(&self) -> bool {
match self {
Self::AutoDeps(on)
| Self::Debug(on)
| Self::Docs(on)
| Self::EmptyDirs(on)
| Self::Libtool(on)
| Self::Lto(on)
| Self::Purge(on)
| Self::PEStrip(on)
| Self::StaticLibs(on)
| Self::Strip(on)
| Self::Zipman(on) => *on,
}
}
pub fn parser(input: &mut &str) -> ModalResult<Self> {
let on = option_bool_parser.parse_next(input)?;
let mut name = option_name_parser.parse_next(input)?;
alt((
"autodeps".value(Self::AutoDeps(on)),
"debug".value(Self::Debug(on)),
"docs".value(Self::Docs(on)),
"emptydirs".value(Self::EmptyDirs(on)),
"libtool".value(Self::Libtool(on)),
"lto".value(Self::Lto(on)),
"pestrip".value(Self::PEStrip(on)),
"purge".value(Self::Purge(on)),
"staticlibs".value(Self::StaticLibs(on)),
"strip".value(Self::Strip(on)),
"zipman".value(Self::Zipman(on)),
fail.context(StrContext::Label("makepkg packaging option"))
.context_with(iter_str_context!([PackageOption::VARIANTS])),
))
.parse_next(&mut name)
}
}
impl FromStr for PackageOption {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::parser.parse(s)?)
}
}
impl Display for PackageOption {
fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
write!(fmt, "{}{}", if self.on() { "" } else { "!" }, self.name())
}
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct InstalledPackage {
name: Name,
version: FullVersion,
architecture: Architecture,
}
impl InstalledPackage {
pub fn new(name: Name, version: FullVersion, architecture: Architecture) -> Self {
Self {
name,
version,
architecture,
}
}
pub fn name(&self) -> &Name {
&self.name
}
pub fn version(&self) -> &FullVersion {
&self.version
}
pub fn architecture(&self) -> &Architecture {
&self.architecture
}
pub fn to_package_relation(&self) -> PackageRelation {
PackageRelation {
name: self.name.clone(),
version_requirement: Some(VersionRequirement {
comparison: VersionComparison::Equal,
version: self.version.clone().into(),
}),
}
}
pub fn parser(input: &mut &str) -> ModalResult<Self> {
let dashes: usize = input.chars().filter(|char| char == &'-').count();
if dashes < 2 {
let context_error = ContextError::from_input(input)
.add_context(
input,
&input.checkpoint(),
StrContext::Label("alpm-package file name"),
)
.add_context(
input,
&input.checkpoint(),
StrContext::Expected(StrContextValue::Description(
concat!(
"a package name, followed by an alpm-package-version (full or full with epoch) and an architecture.",
"\nAll components must be delimited with a dash ('-')."
)
))
);
return Err(ErrMode::Cut(context_error));
}
let dashes_in_name = dashes.saturating_sub(3);
let name = cut_err(
repeat::<_, _, (), _, _>(
dashes_in_name + 1,
(opt("-"), take_until(0.., "-"), peek("-")),
)
.take()
.and_then(Name::parser),
)
.context(StrContext::Label("alpm-package-name"))
.parse_next(input)?;
"-".parse_next(input)?;
let version: FullVersion = cut_err((take_until(0.., "-"), "-", take_until(0.., "-")))
.context(StrContext::Label("alpm-package-version"))
.context(StrContext::Expected(StrContextValue::Description(
"an alpm-package-version (full or full with epoch) followed by a `-` and an architecture",
)))
.take()
.and_then(cut_err(FullVersion::parser))
.parse_next(input)?;
"-".parse_next(input)?;
let architecture = rest.and_then(Architecture::parser).parse_next(input)?;
Ok(Self {
name,
version,
architecture,
})
}
}
impl From<PackageFileName> for InstalledPackage {
fn from(value: PackageFileName) -> Self {
Self {
name: value.name,
version: value.version,
architecture: value.architecture,
}
}
}
impl FromStr for InstalledPackage {
type Err = Error;
fn from_str(s: &str) -> Result<InstalledPackage, Self::Err> {
Ok(Self::parser.parse(s)?)
}
}
impl Display for InstalledPackage {
fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
write!(fmt, "{}-{}-{}", self.name, self.version, self.architecture)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use testresult::TestResult;
use super::*;
use crate::SystemArchitecture;
#[rstest]
#[case(
"!makeflags",
MakepkgOption::BuildEnvironment(BuildEnvironmentOption::MakeFlags(false))
)]
#[case("autodeps", MakepkgOption::Package(PackageOption::AutoDeps(true)))]
#[case(
"ccache",
MakepkgOption::BuildEnvironment(BuildEnvironmentOption::Ccache(true))
)]
fn makepkg_option(#[case] input: &str, #[case] expected: MakepkgOption) {
let result = MakepkgOption::from_str(input).expect("Parser should be successful");
assert_eq!(result, expected);
}
#[rstest]
#[case(
"!somethingelse",
concat!(
"expected `buildflags`, `ccache`, `check`, `color`, `distcc`, `sign`, `makeflags`, ",
"`autodeps`, `debug`, `docs`, `emptydirs`, `libtool`, `lto`, `pestrip`, `purge`, ",
"`staticlibs`, `strip`, `zipman`",
)
)]
#[case(
"#somethingelse",
"expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
)]
fn invalid_makepkg_option(#[case] input: &str, #[case] err_snippet: &str) {
let Err(Error::ParseError(err_msg)) = MakepkgOption::from_str(input) else {
panic!("'{input}' erroneously parsed as VersionRequirement")
};
assert!(
err_msg.contains(err_snippet),
"Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
);
}
#[rstest]
#[case("autodeps", PackageOption::AutoDeps(true))]
#[case("debug", PackageOption::Debug(true))]
#[case("docs", PackageOption::Docs(true))]
#[case("emptydirs", PackageOption::EmptyDirs(true))]
#[case("!libtool", PackageOption::Libtool(false))]
#[case("lto", PackageOption::Lto(true))]
#[case("pestrip", PackageOption::PEStrip(true))]
#[case("purge", PackageOption::Purge(true))]
#[case("staticlibs", PackageOption::StaticLibs(true))]
#[case("strip", PackageOption::Strip(true))]
#[case("zipman", PackageOption::Zipman(true))]
fn package_option(#[case] s: &str, #[case] expected: PackageOption) {
let result = PackageOption::from_str(s).expect("Parser should be successful");
assert_eq!(result, expected);
}
#[rstest]
#[case(
"!somethingelse",
"expected `autodeps`, `debug`, `docs`, `emptydirs`, `libtool`, `lto`, `pestrip`, `purge`, `staticlibs`, `strip`, `zipman`"
)]
#[case(
"#somethingelse",
"expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
)]
fn invalid_package_option(#[case] input: &str, #[case] err_snippet: &str) {
let Err(Error::ParseError(err_msg)) = PackageOption::from_str(input) else {
panic!("'{input}' erroneously parsed as VersionRequirement")
};
assert!(
err_msg.contains(err_snippet),
"Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
);
}
#[rstest]
#[case("buildflags", BuildEnvironmentOption::BuildFlags(true))]
#[case("ccache", BuildEnvironmentOption::Ccache(true))]
#[case("check", BuildEnvironmentOption::Check(true))]
#[case("color", BuildEnvironmentOption::Color(true))]
#[case("distcc", BuildEnvironmentOption::Distcc(true))]
#[case("!makeflags", BuildEnvironmentOption::MakeFlags(false))]
#[case("sign", BuildEnvironmentOption::Sign(true))]
#[case("!sign", BuildEnvironmentOption::Sign(false))]
fn build_environment_option(#[case] input: &str, #[case] expected: BuildEnvironmentOption) {
let result = BuildEnvironmentOption::from_str(input).expect("Parser should be successful");
assert_eq!(result, expected);
}
#[rstest]
#[case(
"!somethingelse",
"expected `buildflags`, `ccache`, `check`, `color`, `distcc`, `sign`, `makeflags`"
)]
#[case(
"#somethingelse",
"expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
)]
fn invalid_build_environment_option(#[case] input: &str, #[case] err_snippet: &str) {
let Err(Error::ParseError(err_msg)) = BuildEnvironmentOption::from_str(input) else {
panic!("'{input}' erroneously parsed as VersionRequirement")
};
assert!(
err_msg.contains(err_snippet),
"Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
);
}
#[rstest]
#[case("#test", "invalid character in makepkg option")]
#[case("test!", "invalid character in makepkg option")]
fn invalid_option(#[case] input: &str, #[case] error_snippet: &str) {
let result = option_name_parser.parse(input);
assert!(result.is_err(), "Expected makepkg option parsing to fail");
let err = result.unwrap_err();
let pretty_error = err.to_string();
assert!(
pretty_error.contains(error_snippet),
"Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
);
}
#[rstest]
#[case(
"foo-bar-1:1.0.0-1-any",
InstalledPackage {
name: Name::new("foo-bar")?,
version: FullVersion::from_str("1:1.0.0-1")?,
architecture: Architecture::Any,
},
)]
#[case(
"foobar-1.0.0-1-x86_64",
InstalledPackage {
name: Name::new("foobar")?,
version: FullVersion::from_str("1.0.0-1")?,
architecture: SystemArchitecture::X86_64.into(),
},
)]
fn installed_from_str(#[case] s: &str, #[case] result: InstalledPackage) -> TestResult {
assert_eq!(InstalledPackage::from_str(s), Ok(result));
Ok(())
}
#[rstest]
#[case("foo-1:1.0.0-bar-any", "invalid package release")]
#[case(
"foo-1:1.0.0_any",
"expected a package name, followed by an alpm-package-version (full or full with epoch) and an architecture."
)]
#[case("packagename-30-0.1oops-any", "expected end of package release value")]
#[case("package$with$dollars-30-0.1-any", "invalid character in package name")]
#[case(
"packagename-30-0.1-any*asdf",
"invalid character in system architecture"
)]
fn installed_new_parse_error(#[case] input: &str, #[case] error_snippet: &str) {
let result = InstalledPackage::from_str(input);
assert!(result.is_err(), "Expected InstalledPackage parsing to fail");
let err = result.unwrap_err();
let pretty_error = err.to_string();
assert!(
pretty_error.contains(error_snippet),
"Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
);
}
}