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