Skip to main content

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