alpm_srcinfo/source_info/
parser.rs

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