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