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    pub value: T,
130}
131
132/// Recognizes and returns the architecture suffix of a keyword, if it exists.
133///
134/// Returns [`None`] if no architecture suffix is found.
135///
136/// ## Examples
137/// ```txt
138/// sha256sums_i386 = 0db1b39fd70097c6733cdcce56b1559ece5521ec1aad9ee1d520dda73eff03d0
139///           ^^^^^
140///         This is the suffix with `i386` being the architecture.
141/// ```
142fn architecture_suffix(input: &mut &str) -> ModalResult<Option<Architecture>> {
143    // First up, check if there's an underscore.
144    // If there's none, there's no suffix and we can return early.
145    let underscore = opt('_').parse_next(input)?;
146    if underscore.is_none() {
147        return Ok(None);
148    }
149
150    // There has been an underscore, so now we **expect** an architecture to be there and we have
151    // to fail hard if that doesn't work.
152    // We now grab all content until the expected space of the delimiter and map it to an
153    // alpm_types::Architecture.
154    let architecture =
155        cut_err(take_till(0.., |c| c == ' ' || c == '=').try_map(Architecture::from_str))
156            .context(StrContext::Label("architecture"))
157            .context(StrContext::Expected(StrContextValue::Description(
158                "an alpm-architecture:",
159            )))
160            .context_with(iter_str_context!([Architecture::VARIANTS]))
161            .parse_next(input)?;
162
163    Ok(Some(architecture))
164}
165
166/// Track empty/comment lines
167#[derive(Debug)]
168pub enum Ignored {
169    EmptyLine,
170    Comment(String),
171}
172
173/// A representation of all high-level components of parsed SRCINFO data.
174#[derive(Debug)]
175pub struct SourceInfoContent {
176    /// Empty or comment lines that occur outside of `pkgbase` or `pkgname` sections.
177    pub preceding_lines: Vec<Ignored>,
178
179    pub package_base: RawPackageBase,
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    pub name: Name,
258    pub properties: Vec<PackageBaseProperty>,
259}
260
261impl RawPackageBase {
262    /// Recognizes the entire `pkgbase` section in SRCINFO data.
263    fn parser(input: &mut &str) -> ModalResult<RawPackageBase> {
264        cut_err("pkgbase")
265            .context(StrContext::Label("pkgbase section header"))
266            .parse_next(input)?;
267
268        cut_err(" = ")
269            .context(StrContext::Label("pkgbase section header delimiter"))
270            .context(StrContext::Expected(StrContextValue::Description("' = '")))
271            .parse_next(input)?;
272
273        // Get the name of the base package.
274        // Don't use `till_line_ending`, as we want the name to have a length of at least one.
275        let name = till_line_end
276            .and_then(Name::parser)
277            .context(StrContext::Label("package base name"))
278            .context(StrContext::Expected(StrContextValue::Description(
279                "the name of the base package",
280            )))
281            .parse_next(input)?;
282
283        // Go through the lines after the initial `pkgbase` statement.
284        //
285        // We explicitly use `repeat` to allow backtracking from the inside.
286        // The reason for this is that SRCINFO is no structured data format per se and we have no
287        // clear indicator that a `pkgbase` section just stopped and a `pkgname` section started.
288        //
289        // The only way to detect this is to look for the `pkgname` keyword while parsing lines in
290        // `package_base_line`. If that keyword is detected, we trigger a backtracking error that
291        // results in this `repeat` call to wrap up and return successfully.
292        let properties: Vec<PackageBaseProperty> =
293            repeat(0.., PackageBaseProperty::parser).parse_next(input)?;
294
295        Ok(RawPackageBase { name, properties })
296    }
297}
298
299/// The parsed contents of a `pkgname` section in SRCINFO data.
300#[derive(Debug)]
301pub struct RawPackage {
302    pub name: Name,
303    pub properties: Vec<PackageProperty>,
304}
305
306impl RawPackage {
307    /// Recognizes an entire single `pkgname` section in SRCINFO data.
308    fn parser(input: &mut &str) -> ModalResult<RawPackage> {
309        cut_err("pkgname")
310            .context(StrContext::Label("pkgname section header"))
311            .parse_next(input)?;
312
313        cut_err(" = ")
314            .context(StrContext::Label("pkgname section header delimiter"))
315            .context(StrContext::Expected(StrContextValue::Description("' = '")))
316            .parse_next(input)?;
317
318        // Get the name of the base package.
319        let name = till_line_end
320            .and_then(Name::parser)
321            .context(StrContext::Label("package name"))
322            .context(StrContext::Expected(StrContextValue::Description(
323                "the name of a package",
324            )))
325            .parse_next(input)?;
326
327        // Go through the lines after the initial `pkgname` statement.
328        //
329        // We explicitly use `repeat` to allow backtracking from the inside.
330        // The reason for this is that SRCINFO is no structured data format per se and we have no
331        // clear indicator that the current `pkgname` section just stopped and a new `pkgname`
332        // section started.
333        //
334        // The only way to detect this is to look for the `pkgname` keyword while parsing lines in
335        // `package_line`. If that keyword is detected, we trigger a backtracking error that
336        // results in this `repeat` call to wrap up and return successfully.
337        let properties: Vec<PackageProperty> =
338            repeat(0.., PackageProperty::parser).parse_next(input)?;
339
340        Ok(RawPackage { name, properties })
341    }
342}
343
344/// Keywords that are exclusive to the `pkgbase` section in SRCINFO data.
345#[derive(Debug, EnumString, VariantNames)]
346#[strum(serialize_all = "lowercase")]
347pub enum PackageBaseKeyword {
348    CheckDepends,
349    MakeDepends,
350    PkgVer,
351    PkgRel,
352    Epoch,
353    ValidPGPKeys,
354}
355
356impl PackageBaseKeyword {
357    /// Recognizes a [`PackageBaseKeyword`] in an input string slice.
358    pub fn parser(input: &mut &str) -> ModalResult<PackageBaseKeyword> {
359        trace(
360            "package_base_keyword",
361            // Read until we hit something non alphabetical.
362            // This could be either a space or a `_` in case there's an architecture specifier.
363            alpha1.try_map(PackageBaseKeyword::from_str),
364        )
365        .parse_next(input)
366    }
367}
368
369/// All possible properties of a `pkgbase` section in SRCINFO data.
370///
371/// The ordering of the variants represents the order in which keywords would appear in a SRCINFO
372/// file. This is important as the file format represents stateful data which needs normalization.
373///
374/// The SRCINFO format allows comments and empty lines anywhere in the file.
375/// To produce meaningful error messages for the consumer during data normalization, the line number
376/// on which an error occurred is encoded in the parsed data.
377#[derive(Debug)]
378pub enum PackageBaseProperty {
379    EmptyLine,
380    Comment(String),
381    MetaProperty(SharedMetaProperty),
382    PackageVersion(PackageVersion),
383    PackageRelease(PackageRelease),
384    PackageEpoch(Epoch),
385    ValidPgpKeys(OpenPGPIdentifier),
386    RelationProperty(RelationProperty),
387    /// Build-time specific check dependencies.
388    CheckDependency(ArchProperty<PackageRelation>),
389    /// Build-time specific make dependencies.
390    MakeDependency(ArchProperty<PackageRelation>),
391    /// Source file properties
392    SourceProperty(SourceProperty),
393}
394
395impl PackageBaseProperty {
396    /// Recognizes any line in the `pkgbase` section of SRCINFO data.
397    ///
398    /// This is a wrapper to separate the logic between comments/empty lines and actual `pkgbase`
399    /// properties.
400    fn parser(input: &mut &str) -> ModalResult<PackageBaseProperty> {
401        // Trim any leading spaces, which are allowed per spec.
402        let _ = space0.parse_next(input)?;
403
404        // Look for the `pkgbase` exit condition, which is the start of a `pkgname` section or the
405        // EOL if the pkgname section is missing.
406        // Read the docs above where this function is called for more info.
407        let pkgname = peek(opt(alt(("pkgname", eof)))).parse_next(input)?;
408        if pkgname.is_some() {
409            // If we find a `pkgname` keyword, we know that the current `pkgbase` section finished.
410            // Return a backtrack so the calling parser may wrap up and we can continue with
411            // `pkgname` parsing.
412            return Err(ErrMode::Backtrack(ParserError::from_input(input)));
413        }
414
415        trace(
416            "package_base_line",
417            alt((
418                // First of handle any empty lines or comments.
419                preceded(("#", take_until(0.., "\n")), line_ending)
420                    .map(|s: &str| PackageBaseProperty::Comment(s.to_string())),
421                preceded(space0, line_ending).map(|_| PackageBaseProperty::EmptyLine),
422                // In case we got text, start parsing properties
423                Self::property_parser,
424            )),
425        )
426        .parse_next(input)
427    }
428
429    /// Recognizes keyword assignments in the `pkgbase` section in SRCINFO data.
430    ///
431    /// Since there're a lot of keywords and many of them are shared between the `pkgbase` and
432    /// `pkgname` section, the keywords are bundled into somewhat logical groups.
433    ///
434    /// - [`SourceProperty`] are keywords that are related to the `source` keyword, such as
435    ///   checksums.
436    /// - [`SharedMetaProperty`] are keywords that are related to general meta properties of the
437    ///   package.
438    /// - [`RelationProperty`] are keywords that describe the relation of the package to other
439    ///   packages. [`RawPackageBase`] has two special relations that are explicitly handled in
440    ///   [`Self::exclusive_property_parser`].
441    /// - Other fields that're unique to the [`RawPackageBase`] are handled in
442    ///   [`Self::exclusive_property_parser`].
443    fn property_parser(input: &mut &str) -> ModalResult<PackageBaseProperty> {
444        // First off, get the type of the property.
445        trace(
446            "pkgbase_property",
447            alt((
448                SourceProperty::parser.map(PackageBaseProperty::SourceProperty),
449                SharedMetaProperty::parser.map(PackageBaseProperty::MetaProperty),
450                RelationProperty::parser.map(PackageBaseProperty::RelationProperty),
451                PackageBaseProperty::exclusive_property_parser,
452                cut_err(fail)
453                    .context(StrContext::Label("package base property type"))
454                    .context(StrContext::Expected(StrContextValue::Description(
455                        "one of the allowed pkgbase section properties:",
456                    )))
457                    .context_with(iter_str_context!([
458                        PackageBaseKeyword::VARIANTS,
459                        RelationKeyword::VARIANTS,
460                        SharedMetaKeyword::VARIANTS,
461                        SourceKeyword::VARIANTS,
462                    ])),
463            )),
464        )
465        .parse_next(input)
466    }
467
468    /// Recognizes keyword assignments exclusive to the `pkgbase` section in SRCINFO data.
469    ///
470    /// This function backtracks in case no keyword in this group matches.
471    fn exclusive_property_parser(input: &mut &str) -> ModalResult<PackageBaseProperty> {
472        // First off, get the type of the property.
473        let keyword =
474            trace("exclusive_pkgbase_property", PackageBaseKeyword::parser).parse_next(input)?;
475
476        // Parse a possible architecture suffix for architecture specific fields.
477        let architecture = match keyword {
478            PackageBaseKeyword::MakeDepends | PackageBaseKeyword::CheckDepends => {
479                architecture_suffix.parse_next(input)?
480            }
481            _ => None,
482        };
483
484        // Expect the ` = ` separator between the key-value pair
485        let _ = delimiter.parse_next(input)?;
486
487        let property = match keyword {
488            PackageBaseKeyword::PkgVer => cut_err(
489                till_line_end
490                    .and_then(PackageVersion::parser)
491                    .map(PackageBaseProperty::PackageVersion),
492            )
493            .parse_next(input)?,
494            PackageBaseKeyword::PkgRel => cut_err(
495                till_line_end
496                    .and_then(PackageRelease::parser)
497                    .map(PackageBaseProperty::PackageRelease),
498            )
499            .parse_next(input)?,
500
501            PackageBaseKeyword::Epoch => cut_err(
502                till_line_end
503                    .and_then(Epoch::parser)
504                    .map(PackageBaseProperty::PackageEpoch),
505            )
506            .parse_next(input)?,
507            PackageBaseKeyword::ValidPGPKeys => cut_err(
508                till_line_end
509                    .try_map(OpenPGPIdentifier::from_str)
510                    .map(PackageBaseProperty::ValidPgpKeys),
511            )
512            .parse_next(input)?,
513
514            // Handle `pkgbase` specific package relations.
515            PackageBaseKeyword::MakeDepends | PackageBaseKeyword::CheckDepends => {
516                // Read and parse the generic architecture specific PackageRelation.
517                let value =
518                    cut_err(till_line_end.and_then(PackageRelation::parser)).parse_next(input)?;
519                let arch_property = ArchProperty {
520                    architecture,
521                    value,
522                };
523
524                // Now map the generic relation to the specific relation type.
525                match keyword {
526                    PackageBaseKeyword::CheckDepends => {
527                        PackageBaseProperty::CheckDependency(arch_property)
528                    }
529                    PackageBaseKeyword::MakeDepends => {
530                        PackageBaseProperty::MakeDependency(arch_property)
531                    }
532                    _ => unreachable!(),
533                }
534            }
535        };
536
537        Ok(property)
538    }
539}
540
541/// All possible properties of a `pkgname` section in SRCINFO data.
542///
543/// It's very similar to [`RawPackageBase`], but with less fields and the possibility to explicitly
544/// set some fields to "empty".
545#[derive(Debug)]
546pub enum PackageProperty {
547    EmptyLine,
548    Comment(String),
549    MetaProperty(SharedMetaProperty),
550    RelationProperty(RelationProperty),
551    Clear(ClearableProperty),
552}
553
554impl PackageProperty {
555    /// Handles any line in a `pkgname` package section.
556    ///
557    /// This is a wrapper to separate the logic between comments/empty lines and actual package
558    /// properties.
559    fn parser(input: &mut &str) -> ModalResult<PackageProperty> {
560        // Trim any leading spaces, which are allowed per spec.
561        let _ = space0.parse_next(input)?;
562
563        // Look for one of the `pkgname` exit conditions, which is the start of a new `pkgname`
564        // section. Read the docs above where this function is called for more info.
565        let pkgname = peek(opt("pkgname")).parse_next(input)?;
566        if pkgname.is_some() {
567            // If we find a `pkgname` keyword, we know that the current `pkgname` section finished.
568            // Return a backtrack so the calling parser may wrap up.
569            return Err(ErrMode::Backtrack(ParserError::from_input(input)));
570        }
571
572        // Check if we're at the end of the file.
573        // If so, throw a backtrack error.
574        let eof_found = opt(eof).parse_next(input)?;
575        if eof_found.is_some() {
576            return Err(ErrMode::Backtrack(ParserError::from_input(input)));
577        }
578
579        trace(
580            "package_line",
581            alt((
582                // First of handle any empty lines or comments, which might also occur at the
583                // end of the file.
584                preceded(("#", take_until(0.., "\n")), alt((line_ending, eof)))
585                    .map(|s: &str| PackageProperty::Comment(s.to_string())),
586                preceded(space0, alt((line_ending, eof))).map(|_| PackageProperty::EmptyLine),
587                // In case we got text, start parsing properties
588                Self::property_parser,
589            )),
590        )
591        .parse_next(input)
592    }
593
594    /// Recognizes keyword assignments in a `pkgname` section in SRCINFO data.
595    ///
596    /// Since there're a lot of keywords and many of them are shared between the `pkgbase` and
597    /// `pkgname` section, the keywords are bundled into somewhat logical groups.
598    ///
599    /// - [`SourceProperty`] are keywords that are related to the `source` keyword, such as
600    ///   checksums.
601    /// - [`SharedMetaProperty`] are keywords that are related to general meta properties of the
602    ///   package.
603    /// - [`RelationProperty`] are keywords that describe the relation of the package to other
604    ///   packages. [`RawPackageBase`] has two special relations that are explicitly handled in that
605    ///   enum.
606    fn property_parser(input: &mut &str) -> ModalResult<PackageProperty> {
607        // The way we handle `ClearableProperty` is a bit imperformant.
608        // Since clearable properties are only allowed to occur in `pkgname` sections, I decided to
609        // not handle clearable properties in the respective property parsers to keep the
610        // code as reusable between `pkgbase` and `pkgname` as possible.
611        //
612        // Hence, we do a check for any clearable properties at the very start. If none is detected,
613        // the actual property setters will be checked afterwards.
614        // This means that every property is preceded by `clearable_property` pass.
615        //
616        // I don't expect that this will result in any significant performance issues, but **if**
617        // this were to ever become an issue, it would be a good start to duplicate all
618        // `*_property` parser functions, where one of them explicitly handles clearable properties.
619        trace(
620            "pkgname_property",
621            alt((
622                ClearableProperty::relation_parser.map(PackageProperty::Clear),
623                ClearableProperty::shared_meta_parser.map(PackageProperty::Clear),
624                SharedMetaProperty::parser.map(PackageProperty::MetaProperty),
625                RelationProperty::parser.map(PackageProperty::RelationProperty),
626                cut_err(fail)
627                    .context(StrContext::Label("package property type"))
628                    .context(StrContext::Expected(StrContextValue::Description(
629                        "one of the allowed package section properties:",
630                    )))
631                    .context_with(iter_str_context!([
632                        RelationKeyword::VARIANTS,
633                        SharedMetaKeyword::VARIANTS
634                    ])),
635            )),
636        )
637        .parse_next(input)
638    }
639}
640
641/// Keywords that may exist both in `pkgbase` and `pkgname` sections in SRCINFO data.
642#[derive(Debug, EnumString, VariantNames)]
643#[strum(serialize_all = "lowercase")]
644pub enum SharedMetaKeyword {
645    PkgDesc,
646    Url,
647    License,
648    Arch,
649    Changelog,
650    Install,
651    Groups,
652    Options,
653    Backup,
654}
655
656impl SharedMetaKeyword {
657    /// Recognizes a [`SharedMetaKeyword`] in a string slice.
658    pub fn parser(input: &mut &str) -> ModalResult<SharedMetaKeyword> {
659        // Read until we hit something non alphabetical.
660        // This could be either a space or a `_` in case there's an architecture specifier.
661        trace(
662            "shared_meta_keyword",
663            alpha1.try_map(SharedMetaKeyword::from_str),
664        )
665        .parse_next(input)
666    }
667}
668
669/// Metadata properties that may be shared between `pkgbase` and `pkgname` sections in SRCINFO data.
670#[derive(Debug)]
671pub enum SharedMetaProperty {
672    Description(PackageDescription),
673    Url(Url),
674    License(License),
675    Architecture(Architecture),
676    Changelog(RelativePath),
677    Install(RelativePath),
678    Group(String),
679    Option(MakepkgOption),
680    Backup(RelativePath),
681}
682
683impl SharedMetaProperty {
684    /// Recognizes keyword assignments that may be present in both `pkgbase` and `pkgname` sections
685    /// of SRCINFO data.
686    ///
687    /// This function relies on [`SharedMetaKeyword::parser`] to recognize the relevant keywords.
688    ///
689    /// This function backtracks in case no keyword in this group matches.
690    fn parser(input: &mut &str) -> ModalResult<SharedMetaProperty> {
691        // Now get the type of the property.
692        let keyword = SharedMetaKeyword::parser.parse_next(input)?;
693
694        // Expect the ` = ` separator between the key-value pair
695        let _ = delimiter.parse_next(input)?;
696
697        let property = match keyword {
698            SharedMetaKeyword::PkgDesc => cut_err(
699                till_line_end.map(|s| SharedMetaProperty::Description(PackageDescription::from(s))),
700            )
701            .parse_next(input)?,
702            SharedMetaKeyword::Url => cut_err(
703                till_line_end
704                    .try_map(Url::from_str)
705                    .map(SharedMetaProperty::Url),
706            )
707            .parse_next(input)?,
708            SharedMetaKeyword::License => cut_err(
709                till_line_end
710                    .try_map(License::from_str)
711                    .map(SharedMetaProperty::License),
712            )
713            .parse_next(input)?,
714            SharedMetaKeyword::Arch => cut_err(
715                till_line_end
716                    .try_map(Architecture::from_str)
717                    .map(SharedMetaProperty::Architecture),
718            )
719            .parse_next(input)?,
720            SharedMetaKeyword::Changelog => cut_err(
721                till_line_end
722                    .try_map(Changelog::from_str)
723                    .map(SharedMetaProperty::Changelog),
724            )
725            .parse_next(input)?,
726            SharedMetaKeyword::Install => cut_err(
727                till_line_end
728                    .try_map(Install::from_str)
729                    .map(SharedMetaProperty::Install),
730            )
731            .parse_next(input)?,
732            SharedMetaKeyword::Groups => {
733                cut_err(till_line_end.map(|s| SharedMetaProperty::Group(Group::from(s))))
734                    .parse_next(input)?
735            }
736            SharedMetaKeyword::Options => cut_err(
737                till_line_end
738                    .try_map(MakepkgOption::from_str)
739                    .map(SharedMetaProperty::Option),
740            )
741            .parse_next(input)?,
742            SharedMetaKeyword::Backup => cut_err(
743                till_line_end
744                    .try_map(Backup::from_str)
745                    .map(SharedMetaProperty::Backup),
746            )
747            .parse_next(input)?,
748        };
749
750        Ok(property)
751    }
752}
753
754/// Keywords that describe [alpm-package-relations].
755///
756/// [alpm-package-relations]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
757#[derive(Debug, EnumString, VariantNames)]
758#[strum(serialize_all = "lowercase")]
759pub enum RelationKeyword {
760    Depends,
761    OptDepends,
762    Provides,
763    Conflicts,
764    Replaces,
765}
766
767impl RelationKeyword {
768    /// Recognizes a [`RelationKeyword`] in a string slice.
769    pub fn parser(input: &mut &str) -> ModalResult<RelationKeyword> {
770        // Read until we hit something non alphabetical.
771        // This could be either a space or a `_` in case there's an architecture specifier.
772        trace(
773            "relation_keyword",
774            alpha1.try_map(RelationKeyword::from_str),
775        )
776        .parse_next(input)
777    }
778}
779
780/// Properties related to package relations.
781///
782/// This only handles the shared package relations that can be used in both `pkgbase` and `pkgname`
783/// sections.
784/// `pkgbase` specific relations are explicitly handled in the [`RawPackageBase`] enum.
785/// See [alpm-package-relation] for further details on package relations and [alpm-sonamev1] for
786/// information on _soname_ handling.
787/// [alpm-package-relation]: <https://alpm.archlinux.page/specifications/alpm-package-relation.7.html>
788/// [alpm-sonamev1]: <https://alpm.archlinux.page/specifications/alpm-sonamev1.7.html>
789#[derive(Debug)]
790pub enum RelationProperty {
791    Dependency(ArchProperty<RelationOrSoname>),
792    OptionalDependency(ArchProperty<OptionalDependency>),
793    Provides(ArchProperty<RelationOrSoname>),
794    Conflicts(ArchProperty<PackageRelation>),
795    Replaces(ArchProperty<PackageRelation>),
796}
797
798impl RelationProperty {
799    /// Recognizes package relation keyword assignments that may be present in both `pkgbase` and
800    /// `pkgname` sections in SRCINFO data.
801    ///
802    /// This function relies on [`RelationKeyword::parser`] to recognize the relevant keywords.
803    /// This function backtracks in case no keyword in this group matches.
804    fn parser(input: &mut &str) -> ModalResult<RelationProperty> {
805        // First off, get the type of the property.
806        let keyword = RelationKeyword::parser.parse_next(input)?;
807
808        // All of these properties can be architecture specific and may have an architecture suffix.
809        // Get it if there's one.
810        let architecture = architecture_suffix.parse_next(input)?;
811
812        // Expect the ` = ` separator between the key-value pair
813        let _ = delimiter.parse_next(input)?;
814
815        let property = match keyword {
816            // Handle these together in a single blob as they all deserialize to the same base type.
817            RelationKeyword::Conflicts | RelationKeyword::Replaces => {
818                // Read and parse the generic architecture specific PackageRelation.
819                let value =
820                    cut_err(till_line_end.and_then(PackageRelation::parser)).parse_next(input)?;
821                let arch_property = ArchProperty {
822                    architecture,
823                    value,
824                };
825
826                // Now map the generic relation to the specific relation type.
827                match keyword {
828                    RelationKeyword::Replaces => RelationProperty::Replaces(arch_property),
829                    RelationKeyword::Conflicts => RelationProperty::Conflicts(arch_property),
830                    _ => unreachable!(),
831                }
832            }
833            RelationKeyword::Depends | RelationKeyword::Provides => {
834                // Read and parse the generic architecture specific RelationOrSoname.
835                let value =
836                    cut_err(till_line_end.try_map(RelationOrSoname::from_str)).parse_next(input)?;
837                let arch_property = ArchProperty {
838                    architecture,
839                    value,
840                };
841
842                // Now map the generic relation to the specific relation type.
843                match keyword {
844                    RelationKeyword::Depends => RelationProperty::Dependency(arch_property),
845                    RelationKeyword::Provides => RelationProperty::Provides(arch_property),
846                    _ => unreachable!(),
847                }
848            }
849            RelationKeyword::OptDepends => cut_err(
850                till_line_end
851                    .and_then(OptionalDependency::parser)
852                    .map(|value| {
853                        RelationProperty::OptionalDependency(ArchProperty {
854                            architecture,
855                            value,
856                        })
857                    }),
858            )
859            .parse_next(input)?,
860        };
861
862        Ok(property)
863    }
864
865    // Returns the [`Architecture`] of the current variant.
866    //
867    // Can be used to extract the architecture without knowing which variant this is.
868    pub fn architecture(&self) -> Option<Architecture> {
869        match self {
870            RelationProperty::Dependency(arch_property) => arch_property.architecture,
871            RelationProperty::OptionalDependency(arch_property) => arch_property.architecture,
872            RelationProperty::Provides(arch_property) => arch_property.architecture,
873            RelationProperty::Conflicts(arch_property) => arch_property.architecture,
874            RelationProperty::Replaces(arch_property) => arch_property.architecture,
875        }
876    }
877}
878
879/// Package source keywords that are exclusive to the `pkgbase` section in SRCINFO data.
880#[derive(Debug, EnumString, VariantNames)]
881#[strum(serialize_all = "lowercase")]
882pub enum SourceKeyword {
883    Source,
884    NoExtract,
885    B2sums,
886    Md5sums,
887    Sha1sums,
888    Sha224sums,
889    Sha256sums,
890    Sha384sums,
891    Sha512sums,
892}
893
894impl SourceKeyword {
895    /// Parse a [`SourceKeyword`].
896    pub fn parser(input: &mut &str) -> ModalResult<SourceKeyword> {
897        // Read until we hit something non alphabetical.
898        // This could be either a space or a `_` in case there's an architecture specifier.
899        trace(
900            "source_keyword",
901            alphanumeric1.try_map(SourceKeyword::from_str),
902        )
903        .parse_next(input)
904    }
905}
906
907/// Properties related to package sources.
908///
909/// Sources and related properties can be architecture specific.
910///
911/// The `source`, `noextract` and checksum related keywords in SRCINFO data correlate in ordering:
912/// `noextract` and any checksum entries are ordered in the same way as the respective `source`
913/// entry they relate to. The representation of this correlation is normalized after initial
914/// parsing.
915#[derive(Debug)]
916pub enum SourceProperty {
917    Source(ArchProperty<Source>),
918    NoExtract(ArchProperty<String>),
919    B2Checksum(ArchProperty<SkippableChecksum<Blake2b512>>),
920    Md5Checksum(ArchProperty<SkippableChecksum<Md5>>),
921    Sha1Checksum(ArchProperty<SkippableChecksum<Sha1>>),
922    Sha256Checksum(ArchProperty<SkippableChecksum<Sha256>>),
923    Sha224Checksum(ArchProperty<SkippableChecksum<Sha224>>),
924    Sha384Checksum(ArchProperty<SkippableChecksum<Sha384>>),
925    Sha512Checksum(ArchProperty<SkippableChecksum<Sha512>>),
926}
927
928impl SourceProperty {
929    /// Recognizes package source related keyword assignments in SRCINFO data.
930    ///
931    /// This function relies on [`SourceKeyword::parser`] to recognize the relevant keywords.
932    ///
933    /// This function backtracks in case no keyword in this group matches.
934    fn parser(input: &mut &str) -> ModalResult<SourceProperty> {
935        // First off, get the type of the property.
936        let keyword = SourceKeyword::parser.parse_next(input)?;
937
938        // All properties may be architecture specific and thereby have an architecture suffix.
939        let architecture = architecture_suffix.parse_next(input)?;
940
941        // Expect the ` = ` separator between the key-value pair
942        let _ = delimiter.parse_next(input)?;
943
944        let property = match keyword {
945            SourceKeyword::Source => {
946                cut_err(till_line_end.try_map(Source::from_str).map(|value| {
947                    SourceProperty::Source(ArchProperty {
948                        architecture,
949                        value,
950                    })
951                }))
952                .parse_next(input)?
953            }
954            SourceKeyword::NoExtract => cut_err(till_line_end.map(|s| {
955                SourceProperty::NoExtract(ArchProperty {
956                    architecture,
957                    value: s.to_string(),
958                })
959            }))
960            .parse_next(input)?,
961            // all checksum properties are parsed the same way.
962            SourceKeyword::B2sums => SourceProperty::B2Checksum(ArchProperty {
963                architecture,
964                value: till_line_end
965                    .and_then(SkippableChecksum::parser)
966                    .parse_next(input)?,
967            }),
968            SourceKeyword::Md5sums => SourceProperty::Md5Checksum(ArchProperty {
969                architecture,
970                value: till_line_end
971                    .and_then(SkippableChecksum::parser)
972                    .parse_next(input)?,
973            }),
974            SourceKeyword::Sha1sums => SourceProperty::Sha1Checksum(ArchProperty {
975                architecture,
976                value: till_line_end
977                    .and_then(SkippableChecksum::parser)
978                    .parse_next(input)?,
979            }),
980            SourceKeyword::Sha224sums => SourceProperty::Sha224Checksum(ArchProperty {
981                architecture,
982                value: till_line_end
983                    .and_then(SkippableChecksum::parser)
984                    .parse_next(input)?,
985            }),
986            SourceKeyword::Sha256sums => SourceProperty::Sha256Checksum(ArchProperty {
987                architecture,
988                value: till_line_end
989                    .and_then(SkippableChecksum::parser)
990                    .parse_next(input)?,
991            }),
992            SourceKeyword::Sha384sums => SourceProperty::Sha384Checksum(ArchProperty {
993                architecture,
994                value: till_line_end
995                    .and_then(SkippableChecksum::parser)
996                    .parse_next(input)?,
997            }),
998            SourceKeyword::Sha512sums => SourceProperty::Sha512Checksum(ArchProperty {
999                architecture,
1000                value: till_line_end
1001                    .and_then(SkippableChecksum::parser)
1002                    .parse_next(input)?,
1003            }),
1004        };
1005
1006        Ok(property)
1007    }
1008}
1009
1010/// Properties used in `pkgname` sections that can be cleared.
1011///
1012/// Some variants of this enum are architecture-specific, as they might only be cleared for a
1013/// specific architecture, but not for another.
1014///
1015/// Clearing a keyword in SRCINFO data is achieved by an empty keyword assignment, e.g.:
1016///
1017/// ```txt
1018/// depends =
1019/// ```
1020#[derive(Debug, Clone)]
1021pub enum ClearableProperty {
1022    Description,
1023    Url,
1024    Licenses,
1025    Changelog,
1026
1027    Install,
1028    Groups,
1029    Options,
1030    Backups,
1031
1032    Dependencies(Option<Architecture>),
1033    OptionalDependencies(Option<Architecture>),
1034    Provides(Option<Architecture>),
1035    Conflicts(Option<Architecture>),
1036    Replaces(Option<Architecture>),
1037}
1038
1039impl ClearableProperty {
1040    /// Recognizes all keyword assignments in SRCINFO data that represent a cleared
1041    /// [`SharedMetaProperty`].
1042    ///
1043    /// A cleared property is represented by a keyword that is assigned an empty value.
1044    /// It indicates that the keyword assignment should remain empty for a given package.
1045    ///
1046    /// Example:
1047    /// ```txt
1048    /// pkgdesc =
1049    /// depends =
1050    /// ```
1051    ///
1052    /// The above properties would indicate that both `pkgdesc` and the `depends` array are to be
1053    /// cleared and left empty for a given package.
1054    ///
1055    /// This function backtracks in case no keyword in this group matches or in case the property is
1056    /// not cleared.
1057    fn shared_meta_parser(input: &mut &str) -> ModalResult<ClearableProperty> {
1058        // First off, check if this is any of the clearable properties.
1059        let keyword =
1060            trace("clearable_shared_meta_property", SharedMetaKeyword::parser).parse_next(input)?;
1061
1062        // Now check if it's actually a clear.
1063        // This parser fails and backtracks in case there's anything but spaces and a newline after
1064        // the delimiter, which indicates that there's an actual value that is set for this
1065        // property.
1066        let _ = (" =", space0, newline).parse_next(input)?;
1067
1068        let property = match keyword {
1069            // The `Arch` property matches the keyword, but isn't clearable.
1070            SharedMetaKeyword::Arch => {
1071                return Err(ErrMode::Backtrack(ParserError::from_input(input)));
1072            }
1073            SharedMetaKeyword::PkgDesc => ClearableProperty::Description,
1074            SharedMetaKeyword::Url => ClearableProperty::Url,
1075            SharedMetaKeyword::License => ClearableProperty::Licenses,
1076            SharedMetaKeyword::Changelog => ClearableProperty::Changelog,
1077            SharedMetaKeyword::Install => ClearableProperty::Install,
1078            SharedMetaKeyword::Groups => ClearableProperty::Groups,
1079            SharedMetaKeyword::Options => ClearableProperty::Options,
1080            SharedMetaKeyword::Backup => ClearableProperty::Backups,
1081        };
1082
1083        Ok(property)
1084    }
1085
1086    /// Same as [`Self::shared_meta_parser`], but for clearable [RelationProperty].
1087    fn relation_parser(input: &mut &str) -> ModalResult<ClearableProperty> {
1088        // First off, check if this is any of the clearable properties.
1089        let keyword = trace("clearable_property", RelationKeyword::parser).parse_next(input)?;
1090
1091        // All relations may be architecture specific.
1092        let architecture = architecture_suffix.parse_next(input)?;
1093
1094        // Now check if it's actually a clear.
1095        // This parser fails and backtracks in case there's anything but spaces and a newline after
1096        // the delimiter, which indicates that there's an actual value that is set for this
1097        // property.
1098        let _ = (" =", space0, newline).parse_next(input)?;
1099
1100        let property = match keyword {
1101            RelationKeyword::Depends => ClearableProperty::Dependencies(architecture),
1102            RelationKeyword::OptDepends => ClearableProperty::OptionalDependencies(architecture),
1103            RelationKeyword::Provides => ClearableProperty::Provides(architecture),
1104            RelationKeyword::Conflicts => ClearableProperty::Conflicts(architecture),
1105            RelationKeyword::Replaces => ClearableProperty::Replaces(architecture),
1106        };
1107
1108        Ok(property)
1109    }
1110}