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