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