use std::{collections::HashMap, fmt::Display, path::Path, str::FromStr};
use alpm_parsers::iter_str_context;
#[cfg(doc)]
use alpm_types::Architecture;
use strum::{EnumString, VariantNames};
use winnow::{
ModalResult,
Parser,
ascii::{alphanumeric1, space0, space1},
combinator::{alt, cut_err, delimited, eof, opt, preceded, repeat, terminated, trace},
error::{StrContext, StrContextValue},
token::{none_of, one_of, take_till, take_until},
};
use crate::{bridge::run_bridge_script, error::Error};
#[derive(Clone, Debug)]
pub enum Value {
Single(String),
Array(Vec<String>),
}
impl Value {
pub fn as_vec(&self) -> Vec<&String> {
match self {
Value::Single(item) => vec![&item],
Value::Array(items) => Vec::from_iter(items.iter()),
}
}
pub fn as_owned_vec(self) -> Vec<String> {
match self {
Value::Single(item) => vec![item],
Value::Array(items) => items,
}
}
pub fn has_value(&self) -> bool {
match self {
Value::Single(_) => true,
Value::Array(items) => !items.is_empty(),
}
}
fn single_till_newline(input: &mut &str) -> ModalResult<Self> {
cut_err(delimited(
space1,
Self::parse_next_value,
(space0, alt(("\n", eof))),
))
.context(StrContext::Label("variable"))
.map(Value::Single)
.parse_next(input)
}
fn list_till_newline(input: &mut &str) -> ModalResult<Self> {
let values = repeat(0.., preceded(space1, Value::parse_next_value))
.map(Value::Array)
.parse_next(input)?;
cut_err(preceded(space0, alt(("\n", eof))))
.context(StrContext::Label("character"))
.context(StrContext::Expected(StrContextValue::Description(
"end of line or end of file.",
)))
.parse_next(input)?;
Ok(values)
}
pub fn parse_next_value(input: &mut &str) -> ModalResult<String> {
let string_value = trace(
"variable",
preceded(
'"',
cut_err(
terminated(
repeat(0.., Self::variable_character).fold(String::new, |mut string, c| {
string.push(c);
string
}),
'"',
)
.context(StrContext::Label("variable"))
.context(StrContext::Expected(
StrContextValue::Description("A string surrounded by double quotes"),
)),
),
),
)
.parse_next(input)?;
Ok(string_value)
}
pub fn variable_character(input: &mut &str) -> ModalResult<char> {
let c = none_of('"').parse_next(input)?;
if c == '\\' {
cut_err(one_of(['"', '\\']))
.context(StrContext::Label("escaped sequence"))
.context(StrContext::Expected(StrContextValue::Description(
"one of the allowed escape characters: ['\"', '\\']",
)))
.parse_next(input)
} else {
Ok(c)
}
}
}
#[derive(Clone, Debug)]
pub enum ClearableValue {
Single(Option<String>),
Array(Option<Vec<String>>),
}
impl ClearableValue {
fn single_till_newline(input: &mut &str) -> ModalResult<Self> {
let value = preceded(
space1,
terminated(opt(Value::parse_next_value), (space0, alt(("\n", eof)))),
)
.parse_next(input)?;
let Some(value) = value else {
return Ok(ClearableValue::Single(None));
};
if value.is_empty() {
return Ok(ClearableValue::Single(None));
}
Ok(ClearableValue::Single(Some(value)))
}
fn list_till_newline(input: &mut &str) -> ModalResult<Self> {
let values = opt(repeat(1.., preceded(space1, Value::parse_next_value)))
.map(ClearableValue::Array)
.parse_next(input)?;
cut_err(preceded(space0, alt(("\n", eof))))
.context(StrContext::Label("character"))
.context(StrContext::Expected(StrContextValue::Description(
"end of line or end of file.",
)))
.parse_next(input)?;
Ok(values)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Keyword {
pub keyword: String,
pub suffix: Option<String>,
}
impl Display for Keyword {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.keyword)?;
if let Some(suffix) = &self.suffix {
write!(f, "_{suffix}")?;
}
Ok(())
}
}
impl Keyword {
pub fn simple<T: ToString>(keyword: T) -> Self {
Self {
keyword: keyword.to_string(),
suffix: None,
}
}
pub(crate) fn parser(input: &mut &str) -> ModalResult<Keyword> {
let (keyword, suffix) = trace(
"keyword",
cut_err(preceded(
space1,
(
alphanumeric1,
opt(preceded('_', take_till(1.., |c| c == ' ' || c == '\n'))),
),
))
.context(StrContext::Label(
"keyword with potential architecture suffix, e.g. 'source_x86_64'",
))
.context(StrContext::Expected(StrContextValue::Description(
"alphabetic keyword with potential architecture suffix, e.g. 'source_x86_64'",
))),
)
.parse_next(input)?;
Ok(Keyword {
keyword: keyword.to_owned(),
suffix: suffix.map(ToString::to_string),
})
}
}
#[derive(Debug, EnumString, VariantNames)]
#[strum(serialize_all = "UPPERCASE")]
enum VariableType {
Array,
String,
}
impl VariableType {
pub fn parser(input: &mut &str) -> ModalResult<VariableType> {
trace(
"variable_type",
cut_err(preceded(
space1,
take_until(1.., ' ').try_map(VariableType::from_str),
))
.context_with(iter_str_context!([VariableType::VARIANTS])),
)
.parse_next(input)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct RawPackageName(pub Option<String>);
impl RawPackageName {
pub(crate) fn parser(input: &mut &str) -> ModalResult<Self> {
let package_name = trace(
"PackageName",
cut_err(preceded(
(space1, "package"),
opt(preceded('_', take_till(1.., |c| c == ' ' || c == '\n'))),
))
.context(StrContext::Expected(StrContextValue::Description(
"A 'package' function or a 'package_split-package-name' function.",
)))
.map(|opt| opt.map(ToString::to_string)),
)
.parse_next(input)?;
Ok(Self(package_name))
}
}
#[derive(Clone, Debug)]
pub struct BridgeOutput {
pub package_base: HashMap<Keyword, Value>,
pub packages: HashMap<RawPackageName, HashMap<Keyword, ClearableValue>>,
pub functions: Vec<RawPackageName>,
}
impl BridgeOutput {
pub fn from_file(pkgbuild_path: &Path) -> Result<Self, Error> {
let input = run_bridge_script(pkgbuild_path)?;
Self::from_script_output(&input)
}
pub fn from_script_output(input: &str) -> Result<Self, Error> {
Self::parser
.parse(input)
.map_err(|err| Error::BridgeParseError(format!("{err}")))
}
fn parser(input: &mut &str) -> ModalResult<Self> {
let package_base = Self::package_base(input)?;
let packages = Self::packages(input)?;
let functions = Self::functions(input)?;
let _: Option<()> = opt(repeat(0.., (space0, "\n", space0))).parse_next(input)?;
cut_err(eof)
.context(StrContext::Expected(StrContextValue::Description(
"end of file.",
)))
.parse_next(input)?;
Ok(Self {
package_base,
packages,
functions,
})
}
fn package_base(input: &mut &str) -> ModalResult<HashMap<Keyword, Value>> {
repeat(1.., Self::package_base_line).parse_next(input)
}
fn package_base_line(input: &mut &str) -> ModalResult<(Keyword, Value)> {
("VAR GLOBAL").parse_next(input)?;
let variable_type = VariableType::parser.parse_next(input)?;
let keyword = Keyword::parser.parse_next(input)?;
let value = match variable_type {
VariableType::Array => Value::list_till_newline(input)?,
VariableType::String => Value::single_till_newline(input)?,
};
Ok((keyword, value))
}
fn packages(
input: &mut &str,
) -> ModalResult<HashMap<RawPackageName, HashMap<Keyword, ClearableValue>>> {
let lines: Vec<(RawPackageName, Keyword, ClearableValue)> =
repeat(0.., Self::package_line).parse_next(input)?;
let mut packages = HashMap::new();
for (package_name, keyword, value) in lines {
let value_map: &mut HashMap<Keyword, ClearableValue> =
packages.entry(package_name).or_default();
value_map.insert(keyword, value);
}
Ok(packages)
}
fn package_line(input: &mut &str) -> ModalResult<(RawPackageName, Keyword, ClearableValue)> {
("VAR FUNCTION").parse_next(input)?;
let package_name = RawPackageName::parser.parse_next(input)?;
let variable_type = VariableType::parser.parse_next(input)?;
let keyword = Keyword::parser.parse_next(input)?;
let value = match variable_type {
VariableType::Array => ClearableValue::list_till_newline(input)?,
VariableType::String => ClearableValue::single_till_newline(input)?,
};
Ok((package_name, keyword, value))
}
fn functions(input: &mut &str) -> ModalResult<Vec<RawPackageName>> {
repeat(0.., Self::function_line).parse_next(input)
}
fn function_line(input: &mut &str) -> ModalResult<RawPackageName> {
("FUNCTION").parse_next(input)?;
let package_name = RawPackageName::parser.parse_next(input)?;
cut_err((space0, alt((eof, "\n"))))
.context(StrContext::Label("character"))
.context(StrContext::Expected(StrContextValue::Description(
"end of line or end of file.",
)))
.parse_next(input)?;
Ok(package_name)
}
}