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}