alpm_srcinfo/source_info/
parser.rs

1//! The parser for SRCINFO data.
2//!
3//! It returns a rather raw line-based, but already typed representation of the contents.
4//! The representation is not useful for end-users as it provides data that is not yet validated.
5use std::str::FromStr;
6
7use alpm_parsers::iter_str_context;
8use alpm_types::{
9    Architecture,
10    Backup,
11    Changelog,
12    Epoch,
13    Group,
14    Install,
15    License,
16    MakepkgOption,
17    Name,
18    OpenPGPIdentifier,
19    OptionalDependency,
20    PackageDescription,
21    PackageRelation,
22    PackageRelease,
23    PackageVersion,
24    RelationOrSoname,
25    RelativePath,
26    SkippableChecksum,
27    Source,
28    Url,
29    digests::{Blake2b512, Md5, Sha1, Sha224, Sha256, Sha384, Sha512},
30};
31use strum::{EnumString, VariantNames};
32use winnow::{
33    ModalResult,
34    Parser,
35    ascii::{alpha1, alphanumeric1, line_ending, multispace0, newline, space0, till_line_ending},
36    combinator::{
37        alt,
38        cut_err,
39        eof,
40        fail,
41        opt,
42        peek,
43        preceded,
44        repeat,
45        repeat_till,
46        terminated,
47        trace,
48    },
49    error::{ErrMode, ParserError, StrContext, StrContextValue},
50    token::{take_till, take_until},
51};
52
53/// Recognizes the ` = ` delimiter between keywords.
54///
55/// This function expects the delimiter to exist.
56fn delimiter<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
57    cut_err(" = ")
58        .context(StrContext::Label("delimiter"))
59        .context(StrContext::Expected(StrContextValue::Description(
60            "an equal sign surrounded by spaces: ' = '.",
61        )))
62        .parse_next(input)
63}
64
65/// Recognizes all content until the end of line.
66///
67/// This function is called after a ` = ` has been recognized using [`delimiter`].
68/// It extends upon winnow's [`till_line_ending`] by also consuming the newline character.
69/// [`till_line_ending`]: <https://docs.rs/winnow/latest/winnow/ascii/fn.till_line_ending.html>
70fn till_line_end<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
71    // Get the content til the end of line.
72    let out = till_line_ending.parse_next(input)?;
73
74    // Consume the newline. This is `opt` in case we hit `eof`, which is also handled by winnow's
75    // `till_line_ending`
76    opt(line_ending).parse_next(input)?;
77
78    Ok(out)
79}
80
81/// An arbitrarily typed attribute that is specific to an [alpm-architecture].
82///
83/// This type is designed to wrap **any** type that is architecture specific.
84/// For example, all checksums may be architecture specific.
85///
86/// # Example
87///
88/// ```text
89/// # Without architecture
90/// sha256 = 0db1b39fd70097c6733cdcce56b1559ece5521ec1aad9ee1d520dda73eff03d0
91///
92/// # With architecture
93/// sha256_x86_64 = 0db1b39fd70097c6733cdcce56b1559ece5521ec1aad9ee1d520dda73eff03d0
94/// ```
95///
96/// The above would be reflected by the following code.
97/// ```
98/// use std::str::FromStr;
99///
100/// use alpm_srcinfo::source_info::parser::ArchProperty;
101/// use alpm_types::{Architecture, Sha256Checksum};
102///
103/// # fn main() -> Result<(), alpm_srcinfo::Error> {
104/// let without_architecture = ArchProperty {
105///     architecture: None,
106///     value: Sha256Checksum::from_str(
107///         "0db1b39fd70097c6733cdcce56b1559ece5521ec1aad9ee1d520dda73eff03d0",
108///     )?,
109/// };
110///
111/// let with_architecture = ArchProperty {
112///     architecture: Some(Architecture::X86_64),
113///     value: Sha256Checksum::from_str(
114///         "0db1b39fd70097c6733cdcce56b1559ece5521ec1aad9ee1d520dda73eff03d0",
115///     )?,
116/// };
117///
118/// # Ok(())
119/// # }
120/// ```
121///
122/// [alpm-architecture]: <https://alpm.archlinux.page/specifications/alpm-architecture.7.html>
123#[derive(Debug)]
124pub struct ArchProperty<T> {
125    /// The optional [alpm-architecture] of the `value`.
126    ///
127    /// If `architecture` is [`None`] it is considered to be `"any"`.
128    /// [alpm-architecture]: <https://alpm.archlinux.page/specifications/alpm-architecture.7.html>
129    pub architecture: Option<Architecture>,
130    /// The architecture specific type.
131    pub value: T,
132}
133
134/// Recognizes and returns the architecture suffix of a keyword, if it exists.
135///
136/// Returns [`None`] if no architecture suffix is found.
137///
138/// ## Examples
139/// ```txt
140/// sha256sums_i386 = 0db1b39fd70097c6733cdcce56b1559ece5521ec1aad9ee1d520dda73eff03d0
141///           ^^^^^
142///         This is the suffix with `i386` being the architecture.
143/// ```
144pub fn architecture_suffix(input: &mut &str) -> ModalResult<Option<Architecture>> {
145    // First up, check if there's an underscore.
146    // If there's none, there's no suffix and we can return early.
147    let underscore = opt('_').parse_next(input)?;
148    if underscore.is_none() {
149        return Ok(None);
150    }
151
152    // There has been an underscore, so now we **expect** an architecture to be there and we have
153    // to fail hard if that doesn't work.
154    // We now grab all content until the expected space of the delimiter and map it to an
155    // alpm_types::Architecture.
156    let architecture =
157        cut_err(take_till(0.., |c| c == ' ' || c == '=').and_then(Architecture::parser))
158            .parse_next(input)?;
159
160    Ok(Some(architecture))
161}
162
163/// Track empty/comment lines
164#[derive(Debug)]
165pub enum Ignored {
166    /// An empty line
167    EmptyLine,
168
169    /// A commented line.
170    Comment(String),
171}
172
173/// A representation of all high-level components of parsed SRCINFO data.
174#[derive(Debug)]
175pub struct SourceInfoContent {
176    /// Empty or comment lines that occur outside of `pkgbase` or `pkgname` sections.
177    pub preceding_lines: Vec<Ignored>,
178    /// The raw package base data.
179    pub package_base: RawPackageBase,
180    /// The list of raw package data.
181    pub packages: Vec<RawPackage>,
182}
183
184impl SourceInfoContent {
185    /// Parses the start of the file in case it contains one or more empty lines or comment lines.
186    ///
187    /// This consumes the first few lines until the `pkgbase` section is hit.
188    /// Further comments and newlines are handled in the scope of the respective `pkgbase`/`pkgname`
189    /// sections.
190    fn preceding_lines_parser(input: &mut &str) -> ModalResult<Ignored> {
191        trace(
192            "preceding_lines",
193            alt((
194                terminated(("#", take_until(0.., "\n")).take(), line_ending)
195                    .map(|s: &str| Ignored::Comment(s.to_string())),
196                terminated(space0, line_ending).map(|_s: &str| Ignored::EmptyLine),
197            )),
198        )
199        .parse_next(input)
200    }
201
202    /// Recognizes a complete SRCINFO file from a string slice.
203    ///
204    /// ```rust
205    /// use alpm_srcinfo::source_info::parser::SourceInfoContent;
206    /// use winnow::Parser;
207    ///
208    /// # fn main() -> Result<(), alpm_srcinfo::Error> {
209    /// let source_info_data = r#"
210    /// pkgbase = example
211    ///     pkgver = 1.0.0
212    ///     epoch = 1
213    ///     pkgrel = 1
214    ///     pkgdesc = A project that does something
215    ///     url = https://example.org/
216    ///     arch = x86_64
217    ///     depends = glibc
218    ///     optdepends = python: for special-python-script.py
219    ///     makedepends = cmake
220    ///     checkdepends = extra-test-tool
221    ///
222    /// pkgname = example
223    ///     depends = glibc
224    ///     depends = gcc-libs
225    /// "#;
226    ///
227    /// // Parse the given srcinfo content.
228    /// let parsed = SourceInfoContent::parser
229    ///     .parse(source_info_data)
230    ///     .map_err(|err| alpm_srcinfo::Error::ParseError(format!("{err}")))?;
231    /// # Ok(())
232    /// # }
233    /// ```
234    pub fn parser(input: &mut &str) -> ModalResult<SourceInfoContent> {
235        // Handle any comments or empty lines at the start of the line..
236        let preceding_lines: Vec<Ignored> =
237            repeat(0.., Self::preceding_lines_parser).parse_next(input)?;
238
239        // At the first part of any SRCINFO file, a `pkgbase` section is expected which sets the
240        // base metadata and the default values for all packages to come.
241        let package_base = RawPackageBase::parser.parse_next(input)?;
242
243        // Trim newlines or spaces between the pkgbase section and the following pkgname section.
244        let _ = multispace0.parse_next(input)?;
245
246        // Afterwards one or more `pkgname` declarations are to follow.
247        //
248        // `RawPackage::parser` expects all newlines and leading whitespaces to be trimmed.
249        // This is explicitly done once at the start (see above) and implicitly via `terminated` in
250        // between the repeats.
251        multispace0.parse_next(input)?;
252        let (packages, _eof): (Vec<RawPackage>, _) =
253            repeat_till(0.., terminated(RawPackage::parser, multispace0), eof).parse_next(input)?;
254
255        // Fail with a special error if there's no package section.
256        if packages.is_empty() {
257            fail.context(StrContext::Expected(StrContextValue::Description(
258                "a pkgname section",
259            )))
260            .parse_next(input)?;
261        }
262
263        Ok(SourceInfoContent {
264            preceding_lines,
265            package_base,
266            packages,
267        })
268    }
269}
270
271/// The parsed contents of a `pkgbase` section in SRCINFO data.
272#[derive(Debug)]
273pub struct RawPackageBase {
274    /// The name of the `pkgbase` section.
275    pub name: Name,
276    /// The properties of the `pkbase` section.
277    pub properties: Vec<PackageBaseProperty>,
278}
279
280impl RawPackageBase {
281    /// Recognizes the entire `pkgbase` section in SRCINFO data.
282    fn parser(input: &mut &str) -> ModalResult<RawPackageBase> {
283        cut_err("pkgbase")
284            .context(StrContext::Label("pkgbase section header"))
285            .parse_next(input)?;
286
287        cut_err(" = ")
288            .context(StrContext::Label("pkgbase section header delimiter"))
289            .context(StrContext::Expected(StrContextValue::Description("' = '")))
290            .parse_next(input)?;
291
292        // Get the name of the base package.
293        // Don't use `till_line_ending`, as we want the name to have a length of at least one.
294        let name = till_line_end
295            .and_then(Name::parser)
296            .context(StrContext::Label("package base name"))
297            .context(StrContext::Expected(StrContextValue::Description(
298                "the name of the base package",
299            )))
300            .parse_next(input)?;
301
302        // Go through the lines after the initial `pkgbase` statement.
303        //
304        // We explicitly use `repeat` to allow backtracking from the inside.
305        // The reason for this is that SRCINFO is no structured data format per se and we have no
306        // clear indicator that a `pkgbase` section just stopped and a `pkgname` section started.
307        //
308        // The only way to detect this is to look for the `pkgname` keyword while parsing lines in
309        // `package_base_line`. If that keyword is detected, we trigger a backtracking error that
310        // results in this `repeat` call to wrap up and return successfully.
311        let properties: Vec<PackageBaseProperty> =
312            repeat(0.., PackageBaseProperty::parser).parse_next(input)?;
313
314        Ok(RawPackageBase { name, properties })
315    }
316}
317
318/// The parsed contents of a `pkgname` section in SRCINFO data.
319#[derive(Debug)]
320pub struct RawPackage {
321    /// The name of the `pkgname` section.
322    pub name: Name,
323    /// The properties of the `pkgname` section.
324    pub properties: Vec<PackageProperty>,
325}
326
327impl RawPackage {
328    /// Recognizes an entire single `pkgname` section in SRCINFO data.
329    ///
330    /// # Note
331    ///
332    /// This parser expects the cursor to directly start at the `pkgname` keyword.
333    /// This means that the caller must trim any leading newlines or whitespaces.
334    fn parser(input: &mut &str) -> ModalResult<RawPackage> {
335        cut_err("pkgname")
336            .context(StrContext::Label("pkgname section header"))
337            .parse_next(input)?;
338
339        cut_err(" = ")
340            .context(StrContext::Label("pkgname section header delimiter"))
341            .context(StrContext::Expected(StrContextValue::Description("' = '")))
342            .parse_next(input)?;
343
344        // Get the name of the base package.
345        let name = till_line_end
346            .and_then(Name::parser)
347            .context(StrContext::Label("package name"))
348            .context(StrContext::Expected(StrContextValue::Description(
349                "the name of a package",
350            )))
351            .parse_next(input)?;
352
353        // Trim any leading whitespaces before the first pass of the `PackageProperty::parser`.
354        space0.parse_next(input)?;
355
356        // Go through the lines after the initial `pkgname` statement.
357        //
358        // # Usage of Backtracking
359        //
360        // We explicitly use `repeat` to allow backtracking from the inside.
361        // The reason for this is that SRCINFO is no structured data format per se and we have no
362        // clear indicator that the current `pkgname` section just stopped and a new `pkgname`
363        // section started.
364        //
365        // The only way to detect this is to look for the `pkgname` keyword while parsing lines in
366        // `package_line`. If that keyword is detected, we trigger a backtracking error that
367        // results in this `repeat` call to wrap up and return successfully.
368        //
369        // # Whitespace handling
370        //
371        // `PackageProperty::parser` expects leading whitespaces of a line to be trimmed.
372        // This is explicitly done once at the start (see above) and implicitly done via
373        // `terminated` in between the repeats.
374        let properties: Vec<PackageProperty> =
375            repeat(0.., terminated(PackageProperty::parser, space0)).parse_next(input)?;
376
377        Ok(RawPackage { name, properties })
378    }
379}
380
381/// Keywords that are exclusive to the `pkgbase` section in SRCINFO data.
382#[derive(Debug, EnumString, VariantNames)]
383#[strum(serialize_all = "lowercase")]
384pub enum PackageBaseKeyword {
385    /// Test dependencies.
386    CheckDepends,
387    /// Build dependencies.
388    MakeDepends,
389    /// An alpm-pkgver.
390    PkgVer,
391    /// An alpm-pkgrel.
392    PkgRel,
393    /// An alpm-epoch
394    Epoch,
395    /// Valid Openpgp keys.
396    ValidPGPKeys,
397}
398
399impl PackageBaseKeyword {
400    /// Recognizes a [`PackageBaseKeyword`] in an input string slice.
401    pub fn parser(input: &mut &str) -> ModalResult<PackageBaseKeyword> {
402        trace(
403            "package_base_keyword",
404            // Read until we hit something non alphabetical.
405            // This could be either a space or a `_` in case there's an architecture specifier.
406            alpha1.try_map(PackageBaseKeyword::from_str),
407        )
408        .parse_next(input)
409    }
410}
411
412/// All possible properties of a `pkgbase` section in SRCINFO data.
413///
414/// The ordering of the variants represents the order in which keywords would appear in a SRCINFO
415/// file. This is important as the file format represents stateful data which needs normalization.
416///
417/// The SRCINFO format allows comments and empty lines anywhere in the file.
418/// To produce meaningful error messages for the consumer during data normalization, the line number
419/// on which an error occurred is encoded in the parsed data.
420#[derive(Debug)]
421pub enum PackageBaseProperty {
422    /// An empty line.
423    EmptyLine,
424    /// A commented line.
425    Comment(String),
426    /// A [`SharedMetaProperty`].
427    MetaProperty(SharedMetaProperty),
428    /// A [`PackageVersion`].
429    PackageVersion(PackageVersion),
430    /// A [`PackageRelease`].
431    PackageRelease(PackageRelease),
432    /// An [`Epoch`].
433    PackageEpoch(Epoch),
434    /// An [`OpenPGPIdentifier`].
435    ValidPgpKeys(OpenPGPIdentifier),
436    /// A [`RelationProperty`]
437    RelationProperty(RelationProperty),
438    /// Build-time specific check dependencies.
439    CheckDependency(ArchProperty<PackageRelation>),
440    /// Build-time specific make dependencies.
441    MakeDependency(ArchProperty<PackageRelation>),
442    /// Source file properties
443    SourceProperty(SourceProperty),
444}
445
446impl PackageBaseProperty {
447    /// Recognizes any line in the `pkgbase` section of SRCINFO data.
448    ///
449    /// This is a wrapper to separate the logic between comments/empty lines and actual `pkgbase`
450    /// properties.
451    fn parser(input: &mut &str) -> ModalResult<PackageBaseProperty> {
452        // Trim any leading spaces, which are allowed per spec.
453        let _ = multispace0.parse_next(input)?;
454
455        // Look for the `pkgbase` exit condition, which is the start of a `pkgname` section or the
456        // EOL if the pkgname section is missing.
457        // Read the docs above where this function is called for more info.
458        let pkgname = peek(opt(alt(("pkgname", eof)))).parse_next(input)?;
459        if pkgname.is_some() {
460            // If we find a `pkgname` keyword, we know that the current `pkgbase` section finished.
461            // Return a backtrack so the calling parser may wrap up and we can continue with
462            // `pkgname` parsing.
463            return Err(ErrMode::Backtrack(ParserError::from_input(input)));
464        }
465
466        trace(
467            "package_base_line",
468            alt((
469                // First of handle any empty lines or comments.
470                preceded(("#", take_until(0.., "\n")), line_ending)
471                    .map(|s: &str| PackageBaseProperty::Comment(s.to_string())),
472                preceded(space0, line_ending).map(|_| PackageBaseProperty::EmptyLine),
473                // In case we got text, start parsing properties
474                Self::property_parser,
475            )),
476        )
477        .parse_next(input)
478    }
479
480    /// Recognizes keyword assignments in the `pkgbase` section in SRCINFO data.
481    ///
482    /// Since there're a lot of keywords and many of them are shared between the `pkgbase` and
483    /// `pkgname` section, the keywords are bundled into somewhat logical groups.
484    ///
485    /// - [`SourceProperty`] are keywords that are related to the `source` keyword, such as
486    ///   checksums.
487    /// - [`SharedMetaProperty`] are keywords that are related to general meta properties of the
488    ///   package.
489    /// - [`RelationProperty`] are keywords that describe the relation of the package to other
490    ///   packages. [`RawPackageBase`] has two special relations that are explicitly handled in
491    ///   [`Self::exclusive_property_parser`].
492    /// - Other fields that're unique to the [`RawPackageBase`] are handled in
493    ///   [`Self::exclusive_property_parser`].
494    fn property_parser(input: &mut &str) -> ModalResult<PackageBaseProperty> {
495        // First off, get the type of the property.
496        trace(
497            "pkgbase_property",
498            alt((
499                SourceProperty::parser.map(PackageBaseProperty::SourceProperty),
500                SharedMetaProperty::parser.map(PackageBaseProperty::MetaProperty),
501                RelationProperty::parser.map(PackageBaseProperty::RelationProperty),
502                PackageBaseProperty::exclusive_property_parser,
503                cut_err(fail)
504                    .context(StrContext::Label("package base property type"))
505                    .context(StrContext::Expected(StrContextValue::Description(
506                        "one of the allowed pkgbase section properties:",
507                    )))
508                    .context_with(iter_str_context!([
509                        PackageBaseKeyword::VARIANTS,
510                        RelationKeyword::VARIANTS,
511                        SharedMetaKeyword::VARIANTS,
512                        SourceKeyword::VARIANTS,
513                    ])),
514            )),
515        )
516        .parse_next(input)
517    }
518
519    /// Recognizes keyword assignments exclusive to the `pkgbase` section in SRCINFO data.
520    ///
521    /// This function backtracks in case no keyword in this group matches.
522    fn exclusive_property_parser(input: &mut &str) -> ModalResult<PackageBaseProperty> {
523        // First off, get the type of the property.
524        let keyword =
525            trace("exclusive_pkgbase_property", PackageBaseKeyword::parser).parse_next(input)?;
526
527        // Parse a possible architecture suffix for architecture specific fields.
528        let architecture = match keyword {
529            PackageBaseKeyword::MakeDepends | PackageBaseKeyword::CheckDepends => {
530                architecture_suffix.parse_next(input)?
531            }
532            _ => None,
533        };
534
535        // Expect the ` = ` separator between the key-value pair
536        let _ = delimiter.parse_next(input)?;
537
538        let property = match keyword {
539            PackageBaseKeyword::PkgVer => cut_err(
540                till_line_end
541                    .and_then(PackageVersion::parser)
542                    .map(PackageBaseProperty::PackageVersion),
543            )
544            .parse_next(input)?,
545            PackageBaseKeyword::PkgRel => cut_err(
546                till_line_end
547                    .and_then(PackageRelease::parser)
548                    .map(PackageBaseProperty::PackageRelease),
549            )
550            .parse_next(input)?,
551
552            PackageBaseKeyword::Epoch => cut_err(
553                till_line_end
554                    .and_then(Epoch::parser)
555                    .map(PackageBaseProperty::PackageEpoch),
556            )
557            .parse_next(input)?,
558            PackageBaseKeyword::ValidPGPKeys => cut_err(
559                till_line_end
560                    .try_map(OpenPGPIdentifier::from_str)
561                    .map(PackageBaseProperty::ValidPgpKeys),
562            )
563            .parse_next(input)?,
564
565            // Handle `pkgbase` specific package relations.
566            PackageBaseKeyword::MakeDepends | PackageBaseKeyword::CheckDepends => {
567                // Read and parse the generic architecture specific PackageRelation.
568                let value =
569                    cut_err(till_line_end.and_then(PackageRelation::parser)).parse_next(input)?;
570                let arch_property = ArchProperty {
571                    architecture,
572                    value,
573                };
574
575                // Now map the generic relation to the specific relation type.
576                match keyword {
577                    PackageBaseKeyword::CheckDepends => {
578                        PackageBaseProperty::CheckDependency(arch_property)
579                    }
580                    PackageBaseKeyword::MakeDepends => {
581                        PackageBaseProperty::MakeDependency(arch_property)
582                    }
583                    _ => unreachable!(),
584                }
585            }
586        };
587
588        Ok(property)
589    }
590}
591
592/// All possible properties of a `pkgname` section in SRCINFO data.
593///
594/// It's very similar to [`RawPackageBase`], but with less fields and the possibility to explicitly
595/// set some fields to "empty".
596#[derive(Debug)]
597pub enum PackageProperty {
598    /// An empty line.
599    EmptyLine,
600    /// A commented line.
601    Comment(String),
602    /// A [`SharedMetaProperty`].
603    MetaProperty(SharedMetaProperty),
604    /// A [`RelationProperty`].
605    RelationProperty(RelationProperty),
606    /// A [`ClearableProperty`].
607    Clear(ClearableProperty),
608}
609
610impl PackageProperty {
611    /// Handles any line in a `pkgname` package section.
612    ///
613    /// This is a wrapper to separate the logic between comments/empty lines and actual package
614    /// properties.
615    fn parser(input: &mut &str) -> ModalResult<PackageProperty> {
616        // Look for one of the `pkgname` exit conditions, which is the start of a new `pkgname`
617        // section. Read the docs above where this function is called for more info.
618        let pkgname = peek(opt("pkgname")).parse_next(input)?;
619        if pkgname.is_some() {
620            // If we find a `pkgname` keyword, we know that the current `pkgname` section finished.
621            // Return a backtrack so the calling parser may wrap up.
622            return Err(ErrMode::Backtrack(ParserError::from_input(input)));
623        }
624
625        // Check if we're at the end of the file.
626        // If so, throw a backtrack error.
627        let eof_found = opt(eof).parse_next(input)?;
628        if eof_found.is_some() {
629            return Err(ErrMode::Backtrack(ParserError::from_input(input)));
630        }
631
632        trace(
633            "package_line",
634            alt((
635                // First of handle any empty lines or comments, which might also occur at the
636                // end of the file.
637                preceded("#", till_line_end).map(|s: &str| PackageProperty::Comment(s.to_string())),
638                line_ending.map(|_| PackageProperty::EmptyLine),
639                // In case we got text, start parsing properties
640                Self::property_parser,
641            )),
642        )
643        .parse_next(input)
644    }
645
646    /// Recognizes keyword assignments in a `pkgname` section in SRCINFO data.
647    ///
648    /// Since there're a lot of keywords and many of them are shared between the `pkgbase` and
649    /// `pkgname` section, the keywords are bundled into somewhat logical groups.
650    ///
651    /// - [`SourceProperty`] are keywords that are related to the `source` keyword, such as
652    ///   checksums.
653    /// - [`SharedMetaProperty`] are keywords that are related to general meta properties of the
654    ///   package.
655    /// - [`RelationProperty`] are keywords that describe the relation of the package to other
656    ///   packages. [`RawPackageBase`] has two special relations that are explicitly handled in that
657    ///   enum.
658    fn property_parser(input: &mut &str) -> ModalResult<PackageProperty> {
659        // The way we handle `ClearableProperty` is a bit imperformant.
660        // Since clearable properties are only allowed to occur in `pkgname` sections, I decided to
661        // not handle clearable properties in the respective property parsers to keep the
662        // code as reusable between `pkgbase` and `pkgname` as possible.
663        //
664        // Hence, we do a check for any clearable properties at the very start. If none is detected,
665        // the actual property setters will be checked afterwards.
666        // This means that every property is preceded by `clearable_property` pass.
667        //
668        // I don't expect that this will result in any significant performance issues, but **if**
669        // this were to ever become an issue, it would be a good start to duplicate all
670        // `*_property` parser functions, where one of them explicitly handles clearable properties.
671        trace(
672            "pkgname_property",
673            alt((
674                ClearableProperty::relation_parser.map(PackageProperty::Clear),
675                ClearableProperty::shared_meta_parser.map(PackageProperty::Clear),
676                SharedMetaProperty::parser.map(PackageProperty::MetaProperty),
677                RelationProperty::parser.map(PackageProperty::RelationProperty),
678                cut_err(fail)
679                    .context(StrContext::Label("package property type"))
680                    .context(StrContext::Expected(StrContextValue::Description(
681                        "one of the allowed package section properties:",
682                    )))
683                    .context_with(iter_str_context!([
684                        RelationKeyword::VARIANTS,
685                        SharedMetaKeyword::VARIANTS
686                    ])),
687            )),
688        )
689        .parse_next(input)
690    }
691}
692
693/// Keywords that may exist both in `pkgbase` and `pkgname` sections in SRCINFO data.
694#[derive(Debug, EnumString, VariantNames)]
695#[strum(serialize_all = "lowercase")]
696pub enum SharedMetaKeyword {
697    /// The description of a package.
698    PkgDesc,
699    /// The upstream URL of a package.
700    Url,
701    /// The license of a package.
702    License,
703    /// The alpm-architecture of a package.
704    Arch,
705    /// The path to a changelog file of a package.
706    Changelog,
707    /// The path to an alpm-install-scriptlet of a package.
708    Install,
709    /// The alpm-package-groups a package is part of.
710    Groups,
711    /// The build tool options used when building a package.
712    Options,
713    /// The path of a file in a package that should be backed up.
714    Backup,
715}
716
717impl SharedMetaKeyword {
718    /// Recognizes a [`SharedMetaKeyword`] in a string slice.
719    pub fn parser(input: &mut &str) -> ModalResult<SharedMetaKeyword> {
720        // Read until we hit something non alphabetical.
721        // This could be either a space or a `_` in case there's an architecture specifier.
722        trace(
723            "shared_meta_keyword",
724            alpha1.try_map(SharedMetaKeyword::from_str),
725        )
726        .parse_next(input)
727    }
728}
729
730/// Metadata properties that may be shared between `pkgbase` and `pkgname` sections in SRCINFO data.
731#[derive(Debug)]
732pub enum SharedMetaProperty {
733    /// A [`PackageDescription`].
734    Description(PackageDescription),
735    /// A [`Url`].
736    Url(Url),
737    /// A [`License`].
738    License(License),
739    /// An [`Architecture`].
740    Architecture(Architecture),
741    /// A [`RelativePath`] for a changelog file of a package.
742    Changelog(RelativePath),
743    /// A [`RelativePath`] for an alpm-install-scriptlet file of a package.
744    Install(RelativePath),
745    /// An alpm-package-group of a package.
746    Group(String),
747    /// A [`MakepkgOption`] used for building a package.
748    Option(MakepkgOption),
749    /// A [`RelativePath`] for file in a package that should be backed up.
750    Backup(RelativePath),
751}
752
753impl SharedMetaProperty {
754    /// Recognizes keyword assignments that may be present in both `pkgbase` and `pkgname` sections
755    /// of SRCINFO data.
756    ///
757    /// This function relies on [`SharedMetaKeyword::parser`] to recognize the relevant keywords.
758    ///
759    /// This function backtracks in case no keyword in this group matches.
760    fn parser(input: &mut &str) -> ModalResult<SharedMetaProperty> {
761        // Now get the type of the property.
762        let keyword = SharedMetaKeyword::parser.parse_next(input)?;
763
764        // Expect the ` = ` separator between the key-value pair
765        let _ = delimiter.parse_next(input)?;
766
767        let property = match keyword {
768            SharedMetaKeyword::PkgDesc => cut_err(
769                till_line_end.map(|s| SharedMetaProperty::Description(PackageDescription::from(s))),
770            )
771            .parse_next(input)?,
772            SharedMetaKeyword::Url => cut_err(
773                till_line_end
774                    .try_map(Url::from_str)
775                    .map(SharedMetaProperty::Url),
776            )
777            .parse_next(input)?,
778            SharedMetaKeyword::License => cut_err(
779                till_line_end
780                    .try_map(License::from_str)
781                    .map(SharedMetaProperty::License),
782            )
783            .parse_next(input)?,
784            SharedMetaKeyword::Arch => cut_err(
785                till_line_end
786                    .and_then(Architecture::parser)
787                    .map(SharedMetaProperty::Architecture),
788            )
789            .parse_next(input)?,
790            SharedMetaKeyword::Changelog => cut_err(
791                till_line_end
792                    .try_map(Changelog::from_str)
793                    .map(SharedMetaProperty::Changelog),
794            )
795            .parse_next(input)?,
796            SharedMetaKeyword::Install => cut_err(
797                till_line_end
798                    .try_map(Install::from_str)
799                    .map(SharedMetaProperty::Install),
800            )
801            .parse_next(input)?,
802            SharedMetaKeyword::Groups => {
803                cut_err(till_line_end.map(|s| SharedMetaProperty::Group(Group::from(s))))
804                    .parse_next(input)?
805            }
806            SharedMetaKeyword::Options => cut_err(
807                till_line_end
808                    .try_map(MakepkgOption::from_str)
809                    .map(SharedMetaProperty::Option),
810            )
811            .parse_next(input)?,
812            SharedMetaKeyword::Backup => cut_err(
813                till_line_end
814                    .try_map(Backup::from_str)
815                    .map(SharedMetaProperty::Backup),
816            )
817            .parse_next(input)?,
818        };
819
820        Ok(property)
821    }
822}
823
824/// Keywords that describe [alpm-package-relations].
825///
826/// [alpm-package-relations]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
827#[derive(Debug, EnumString, VariantNames)]
828#[strum(serialize_all = "lowercase")]
829pub enum RelationKeyword {
830    /// A run-time dependency.
831    Depends,
832    /// An optional dependency.
833    OptDepends,
834    /// A provision.
835    Provides,
836    /// A conflict.
837    Conflicts,
838    /// A replacement.
839    Replaces,
840}
841
842impl RelationKeyword {
843    /// Recognizes a [`RelationKeyword`] in a string slice.
844    pub fn parser(input: &mut &str) -> ModalResult<RelationKeyword> {
845        // Read until we hit something non alphabetical.
846        // This could be either a space or a `_` in case there's an architecture specifier.
847        trace(
848            "relation_keyword",
849            alpha1.try_map(RelationKeyword::from_str),
850        )
851        .parse_next(input)
852    }
853}
854
855/// Properties related to package relations.
856///
857/// This only handles the shared package relations that can be used in both `pkgbase` and `pkgname`
858/// sections.
859/// `pkgbase` specific relations are explicitly handled in the [`RawPackageBase`] enum.
860/// See [alpm-package-relation] for further details on package relations and [alpm-sonamev1] for
861/// information on _soname_ handling.
862/// [alpm-package-relation]: <https://alpm.archlinux.page/specifications/alpm-package-relation.7.html>
863/// [alpm-sonamev1]: <https://alpm.archlinux.page/specifications/alpm-sonamev1.7.html>
864#[derive(Debug)]
865pub enum RelationProperty {
866    /// An [`ArchProperty<RelationOrSoname>`] for a run-time dependency.
867    Dependency(ArchProperty<RelationOrSoname>),
868    /// An [`ArchProperty<OptionalDependency>`] for an optional dependency.
869    OptionalDependency(ArchProperty<OptionalDependency>),
870    /// An [`ArchProperty<RelationOrSoname>`] for a provision.
871    Provides(ArchProperty<RelationOrSoname>),
872    /// An [`ArchProperty<PackageRelation>`] for a conflict.
873    Conflicts(ArchProperty<PackageRelation>),
874    /// An [`ArchProperty<PackageRelation>`] for a replacement.
875    Replaces(ArchProperty<PackageRelation>),
876}
877
878impl RelationProperty {
879    /// Recognizes package relation keyword assignments that may be present in both `pkgbase` and
880    /// `pkgname` sections in SRCINFO data.
881    ///
882    /// This function relies on [`RelationKeyword::parser`] to recognize the relevant keywords.
883    /// This function backtracks in case no keyword in this group matches.
884    fn parser(input: &mut &str) -> ModalResult<RelationProperty> {
885        // First off, get the type of the property.
886        let keyword = RelationKeyword::parser.parse_next(input)?;
887
888        // All of these properties can be architecture specific and may have an architecture suffix.
889        // Get it if there's one.
890        let architecture = architecture_suffix.parse_next(input)?;
891
892        // Expect the ` = ` separator between the key-value pair
893        let _ = delimiter.parse_next(input)?;
894
895        let property = match keyword {
896            // Handle these together in a single blob as they all deserialize to the same base type.
897            RelationKeyword::Conflicts | RelationKeyword::Replaces => {
898                // Read and parse the generic architecture specific PackageRelation.
899                let value =
900                    cut_err(till_line_end.and_then(PackageRelation::parser)).parse_next(input)?;
901                let arch_property = ArchProperty {
902                    architecture,
903                    value,
904                };
905
906                // Now map the generic relation to the specific relation type.
907                match keyword {
908                    RelationKeyword::Replaces => RelationProperty::Replaces(arch_property),
909                    RelationKeyword::Conflicts => RelationProperty::Conflicts(arch_property),
910                    _ => unreachable!(),
911                }
912            }
913            RelationKeyword::Depends | RelationKeyword::Provides => {
914                // Read and parse the generic architecture specific RelationOrSoname.
915                let value =
916                    cut_err(till_line_end.try_map(RelationOrSoname::from_str)).parse_next(input)?;
917                let arch_property = ArchProperty {
918                    architecture,
919                    value,
920                };
921
922                // Now map the generic relation to the specific relation type.
923                match keyword {
924                    RelationKeyword::Depends => RelationProperty::Dependency(arch_property),
925                    RelationKeyword::Provides => RelationProperty::Provides(arch_property),
926                    _ => unreachable!(),
927                }
928            }
929            RelationKeyword::OptDepends => cut_err(
930                till_line_end
931                    .and_then(OptionalDependency::parser)
932                    .map(|value| {
933                        RelationProperty::OptionalDependency(ArchProperty {
934                            architecture,
935                            value,
936                        })
937                    }),
938            )
939            .parse_next(input)?,
940        };
941
942        Ok(property)
943    }
944
945    /// Returns the [`Architecture`] of the current variant.
946    ///
947    /// Can be used to extract the architecture without knowing which variant this is.
948    pub fn architecture(&self) -> Option<Architecture> {
949        match self {
950            RelationProperty::Dependency(arch_property) => arch_property.architecture,
951            RelationProperty::OptionalDependency(arch_property) => arch_property.architecture,
952            RelationProperty::Provides(arch_property) => arch_property.architecture,
953            RelationProperty::Conflicts(arch_property) => arch_property.architecture,
954            RelationProperty::Replaces(arch_property) => arch_property.architecture,
955        }
956    }
957}
958
959/// Package source keywords that are exclusive to the `pkgbase` section in SRCINFO data.
960#[derive(Debug, EnumString, VariantNames)]
961#[strum(serialize_all = "lowercase")]
962pub enum SourceKeyword {
963    /// A source entry.
964    Source,
965    /// A noextract entry.
966    NoExtract,
967    /// A blake2 hash digest.
968    B2sums,
969    /// An MD-5 hash digest.
970    Md5sums,
971    /// An SHA-1 hash digest.
972    Sha1sums,
973    /// An SHA-224 hash digest.
974    Sha224sums,
975    /// An SHA-256 hash digest.
976    Sha256sums,
977    /// An SHA-384 hash digest.
978    Sha384sums,
979    /// An SHA-512 hash digest.
980    Sha512sums,
981}
982
983impl SourceKeyword {
984    /// Parse a [`SourceKeyword`].
985    pub fn parser(input: &mut &str) -> ModalResult<SourceKeyword> {
986        // Read until we hit something non alphabetical.
987        // This could be either a space or a `_` in case there's an architecture specifier.
988        trace(
989            "source_keyword",
990            alphanumeric1.try_map(SourceKeyword::from_str),
991        )
992        .parse_next(input)
993    }
994}
995
996/// Properties related to package sources.
997///
998/// Sources and related properties can be architecture specific.
999///
1000/// The `source`, `noextract` and checksum related keywords in SRCINFO data correlate in ordering:
1001/// `noextract` and any checksum entries are ordered in the same way as the respective `source`
1002/// entry they relate to. The representation of this correlation is normalized after initial
1003/// parsing.
1004#[derive(Debug)]
1005pub enum SourceProperty {
1006    /// An [`ArchProperty<Source>`] for a source entry.
1007    Source(ArchProperty<Source>),
1008    /// An [`ArchProperty<String>`] for a noextract entry.
1009    NoExtract(String),
1010    /// An [`ArchProperty<SkippableChecksum<Blake2b512>>`] for a blake2 hash digest.
1011    B2Checksum(ArchProperty<SkippableChecksum<Blake2b512>>),
1012    /// An [`ArchProperty<SkippableChecksum<Md5>>`] for an MD-5 hash digest.
1013    Md5Checksum(ArchProperty<SkippableChecksum<Md5>>),
1014    /// An [`ArchProperty<SkippableChecksum<Sha1>>`] for a SHA-1 hash digest.
1015    Sha1Checksum(ArchProperty<SkippableChecksum<Sha1>>),
1016    /// An [`ArchProperty<SkippableChecksum<Sha256>>`] for a SHA-256 hash digest.
1017    Sha256Checksum(ArchProperty<SkippableChecksum<Sha256>>),
1018    /// An [`ArchProperty<SkippableChecksum<Sha224>>`] for a SHA-224 hash digest.
1019    Sha224Checksum(ArchProperty<SkippableChecksum<Sha224>>),
1020    /// An [`ArchProperty<SkippableChecksum<Sha384>>`] for a SHA-384 hash digest.
1021    Sha384Checksum(ArchProperty<SkippableChecksum<Sha384>>),
1022    /// An [`ArchProperty<SkippableChecksum<Sha512>>`] for a SHA-512 hash digest.
1023    Sha512Checksum(ArchProperty<SkippableChecksum<Sha512>>),
1024}
1025
1026impl SourceProperty {
1027    /// Recognizes package source related keyword assignments in SRCINFO data.
1028    ///
1029    /// This function relies on [`SourceKeyword::parser`] to recognize the relevant keywords.
1030    ///
1031    /// This function backtracks in case no keyword in this group matches.
1032    fn parser(input: &mut &str) -> ModalResult<SourceProperty> {
1033        // First off, get the type of the property.
1034        let keyword = SourceKeyword::parser.parse_next(input)?;
1035
1036        let property = match keyword {
1037            SourceKeyword::NoExtract => {
1038                // Expect the ` = ` separator between the key-value pair
1039                let _ = delimiter.parse_next(input)?;
1040
1041                cut_err(till_line_end.map(|s| SourceProperty::NoExtract(s.to_string())))
1042                    .parse_next(input)?
1043            }
1044            SourceKeyword::Source
1045            | SourceKeyword::B2sums
1046            | SourceKeyword::Md5sums
1047            | SourceKeyword::Sha1sums
1048            | SourceKeyword::Sha224sums
1049            | SourceKeyword::Sha256sums
1050            | SourceKeyword::Sha384sums
1051            | SourceKeyword::Sha512sums => {
1052                // All other properties may be architecture specific and thereby have an
1053                // architecture suffix.
1054                let architecture = architecture_suffix.parse_next(input)?;
1055
1056                // Expect the ` = ` separator between the key-value pair
1057                let _ = delimiter.parse_next(input)?;
1058
1059                match keyword {
1060                    SourceKeyword::Source => {
1061                        cut_err(till_line_end.try_map(Source::from_str).map(|value| {
1062                            SourceProperty::Source(ArchProperty {
1063                                architecture,
1064                                value,
1065                            })
1066                        }))
1067                        .parse_next(input)?
1068                    }
1069                    // all checksum properties are parsed the same way.
1070                    SourceKeyword::B2sums => SourceProperty::B2Checksum(ArchProperty {
1071                        architecture,
1072                        value: till_line_end
1073                            .and_then(SkippableChecksum::parser)
1074                            .parse_next(input)?,
1075                    }),
1076                    SourceKeyword::Md5sums => SourceProperty::Md5Checksum(ArchProperty {
1077                        architecture,
1078                        value: till_line_end
1079                            .and_then(SkippableChecksum::parser)
1080                            .parse_next(input)?,
1081                    }),
1082                    SourceKeyword::Sha1sums => SourceProperty::Sha1Checksum(ArchProperty {
1083                        architecture,
1084                        value: till_line_end
1085                            .and_then(SkippableChecksum::parser)
1086                            .parse_next(input)?,
1087                    }),
1088                    SourceKeyword::Sha224sums => SourceProperty::Sha224Checksum(ArchProperty {
1089                        architecture,
1090                        value: till_line_end
1091                            .and_then(SkippableChecksum::parser)
1092                            .parse_next(input)?,
1093                    }),
1094                    SourceKeyword::Sha256sums => SourceProperty::Sha256Checksum(ArchProperty {
1095                        architecture,
1096                        value: till_line_end
1097                            .and_then(SkippableChecksum::parser)
1098                            .parse_next(input)?,
1099                    }),
1100                    SourceKeyword::Sha384sums => SourceProperty::Sha384Checksum(ArchProperty {
1101                        architecture,
1102                        value: till_line_end
1103                            .and_then(SkippableChecksum::parser)
1104                            .parse_next(input)?,
1105                    }),
1106                    SourceKeyword::Sha512sums => SourceProperty::Sha512Checksum(ArchProperty {
1107                        architecture,
1108                        value: till_line_end
1109                            .and_then(SkippableChecksum::parser)
1110                            .parse_next(input)?,
1111                    }),
1112                    SourceKeyword::NoExtract => unreachable!(),
1113                }
1114            }
1115        };
1116
1117        Ok(property)
1118    }
1119}
1120
1121/// Properties used in `pkgname` sections that can be cleared.
1122///
1123/// Some variants of this enum are architecture-specific, as they might only be cleared for a
1124/// specific architecture, but not for another.
1125///
1126/// Clearing a keyword in SRCINFO data is achieved by an empty keyword assignment, e.g.:
1127///
1128/// ```txt
1129/// depends =
1130/// ```
1131#[derive(Clone, Debug)]
1132pub enum ClearableProperty {
1133    /// The description for a package.
1134    Description,
1135    /// The upstream URL for a package.
1136    Url,
1137    /// The licenses that apply to a package.
1138    Licenses,
1139    /// The changelog for a package.
1140    Changelog,
1141    /// The alpm-install-scriptlet for a package.
1142    Install,
1143    /// The alpm-package-groups a package is part of.
1144    Groups,
1145    /// The build tool options used for building a package.
1146    Options,
1147    /// The path to a file in a package that should be backed up.
1148    Backups,
1149    /// The alpm-architecture of run-time dependencies.
1150    Dependencies(Option<Architecture>),
1151    /// The alpm-architecture of optional dependencies.
1152    OptionalDependencies(Option<Architecture>),
1153    /// The alpm-architecture of provisions.
1154    Provides(Option<Architecture>),
1155    /// The alpm-architecture of conflicts.
1156    Conflicts(Option<Architecture>),
1157    /// The alpm-architecture of replacements.
1158    Replaces(Option<Architecture>),
1159}
1160
1161impl ClearableProperty {
1162    /// Recognizes all keyword assignments in SRCINFO data that represent a cleared
1163    /// [`SharedMetaProperty`].
1164    ///
1165    /// A cleared property is represented by a keyword that is assigned an empty value.
1166    /// It indicates that the keyword assignment should remain empty for a given package.
1167    ///
1168    /// Example:
1169    /// ```txt
1170    /// pkgdesc =
1171    /// depends =
1172    /// ```
1173    ///
1174    /// The above properties would indicate that both `pkgdesc` and the `depends` array are to be
1175    /// cleared and left empty for a given package.
1176    ///
1177    /// This function backtracks in case no keyword in this group matches or in case the property is
1178    /// not cleared.
1179    fn shared_meta_parser(input: &mut &str) -> ModalResult<ClearableProperty> {
1180        // First off, check if this is any of the clearable properties.
1181        let keyword =
1182            trace("clearable_shared_meta_property", SharedMetaKeyword::parser).parse_next(input)?;
1183
1184        // Now check if it's actually a clear.
1185        // This parser fails and backtracks in case there's anything but spaces and a newline after
1186        // the delimiter, which indicates that there's an actual value that is set for this
1187        // property.
1188        let _ = (" =", space0, newline).parse_next(input)?;
1189
1190        let property = match keyword {
1191            // The `Arch` property matches the keyword, but isn't clearable.
1192            SharedMetaKeyword::Arch => {
1193                return Err(ErrMode::Backtrack(ParserError::from_input(input)));
1194            }
1195            SharedMetaKeyword::PkgDesc => ClearableProperty::Description,
1196            SharedMetaKeyword::Url => ClearableProperty::Url,
1197            SharedMetaKeyword::License => ClearableProperty::Licenses,
1198            SharedMetaKeyword::Changelog => ClearableProperty::Changelog,
1199            SharedMetaKeyword::Install => ClearableProperty::Install,
1200            SharedMetaKeyword::Groups => ClearableProperty::Groups,
1201            SharedMetaKeyword::Options => ClearableProperty::Options,
1202            SharedMetaKeyword::Backup => ClearableProperty::Backups,
1203        };
1204
1205        Ok(property)
1206    }
1207
1208    /// Same as [`Self::shared_meta_parser`], but for clearable [RelationProperty].
1209    fn relation_parser(input: &mut &str) -> ModalResult<ClearableProperty> {
1210        // First off, check if this is any of the clearable properties.
1211        let keyword = trace("clearable_property", RelationKeyword::parser).parse_next(input)?;
1212
1213        // All relations may be architecture specific.
1214        let architecture = architecture_suffix.parse_next(input)?;
1215
1216        // Now check if it's actually a clear.
1217        // This parser fails and backtracks in case there's anything but spaces and a newline after
1218        // the delimiter, which indicates that there's an actual value that is set for this
1219        // property.
1220        let _ = (" =", space0, newline).parse_next(input)?;
1221
1222        let property = match keyword {
1223            RelationKeyword::Depends => ClearableProperty::Dependencies(architecture),
1224            RelationKeyword::OptDepends => ClearableProperty::OptionalDependencies(architecture),
1225            RelationKeyword::Provides => ClearableProperty::Provides(architecture),
1226            RelationKeyword::Conflicts => ClearableProperty::Conflicts(architecture),
1227            RelationKeyword::Replaces => ClearableProperty::Replaces(architecture),
1228        };
1229
1230        Ok(property)
1231    }
1232}