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