Skip to main content

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::str::FromStr;
6
7use alpm_parsers::{iter_str_context, prelude::*};
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    ascii::{line_ending, newline, space0, till_line_ending},
31    combinator::{
32        alt,
33        cut_err,
34        delimited,
35        eof,
36        not,
37        opt,
38        peek,
39        preceded,
40        repeat,
41        repeat_till,
42        terminated,
43    },
44    error::ErrMode,
45    token::take_while,
46};
47
48/// A known section name in an [alpm-db-desc] file.
49///
50/// Section names are e.g. `%NAME%` or `%VERSION%`.
51///
52/// [alpm-db-desc]: https://alpm.archlinux.page/specifications/alpm-db-desc.5.html
53#[derive(Clone, Debug, Display, EnumString, Eq, Hash, PartialEq, VariantNames)]
54#[strum(serialize_all = "UPPERCASE")]
55pub enum SectionKeyword {
56    /// %NAME%
57    Name,
58    /// %VERSION%
59    Version,
60    /// %BASE%
61    Base,
62    /// %DESC%
63    Desc,
64    /// %URL%
65    Url,
66    /// %ARCH%
67    Arch,
68    /// %BUILDDATE%
69    BuildDate,
70    /// %INSTALLDATE%
71    InstallDate,
72    /// %PACKAGER%
73    Packager,
74    /// %SIZE%
75    Size,
76    /// %GROUPS%
77    Groups,
78    /// %REASON%
79    Reason,
80    /// %LICENSE%
81    License,
82    /// %VALIDATION%
83    Validation,
84    /// %REPLACES%
85    Replaces,
86    /// %DEPENDS%
87    Depends,
88    /// %OPTDEPENDS%
89    OptDepends,
90    /// %CONFLICTS%
91    Conflicts,
92    /// %PROVIDES%
93    Provides,
94    /// %XDATA%
95    XData,
96}
97
98impl SectionKeyword {
99    /// Recognizes a [`SectionKeyword`] in an input string slice.
100    ///
101    /// # Examples
102    ///
103    /// ```
104    /// use alpm_db::desc::SectionKeyword;
105    /// use alpm_parsers::prelude::*;
106    ///
107    /// # fn main() -> testresult::TestResult {
108    /// let (remaining, kw) = SectionKeyword::parser.parse_peek(Input::new("%NAME%\nfoo\n"))?;
109    /// assert_eq!(kw, SectionKeyword::Name);
110    /// assert_eq!(*remaining, "foo\n");
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<'a>(input: &mut Input<'a>) -> PResult<'a, 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<'a>(input: &mut Input<'a>) -> PResult<'a, ()> {
178    repeat(0.., line_ending).parse_next(input)
179}
180
181/// Recognizes the end of a list of values, without consuming anything.
182///
183/// A list of values ends at a blank line, at the next section header or at the end of the file.
184fn end_of_values<'a>(input: &mut Input<'a>) -> PResult<'a, ()> {
185    peek(alt((
186        line_ending.map(|_| ()),
187        SectionKeyword::parser.map(|_| ()),
188        eof.map(|_| ()),
189    )))
190    .parse_next(input)
191}
192
193/// A parser helper, which returns a [`FromStr`]-style parser for types that don't implement their
194/// own parser.
195///
196/// Parses until the end of the current line. The line ending itself is not consumed.
197/// Wraps the returned parser in a named layer.
198///
199/// # Errors
200///
201/// Returns an error if the next token cannot be parsed into `T`.
202/// The error message of `T::from_str` is preserved as external error.
203fn try_value_from<'a, T>(layer: &str) -> impl Parser<Input<'a>, T, ErrMode<ParseStack<'a>>>
204where
205    T: FromStr,
206    T::Err: std::error::Error + Send + Sync + 'static,
207{
208    till_line_ending.try_map(T::from_str).layer(layer)
209}
210
211/// A parser helper for multi-value types, which returns a [`FromStr`]-style parser for types that
212/// don't implement their own parser.
213///
214/// Parses a list of values, where each value is in its own line.
215/// Repeats until a blank line, the next section header (`%...%`) or the end of the file.
216/// The line ending of the last value is not consumed.
217///
218/// # Errors
219///
220/// Returns an error if a value cannot be parsed into `T` or if the
221/// section layout does not match expectations.
222fn try_values_from<'a, T>(layer: &str) -> impl Parser<Input<'a>, Vec<T>, ErrMode<ParseStack<'a>>>
223where
224    T: FromStr,
225    T::Err: std::error::Error + Send + Sync + 'static,
226{
227    move |input: &mut Input<'a>| -> PResult<'a, Vec<T>> {
228        repeat_till(
229            0..,
230            // A value, followed by the line ending that separates it from the next value.
231            terminated(
232                try_value_from(layer),
233                opt(terminated(line_ending, not(end_of_values))),
234            ),
235            end_of_values,
236        )
237        .map(|(outs, _)| outs)
238        .parse_next(input)
239    }
240}
241
242/// Parses a list of values, where each value is in its own line.
243///
244/// The Parser `P` is used to parse the value of each line.
245///
246/// Repeats until a blank line, the next section header (`%...%`) or the end of the file.
247/// The line ending of the last value is not consumed.
248///
249/// # Errors
250///
251/// Returns an error if a value cannot be parsed into `T` or if the
252/// section layout does not match expectations.
253fn values<'a, T, P>(
254    type_parser: P,
255    layer: &str,
256) -> impl Parser<Input<'a>, Vec<T>, ErrMode<ParseStack<'a>>>
257where
258    P: Parser<Input<'a>, T, ErrMode<ParseStack<'a>>>,
259{
260    let mut type_parser = type_parser;
261    let parser = move |input: &mut Input<'a>| -> PResult<'a, Vec<T>> {
262        repeat_till(
263            0..,
264            // A value, followed by the line ending that separates it from the next value.
265            terminated(
266                type_parser.by_ref(),
267                opt(terminated(line_ending, not(end_of_values))),
268            ),
269            end_of_values,
270        )
271        .map(|(outs, _)| outs)
272        .parse_next(input)
273    };
274
275    parser.layer(layer)
276}
277
278/// Parses a single `%SECTION%` block and returns a [`Section`] variant.
279///
280/// # Errors
281///
282/// Returns an error if:
283///
284/// - the section name is invalid or not recognized,
285/// - the section body contains malformed values,
286/// - or the section does not terminate properly.
287fn section<'a>(input: &mut Input<'a>) -> PResult<'a, Section> {
288    // Parse and validate the header keyword first.
289    let section_keyword = cut_err(SectionKeyword::parser)
290        .description("expected a valid section name that is enclosed in `%` characters.")
291        .context_with(iter_str_context!([SectionKeyword::VARIANTS]))
292        .layer("section header")
293        .parse_next(input)?;
294
295    // Delegate to the corresponding value or values parser.
296    // Every value parser stops before the line ending of its last line.
297    let section = match section_keyword {
298        SectionKeyword::Name => Section::Name(Name::parser_until_line_ending(input)?),
299        SectionKeyword::Version => Section::Version(FullVersion::parser_until_line_ending(input)?),
300        SectionKeyword::Base => Section::Base(PackageBaseName::parser_until_line_ending(input)?),
301        SectionKeyword::Desc => Section::Desc(
302            till_line_ending
303                .map(PackageDescription::new)
304                .parse_next(input)?,
305        ),
306        SectionKeyword::Url => Section::Url(
307            alt((
308                // Handle the case of an empty URL
309                till_line_ending
310                    .verify(|s: &str| s.trim().is_empty())
311                    .map(|_s: &str| None),
312                // Handle the case of an existing URL
313                till_line_ending
314                    .try_map(Url::from_str)
315                    .map(Some)
316                    .layer("url field"),
317            ))
318            .parse_next(input)?,
319        ),
320        SectionKeyword::Arch => Section::Arch(Architecture::parser_until_line_ending(input)?),
321        SectionKeyword::BuildDate => {
322            Section::BuildDate(try_value_from("build date").parse_next(input)?)
323        }
324        SectionKeyword::InstallDate => {
325            Section::InstallDate(try_value_from("install date").parse_next(input)?)
326        }
327        SectionKeyword::Packager => Section::Packager(Packager::parser_until_line_ending(input)?),
328        SectionKeyword::Size => Section::Size(try_value_from("size").parse_next(input)?),
329        SectionKeyword::Groups => Section::Groups(try_values_from("group").parse_next(input)?),
330        SectionKeyword::Reason => {
331            Section::Reason(try_value_from("install reason").parse_next(input)?)
332        }
333        SectionKeyword::License => Section::License(try_values_from("license").parse_next(input)?),
334        SectionKeyword::Validation => Section::Validation(
335            values(PackageValidation::parser_until_line_ending, "validation").parse_next(input)?,
336        ),
337        SectionKeyword::Replaces => Section::Replaces(
338            values(PackageRelation::parser_until_line_ending, "replaces").parse_next(input)?,
339        ),
340        SectionKeyword::Depends => Section::Depends(
341            values(RelationOrSoname::parser_until_line_ending, "depends").parse_next(input)?,
342        ),
343        SectionKeyword::OptDepends => Section::OptDepends(
344            values(OptionalDependency::parser_until_line_ending, "optdepends").parse_next(input)?,
345        ),
346        SectionKeyword::Conflicts => Section::Conflicts(
347            values(PackageRelation::parser_until_line_ending, "conflicts").parse_next(input)?,
348        ),
349        SectionKeyword::Provides => Section::Provides(
350            values(RelationOrSoname::parser_until_line_ending, "provides").parse_next(input)?,
351        ),
352        SectionKeyword::XData => Section::XData(
353            values(ExtraDataEntry::parser_until_line_ending, "xdata")
354                .try_map(ExtraData::try_from)
355                .parse_next(input)?,
356        ),
357    };
358
359    // Consume the newline or handle end-of-file gracefully.
360    alt((line_ending, eof)).parse_next(input)?;
361
362    Ok(section)
363}
364
365/// Parses all `%SECTION%` blocks from the given input into a list of [`Section`]s.
366///
367/// This is the top-level parser used by the higher-level file constructors.
368///
369/// # Errors
370///
371/// Returns an error if:
372///
373/// - any section header is missing or malformed,
374/// - a section value list fails to parse,
375/// - or the overall structure of the file is inconsistent.
376pub(crate) fn sections<'a>(input: &mut Input<'a>) -> PResult<'a, Vec<Section>> {
377    cut_err(repeat_till(
378        0..,
379        preceded(
380            opt(newline),
381            section.layer("alpm-repo-desc file value section"),
382        ),
383        terminated(opt(newlines), eof),
384    ))
385    .parse_next(input)
386    .map(|(sections, _)| sections)
387}