Skip to main content

alpm_repo_db/desc/
parser.rs

1//! Parser for [alpm-repo-desc] files.
2//!
3//! [alpm-repo-desc]: https://alpm.archlinux.page/specifications/alpm-repo-desc.5.html
4
5use std::str::FromStr;
6
7use alpm_parsers::{iter_str_context, prelude::*};
8use alpm_types::{
9    Architecture,
10    Base64OpenPGPSignature,
11    BuildDate,
12    CompressedSize,
13    FullVersion,
14    Group,
15    InstalledSize,
16    License,
17    Md5Checksum,
18    Name,
19    OptionalDependency,
20    PackageBaseName,
21    PackageDescription,
22    PackageFileName,
23    PackageRelation,
24    Packager,
25    RelationOrSoname,
26    Sha256Checksum,
27    Url,
28};
29use strum::{Display, EnumString, VariantNames};
30use winnow::{
31    ascii::{line_ending, newline, space0, till_line_ending},
32    combinator::{
33        alt,
34        cut_err,
35        delimited,
36        eof,
37        not,
38        opt,
39        peek,
40        preceded,
41        repeat,
42        repeat_till,
43        terminated,
44    },
45    error::ErrMode,
46    token::take_while,
47};
48
49/// A known section name in an [alpm-repo-desc] file.
50///
51/// Section names are e.g. `%NAME%` or `%VERSION%`.
52///
53/// [alpm-repo-desc]: https://alpm.archlinux.page/specifications/alpm-repo-desc.5.html
54#[derive(Clone, Debug, Display, EnumString, Eq, Hash, PartialEq, VariantNames)]
55#[strum(serialize_all = "UPPERCASE")]
56pub enum SectionKeyword {
57    /// %FILENAME%
58    Filename,
59    /// %Name%
60    Name,
61    /// %BASE%
62    Base,
63    /// %VERSION%
64    Version,
65    /// %DESC%
66    Desc,
67    /// %GROUPS%
68    Groups,
69    /// %CSIZE%
70    CSize,
71    /// %ISIZE%
72    ISize,
73    /// %MD5SUM%
74    Md5Sum,
75    /// %SHA256SUM%
76    Sha256Sum,
77    /// %PGPSIG%
78    PgpSig,
79    /// %URL%
80    Url,
81    /// %LICENSE%
82    License,
83    /// %ARCH%
84    Arch,
85    /// %BUILDDATE%
86    BuildDate,
87    /// %PACKAGER%
88    Packager,
89    /// %REPLACES%
90    Replaces,
91    /// %CONFLICTS%
92    Conflicts,
93    /// %PROVIDES%
94    Provides,
95    /// %DEPENDS%
96    Depends,
97    /// %OPTDEPENDS%
98    OptDepends,
99    /// %MAKEDEPENDS%
100    MakeDepends,
101    /// %CHECKDEPENDS%
102    CheckDepends,
103}
104
105impl SectionKeyword {
106    /// Recognizes a [`SectionKeyword`] in an input string slice.
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// use alpm_parsers::prelude::*;
112    /// use alpm_repo_db::desc::SectionKeyword;
113    ///
114    /// # fn main() -> testresult::TestResult {
115    /// let (remaining, kw) = SectionKeyword::parser.parse_peek(Input::new("%NAME%\nfoo\n"))?;
116    /// assert_eq!(kw, SectionKeyword::Name);
117    /// assert_eq!(*remaining, "foo\n");
118    /// # Ok(())
119    /// # }
120    /// ```
121    ///
122    /// # Errors
123    ///
124    /// Returns an error if the input does not start with a valid
125    /// `%SECTION%` header followed by a newline.
126    pub fn parser<'a>(input: &mut Input<'a>) -> PResult<'a, Self> {
127        let section = delimited("%", take_while(1.., |c| c != '%'), "%");
128        terminated(
129            preceded(space0, section.try_map(Self::from_str)),
130            line_ending,
131        )
132        .parse_next(input)
133    }
134}
135
136/// A single logical section from a repo desc file.
137#[derive(Clone, Debug)]
138pub enum Section {
139    /// %FILENAME%
140    Filename(PackageFileName),
141    /// %NAME%
142    Name(Name),
143    /// %BASE%
144    Base(PackageBaseName),
145    /// %VERSION%
146    Version(FullVersion),
147    /// %DESC%
148    Desc(PackageDescription),
149    /// %GROUPS%
150    Groups(Vec<Group>),
151    /// %CSIZE%
152    CSize(CompressedSize),
153    /// %ISIZE%
154    ISize(InstalledSize),
155    /// %MD5SUM%
156    Md5Sum(Md5Checksum),
157    /// %SHA256SUM%
158    Sha256Sum(Sha256Checksum),
159    /// %PGPSIG%
160    PgpSig(Base64OpenPGPSignature),
161    /// %URL%
162    Url(Option<Url>),
163    /// %LICENSE%
164    License(Vec<License>),
165    /// %ARCH%
166    Arch(Architecture),
167    /// %BUILDDATE%
168    BuildDate(BuildDate),
169    /// %PACKAGER%
170    Packager(Packager),
171    /// %REPLACES%
172    Replaces(Vec<PackageRelation>),
173    /// %CONFLICTS%
174    Conflicts(Vec<PackageRelation>),
175    /// %PROVIDES%
176    Provides(Vec<RelationOrSoname>),
177    /// %DEPENDS%
178    Depends(Vec<RelationOrSoname>),
179    /// %OPTDEPENDS%
180    OptDepends(Vec<OptionalDependency>),
181    /// %MAKEDEPENDS%
182    MakeDepends(Vec<PackageRelation>),
183    /// %CHECKDEPENDS%
184    CheckDepends(Vec<PackageRelation>),
185}
186
187/// One or multiple newlines.
188///
189/// This also handles the case where there might be multiple lines with spaces.
190fn newlines<'a>(input: &mut Input<'a>) -> PResult<'a, ()> {
191    repeat(0.., line_ending).parse_next(input)
192}
193
194/// Recognizes the end of a list of values, without consuming anything.
195///
196/// A list of values ends at a blank line, at the next section header or at the end of the file.
197fn end_of_values<'a>(input: &mut Input<'a>) -> PResult<'a, ()> {
198    peek(alt((
199        line_ending.map(|_| ()),
200        SectionKeyword::parser.map(|_| ()),
201        eof.map(|_| ()),
202    )))
203    .parse_next(input)
204}
205
206/// A parser helper, which returns a [`FromStr`]-style parser for types that don't implement their
207/// own parser.
208///
209/// Parses until the end of the current line. The line ending itself is not consumed.
210/// Wraps the returned parser in a named layer.
211///
212/// # Errors
213///
214/// Returns an error if the next token cannot be parsed into `T`.
215/// The error message of `T::from_str` is preserved as external error.
216fn try_value_from<'a, T>(layer: &str) -> impl Parser<Input<'a>, T, ErrMode<ParseStack<'a>>>
217where
218    T: FromStr,
219    T::Err: std::error::Error + Send + Sync + 'static,
220{
221    till_line_ending.try_map(T::from_str).layer(layer)
222}
223
224/// A parser helper for multi-value types, which returns a [`FromStr`]-style parser for types that
225/// don't implement their own parser.
226///
227/// Parses a list of values, where each value is in its own line.
228/// Repeats until a blank line, the next section header (`%...%`) or the end of the file.
229/// The line ending of the last value is not consumed.
230///
231/// # Errors
232///
233/// Returns an error if a value cannot be parsed into `T` or if the
234/// section layout does not match expectations.
235fn try_values_from<'a, T>(layer: &str) -> impl Parser<Input<'a>, Vec<T>, ErrMode<ParseStack<'a>>>
236where
237    T: FromStr,
238    T::Err: std::error::Error + Send + Sync + 'static,
239{
240    move |input: &mut Input<'a>| -> PResult<'a, Vec<T>> {
241        repeat_till(
242            0..,
243            // A value, followed by the line ending that separates it from the next value.
244            terminated(
245                try_value_from(layer),
246                opt(terminated(line_ending, not(end_of_values))),
247            ),
248            end_of_values,
249        )
250        .map(|(outs, _)| outs)
251        .parse_next(input)
252    }
253}
254
255/// Parses a list of values, where each value is in its own line.
256/// The Parser `P` is used to parse the value of each line.
257///
258/// Repeats until a blank line, the next section header (`%...%`) or the end of the file.
259/// The line ending of the last value is not consumed.
260///
261/// # Errors
262///
263/// Returns an error if a value cannot be parsed into `T` or if the
264/// section layout does not match expectations.
265fn values<'a, T, P>(
266    type_parser: P,
267    layer: &str,
268) -> impl Parser<Input<'a>, Vec<T>, ErrMode<ParseStack<'a>>>
269where
270    P: Parser<Input<'a>, T, ErrMode<ParseStack<'a>>>,
271{
272    let mut type_parser = type_parser;
273    let parser = move |input: &mut Input<'a>| -> PResult<'a, Vec<T>> {
274        repeat_till(
275            0..,
276            // A value, followed by the line ending that separates it from the next value.
277            terminated(
278                type_parser.by_ref(),
279                opt(terminated(line_ending, not(end_of_values))),
280            ),
281            end_of_values,
282        )
283        .map(|(outs, _)| outs)
284        .parse_next(input)
285    };
286
287    parser.layer(layer)
288}
289
290/// Parses a single `%SECTION%` block and returns a [`Section`] variant.
291///
292/// # Errors
293///
294/// Returns an error if:
295///
296/// - the section name is invalid or not recognized,
297/// - the section body contains malformed values,
298/// - or the section does not terminate properly.
299fn section<'a>(input: &mut Input<'a>) -> PResult<'a, Section> {
300    // Parse and validate the header keyword first.
301    let section_keyword = cut_err(SectionKeyword::parser)
302        .description("expected a valid section name that is enclosed in `%` characters:")
303        .context_with(iter_str_context!([SectionKeyword::VARIANTS]))
304        .layer("section header")
305        .parse_next(input)?;
306
307    // Delegate to the corresponding value or values parser.
308    // Every value parser stops before the line ending of its last line.
309    let section = match section_keyword {
310        SectionKeyword::Filename => {
311            Section::Filename(try_value_from("file name").parse_next(input)?)
312        }
313        SectionKeyword::Name => Section::Name(Name::parser_until_line_ending(input)?),
314        SectionKeyword::Base => Section::Base(PackageBaseName::parser_until_line_ending(input)?),
315        SectionKeyword::Version => Section::Version(FullVersion::parser_until_line_ending(input)?),
316        SectionKeyword::Desc => Section::Desc(
317            till_line_ending
318                .map(PackageDescription::new)
319                .parse_next(input)?,
320        ),
321        SectionKeyword::Groups => Section::Groups(try_values_from("group").parse_next(input)?),
322        SectionKeyword::CSize => {
323            Section::CSize(try_value_from("compressed size").parse_next(input)?)
324        }
325        SectionKeyword::ISize => {
326            Section::ISize(try_value_from("installed size").parse_next(input)?)
327        }
328        SectionKeyword::Md5Sum => Section::Md5Sum(Md5Checksum::parser_until_line_ending(input)?),
329        SectionKeyword::Sha256Sum => {
330            Section::Sha256Sum(Sha256Checksum::parser_until_line_ending(input)?)
331        }
332        SectionKeyword::PgpSig => {
333            Section::PgpSig(try_value_from("pgp signature").parse_next(input)?)
334        }
335        SectionKeyword::Url => Section::Url(
336            alt((
337                // Handle the case of an empty URL
338                till_line_ending
339                    .verify(|s: &str| s.trim().is_empty())
340                    .map(|_s: &str| None),
341                // Handle the case of an existing URL
342                till_line_ending
343                    .try_map(Url::from_str)
344                    .map(Some)
345                    .layer("url field"),
346            ))
347            .parse_next(input)?,
348        ),
349        SectionKeyword::License => Section::License(try_values_from("license").parse_next(input)?),
350        SectionKeyword::Arch => Section::Arch(Architecture::parser_until_line_ending(input)?),
351        SectionKeyword::BuildDate => {
352            Section::BuildDate(try_value_from("build date").parse_next(input)?)
353        }
354        SectionKeyword::Packager => Section::Packager(Packager::parser_until_line_ending(input)?),
355        SectionKeyword::Replaces => Section::Replaces(
356            values(PackageRelation::parser_until_line_ending, "replaces").parse_next(input)?,
357        ),
358        SectionKeyword::Conflicts => Section::Conflicts(
359            values(PackageRelation::parser_until_line_ending, "conflicts").parse_next(input)?,
360        ),
361        SectionKeyword::Provides => Section::Provides(
362            values(RelationOrSoname::parser_until_line_ending, "provides").parse_next(input)?,
363        ),
364        SectionKeyword::Depends => Section::Depends(
365            values(RelationOrSoname::parser_until_line_ending, "depends").parse_next(input)?,
366        ),
367        SectionKeyword::OptDepends => Section::OptDepends(
368            values(OptionalDependency::parser_until_line_ending, "optdepends").parse_next(input)?,
369        ),
370        SectionKeyword::MakeDepends => Section::MakeDepends(
371            values(PackageRelation::parser_until_line_ending, "makedepends").parse_next(input)?,
372        ),
373        SectionKeyword::CheckDepends => Section::CheckDepends(
374            values(PackageRelation::parser_until_line_ending, "checkdepends").parse_next(input)?,
375        ),
376    };
377
378    // Consume the newline or handle end-of-file gracefully.
379    alt((line_ending, eof)).parse_next(input)?;
380
381    Ok(section)
382}
383
384/// Parses all `%SECTION%` blocks from the given input into a list of [`Section`]s.
385///
386/// This is the top-level parser used by the higher-level file constructors.
387///
388/// # Errors
389///
390/// Returns an error if:
391///
392/// - any section header is missing or malformed,
393/// - a section value list fails to parse,
394/// - or the overall structure of the file is inconsistent.
395pub(crate) fn sections<'a>(input: &mut Input<'a>) -> PResult<'a, Vec<Section>> {
396    cut_err(repeat_till(
397        0..,
398        preceded(
399            opt(newline),
400            section.layer("alpm-db-desc file value section"),
401        ),
402        terminated(opt(newlines), eof),
403    ))
404    .parse_next(input)
405    .map(|(sections, _)| sections)
406}