alpm_db/desc/
parser.rs

1//! Parser for [alpm-db-desc] files.
2//!
3//! [alpm-db-desc]: https://alpm.archlinux.page/specifications/alpm-db-desc.5.html
4
5use std::{fmt::Display, str::FromStr};
6
7use alpm_parsers::iter_str_context;
8use alpm_types::{
9    Architecture,
10    BuildDate,
11    ExtraData,
12    ExtraDataEntry,
13    FullVersion,
14    Group,
15    InstalledSize,
16    License,
17    Name,
18    OptionalDependency,
19    PackageBaseName,
20    PackageDescription,
21    PackageInstallReason,
22    PackageRelation,
23    PackageValidation,
24    Packager,
25    RelationOrSoname,
26    Url,
27};
28use strum::{Display, EnumString, VariantNames};
29use winnow::{
30    ModalResult,
31    Parser,
32    ascii::{line_ending, newline, space0, till_line_ending},
33    combinator::{
34        alt,
35        cut_err,
36        delimited,
37        eof,
38        opt,
39        peek,
40        preceded,
41        repeat,
42        repeat_till,
43        terminated,
44    },
45    error::{ContextError, ErrMode, FromExternalError, StrContext, StrContextValue},
46    token::take_while,
47};
48
49/// A known section name in an [alpm-db-desc] file.
50///
51/// Section names are e.g. `%NAME%` or `%VERSION%`.
52///
53/// [alpm-db-desc]: https://alpm.archlinux.page/specifications/alpm-db-desc.5.html
54#[derive(Clone, Debug, Display, EnumString, Eq, Hash, PartialEq, VariantNames)]
55#[strum(serialize_all = "UPPERCASE")]
56pub enum SectionKeyword {
57    /// %NAME%
58    Name,
59    /// %VERSION%
60    Version,
61    /// %BASE%
62    Base,
63    /// %DESC%
64    Desc,
65    /// %URL%
66    Url,
67    /// %ARCH%
68    Arch,
69    /// %BUILDDATE%
70    BuildDate,
71    /// %INSTALLDATE%
72    InstallDate,
73    /// %PACKAGER%
74    Packager,
75    /// %SIZE%
76    Size,
77    /// %GROUPS%
78    Groups,
79    /// %REASON%
80    Reason,
81    /// %LICENSE%
82    License,
83    /// %VALIDATION%
84    Validation,
85    /// %REPLACES%
86    Replaces,
87    /// %DEPENDS%
88    Depends,
89    /// %OPTDEPENDS%
90    OptDepends,
91    /// %CONFLICTS%
92    Conflicts,
93    /// %PROVIDES%
94    Provides,
95    /// %XDATA%
96    XData,
97}
98
99impl SectionKeyword {
100    /// Recognizes a [`SectionKeyword`] in an input string slice.
101    ///
102    /// # Examples
103    ///
104    /// ```
105    /// use alpm_db::desc::SectionKeyword;
106    ///
107    /// # fn main() -> winnow::ModalResult<()> {
108    /// let mut input = "%NAME%\nfoo\n";
109    /// let kw = SectionKeyword::parser(&mut input)?;
110    /// assert_eq!(kw, SectionKeyword::Name);
111    /// # Ok(())
112    /// # }
113    /// ```
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if the input does not start with a valid
118    /// `%SECTION%` header followed by a newline.
119    pub fn parser(input: &mut &str) -> ModalResult<Self> {
120        let section = delimited("%", take_while(1.., |c| c != '%'), "%");
121        terminated(
122            preceded(space0, section.try_map(Self::from_str)),
123            line_ending,
124        )
125        .parse_next(input)
126    }
127}
128
129/// A single logical section from a database desc file.
130#[derive(Clone, Debug)]
131pub enum Section {
132    /// %NAME%
133    Name(Name),
134    /// %VERSION%
135    Version(FullVersion),
136    /// %BASE%
137    Base(PackageBaseName),
138    /// %DESC%
139    Desc(PackageDescription),
140    /// %URL%
141    Url(Option<Url>),
142    /// %ARCH%
143    Arch(Architecture),
144    /// %BUILDDATE%
145    BuildDate(BuildDate),
146    /// %INSTALLDATE%
147    InstallDate(BuildDate),
148    /// %PACKAGER%
149    Packager(Packager),
150    /// %SIZE%
151    Size(InstalledSize),
152    /// %GROUPS%
153    Groups(Vec<Group>),
154    /// %REASON%
155    Reason(PackageInstallReason),
156    /// %LICENSE%
157    License(Vec<License>),
158    /// %VALIDATION%
159    Validation(Vec<PackageValidation>),
160    /// %REPLACES%
161    Replaces(Vec<PackageRelation>),
162    /// %DEPENDS%
163    Depends(Vec<RelationOrSoname>),
164    /// %OPTDEPENDS%
165    OptDepends(Vec<OptionalDependency>),
166    /// %CONFLICTS%
167    Conflicts(Vec<PackageRelation>),
168    /// %PROVIDES%
169    Provides(Vec<RelationOrSoname>),
170    /// %XDATA%
171    XData(ExtraData),
172}
173
174/// One or multiple newlines.
175///
176/// This also handles the case where there might be multiple lines with spaces.
177fn newlines(input: &mut &str) -> ModalResult<()> {
178    repeat(0.., line_ending).parse_next(input)
179}
180
181/// Parses a single value from the input.
182///
183/// Consumes text until the end of the current line.
184///
185/// # Errors
186///
187/// Returns an error if the next token cannot be parsed into `T`.
188fn value<T>(input: &mut &str) -> ModalResult<T>
189where
190    T: FromStr + Display,
191    T::Err: Display,
192{
193    // Parse until the end of the line and attempt conversion to `T`.
194    let value = till_line_ending.parse_to().parse_next(input)?;
195
196    // Consume the newline or handle end-of-file gracefully.
197    alt((line_ending, eof)).parse_next(input)?;
198
199    Ok(value)
200}
201
202/// Parses a single optional value from the input.
203///
204/// Consumes text until the end of the current line.
205///
206/// # Errors
207///
208/// Returns an error if the next token cannot be parsed into `Option<T>`.
209fn opt_value<T>(input: &mut &str) -> ModalResult<Option<T>>
210where
211    T: FromStr + Display,
212    T::Err: Display,
213{
214    // Parse until the end of the line and attempt conversion to `Option<T>`.
215    let value = opt(till_line_ending.parse_to()).parse_next(input)?;
216
217    // Consume the newline or handle end-of-file gracefully.
218    alt((line_ending, eof)).parse_next(input)?;
219
220    Ok(value)
221}
222
223/// Parses a list of values from the input.
224///
225/// Repeats `value()` until the next section header (`%...%`)
226/// or the end of the file.
227///
228/// # Errors
229///
230/// Returns an error if a value cannot be parsed into `T` or if the
231/// section layout does not match expectations.
232fn values<T>(input: &mut &str) -> ModalResult<Vec<T>>
233where
234    T: FromStr + Display,
235    T::Err: Display,
236{
237    let next_section = peek(preceded(newline, SectionKeyword::parser)).map(|_| ());
238
239    // Consume blank lines
240    let blank_line = terminated(space0, newline).map(|_| ());
241
242    repeat_till(0.., value, alt((next_section, blank_line, eof.map(|_| ()))))
243        .context(StrContext::Label("values"))
244        .context(StrContext::Expected(StrContextValue::Description(
245            "a list of values in the database desc file",
246        )))
247        .parse_next(input)
248        .map(|(outs, _)| outs)
249}
250
251/// Parses a single `%SECTION%` block and returns a [`Section`] variant.
252///
253/// # Errors
254///
255/// Returns an error if:
256///
257/// - the section name is invalid or not recognized,
258/// - the section body contains malformed values,
259/// - or the section does not terminate properly.
260fn section(input: &mut &str) -> ModalResult<Section> {
261    // Parse and validate the header keyword first.
262    let section_keyword = cut_err(SectionKeyword::parser)
263        .context(StrContext::Label("section name"))
264        .context(StrContext::Expected(StrContextValue::Description(
265            "a section name that is enclosed in `%` characters",
266        )))
267        .context_with(iter_str_context!([SectionKeyword::VARIANTS]))
268        .parse_next(input)?;
269
270    // Delegate to the corresponding value or values parser.
271    let section = match section_keyword {
272        SectionKeyword::Name => Section::Name(value(input)?),
273        SectionKeyword::Version => Section::Version(value(input)?),
274        SectionKeyword::Base => Section::Base(value(input)?),
275        SectionKeyword::Desc => Section::Desc(value(input)?),
276        SectionKeyword::Url => Section::Url(opt_value(input)?),
277        SectionKeyword::Arch => Section::Arch(value(input)?),
278        SectionKeyword::BuildDate => Section::BuildDate(value(input)?),
279        SectionKeyword::InstallDate => Section::InstallDate(value(input)?),
280        SectionKeyword::Packager => Section::Packager(value(input)?),
281        SectionKeyword::Size => Section::Size(value(input)?),
282        SectionKeyword::Groups => Section::Groups(values(input)?),
283        SectionKeyword::Reason => Section::Reason(value(input)?),
284        SectionKeyword::License => Section::License(values(input)?),
285        SectionKeyword::Validation => Section::Validation(values(input)?),
286        SectionKeyword::Replaces => Section::Replaces(values(input)?),
287        SectionKeyword::Depends => Section::Depends(values(input)?),
288        SectionKeyword::OptDepends => Section::OptDepends(values(input)?),
289        SectionKeyword::Conflicts => Section::Conflicts(values(input)?),
290        SectionKeyword::Provides => Section::Provides(values(input)?),
291        SectionKeyword::XData => {
292            let entries: Vec<ExtraDataEntry> = values(input)?;
293            let xdata = entries
294                .try_into()
295                .map_err(|e| ErrMode::Cut(ContextError::from_external_error(input, e)))?;
296            Section::XData(xdata)
297        }
298    };
299
300    Ok(section)
301}
302
303/// Parses all `%SECTION%` blocks from the given input into a list of [`Section`]s.
304///
305/// This is the top-level parser used by the higher-level file constructors.
306///
307/// # Errors
308///
309/// Returns an error if:
310///
311/// - any section header is missing or malformed,
312/// - a section value list fails to parse,
313/// - or the overall structure of the file is inconsistent.
314pub(crate) fn sections(input: &mut &str) -> ModalResult<Vec<Section>> {
315    cut_err(repeat_till(
316        0..,
317        preceded(opt(newline), section),
318        terminated(opt(newlines), eof),
319    ))
320    .context(StrContext::Label("sections"))
321    .context(StrContext::Expected(StrContextValue::Description(
322        "a section in the database desc file",
323    )))
324    .parse_next(input)
325    .map(|(sections, _)| sections)
326}