1use std::{
6 fmt::{Display, Formatter, Result as FmtResult, Write},
7 str::FromStr,
8};
9
10use alpm_types::{
11 Architecture,
12 Base64OpenPGPSignature,
13 BuildDate,
14 CompressedSize,
15 FullVersion,
16 Group,
17 InstalledSize,
18 License,
19 Name,
20 OptionalDependency,
21 PackageBaseName,
22 PackageDescription,
23 PackageFileName,
24 PackageRelation,
25 Packager,
26 RelationOrSoname,
27 Sha256Checksum,
28 Url,
29};
30use winnow::Parser;
31
32use crate::{
33 Error,
34 desc::{
35 RepoDescFileV1,
36 Section,
37 parser::{SectionKeyword, sections},
38 },
39};
40
41#[derive(Clone, Debug, serde::Deserialize, PartialEq, serde::Serialize)]
108#[serde(deny_unknown_fields)]
109#[serde(rename_all = "lowercase")]
110pub struct RepoDescFileV2 {
111 pub file_name: PackageFileName,
113
114 pub name: Name,
116
117 pub base: PackageBaseName,
119
120 pub version: FullVersion,
122
123 pub description: PackageDescription,
127
128 pub groups: Vec<Group>,
132
133 pub compressed_size: CompressedSize,
135
136 pub installed_size: InstalledSize,
140
141 pub sha256_checksum: Sha256Checksum,
143
144 pub pgp_signature: Option<Base64OpenPGPSignature>,
148
149 pub url: Option<Url>,
151
152 pub license: Vec<License>,
156
157 pub arch: Architecture,
159
160 pub build_date: BuildDate,
162
163 pub packager: Packager,
165
166 pub replaces: Vec<PackageRelation>,
170
171 pub conflicts: Vec<PackageRelation>,
175
176 pub provides: Vec<RelationOrSoname>,
180
181 pub dependencies: Vec<RelationOrSoname>,
185
186 pub optional_dependencies: Vec<OptionalDependency>,
190
191 pub make_dependencies: Vec<PackageRelation>,
195
196 pub check_dependencies: Vec<PackageRelation>,
200}
201
202impl Display for RepoDescFileV2 {
203 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
204 fn single<T: Display, W: Write>(f: &mut W, key: &str, val: &T) -> FmtResult {
206 writeln!(f, "%{key}%\n{val}\n")
207 }
208
209 fn section<T: Display, W: Write>(f: &mut W, key: &str, vals: &[T]) -> FmtResult {
211 if vals.is_empty() {
212 return Ok(());
213 }
214 writeln!(f, "%{key}%")?;
215 for v in vals {
216 writeln!(f, "{v}")?;
217 }
218 writeln!(f)
219 }
220
221 single(f, "FILENAME", &self.file_name)?;
222 single(f, "NAME", &self.name)?;
223 single(f, "BASE", &self.base)?;
224 single(f, "VERSION", &self.version)?;
225 if !&self.description.as_ref().is_empty() {
226 single(f, "DESC", &self.description)?;
227 }
228 section(f, "GROUPS", &self.groups)?;
229 single(f, "CSIZE", &self.compressed_size)?;
230 single(f, "ISIZE", &self.installed_size)?;
231 single(f, "SHA256SUM", &self.sha256_checksum)?;
232 if let Some(pgpsig) = &self.pgp_signature {
233 single(f, "PGPSIG", pgpsig)?;
234 }
235 if let Some(url) = &self.url {
236 single(f, "URL", url)?;
237 }
238 section(f, "LICENSE", &self.license)?;
239 single(f, "ARCH", &self.arch)?;
240 single(f, "BUILDDATE", &self.build_date)?;
241 single(f, "PACKAGER", &self.packager)?;
242 section(f, "REPLACES", &self.replaces)?;
243 section(f, "CONFLICTS", &self.conflicts)?;
244 section(f, "PROVIDES", &self.provides)?;
245 section(f, "DEPENDS", &self.dependencies)?;
246 section(f, "OPTDEPENDS", &self.optional_dependencies)?;
247 section(f, "MAKEDEPENDS", &self.make_dependencies)?;
248 section(f, "CHECKDEPENDS", &self.check_dependencies)?;
249 Ok(())
250 }
251}
252
253impl FromStr for RepoDescFileV2 {
254 type Err = Error;
255
256 fn from_str(s: &str) -> Result<Self, Self::Err> {
325 let sections = sections.parse(s)?;
326 Self::try_from(sections)
327 }
328}
329
330impl TryFrom<Vec<Section>> for RepoDescFileV2 {
331 type Error = Error;
332
333 fn try_from(sections: Vec<Section>) -> Result<Self, Self::Error> {
343 let mut file_name = None;
344 let mut name = None;
345 let mut base = None;
346 let mut version = None;
347 let mut description = None;
348 let mut groups: Vec<Group> = Vec::new();
349 let mut compressed_size = None;
350 let mut installed_size = None;
351 let mut sha256_checksum = None;
352 let mut pgp_signature = None;
353 let mut url = None;
354 let mut license: Vec<License> = Vec::new();
355 let mut arch = None;
356 let mut build_date = None;
357 let mut packager = None;
358 let mut replaces: Vec<PackageRelation> = Vec::new();
359 let mut conflicts: Vec<PackageRelation> = Vec::new();
360 let mut provides: Vec<RelationOrSoname> = Vec::new();
361 let mut dependencies: Vec<RelationOrSoname> = Vec::new();
362 let mut optional_dependencies: Vec<OptionalDependency> = Vec::new();
363 let mut make_dependencies: Vec<PackageRelation> = Vec::new();
364 let mut check_dependencies: Vec<PackageRelation> = Vec::new();
365
366 macro_rules! set_once {
368 ($field:ident, $val:expr, $kw:expr) => {{
369 if $field.is_some() {
370 return Err(Error::DuplicateSection($kw));
371 }
372 $field = Some($val);
373 }};
374 }
375
376 macro_rules! set_vec_once {
379 ($field:ident, $val:expr, $kw:expr) => {{
380 if !$field.is_empty() {
381 return Err(Error::DuplicateSection($kw));
382 }
383 if $val.is_empty() {
384 return Err(Error::EmptySection($kw));
385 }
386 $field = $val;
387 }};
388 }
389
390 for section in sections {
391 match section {
392 Section::Filename(val) => set_once!(file_name, val, SectionKeyword::Filename),
393 Section::Name(val) => set_once!(name, val, SectionKeyword::Name),
394 Section::Base(val) => set_once!(base, val, SectionKeyword::Base),
395 Section::Version(val) => set_once!(version, val, SectionKeyword::Version),
396 Section::Desc(val) => set_once!(description, val, SectionKeyword::Desc),
397 Section::Groups(val) => set_vec_once!(groups, val, SectionKeyword::Groups),
398 Section::CSize(val) => set_once!(compressed_size, val, SectionKeyword::CSize),
399 Section::ISize(val) => set_once!(installed_size, val, SectionKeyword::ISize),
400 Section::Sha256Sum(val) => {
401 set_once!(sha256_checksum, val, SectionKeyword::Sha256Sum)
402 }
403 Section::PgpSig(val) => set_once!(pgp_signature, val, SectionKeyword::PgpSig),
404 Section::Url(val) => set_once!(url, val, SectionKeyword::Url),
405 Section::License(val) => set_vec_once!(license, val, SectionKeyword::License),
406 Section::Arch(val) => set_once!(arch, val, SectionKeyword::Arch),
407 Section::BuildDate(val) => set_once!(build_date, val, SectionKeyword::BuildDate),
408 Section::Packager(val) => set_once!(packager, val, SectionKeyword::Packager),
409 Section::Replaces(val) => set_vec_once!(replaces, val, SectionKeyword::Replaces),
410 Section::Conflicts(val) => set_vec_once!(conflicts, val, SectionKeyword::Conflicts),
411 Section::Provides(val) => set_vec_once!(provides, val, SectionKeyword::Provides),
412 Section::Depends(val) => set_vec_once!(dependencies, val, SectionKeyword::Depends),
413 Section::OptDepends(val) => {
414 set_vec_once!(optional_dependencies, val, SectionKeyword::OptDepends)
415 }
416 Section::MakeDepends(val) => {
417 set_vec_once!(make_dependencies, val, SectionKeyword::MakeDepends)
418 }
419 Section::CheckDepends(val) => {
420 set_vec_once!(check_dependencies, val, SectionKeyword::CheckDepends)
421 }
422 Section::Md5Sum(_) => {
423 return Err(Error::InvalidSectionForVersion {
424 section: SectionKeyword::Md5Sum,
425 version: 2,
426 });
427 }
428 }
429 }
430
431 Ok(RepoDescFileV2 {
432 file_name: file_name.ok_or(Error::MissingSection(SectionKeyword::Filename))?,
433 name: name.ok_or(Error::MissingSection(SectionKeyword::Name))?,
434 base: base.ok_or(Error::MissingSection(SectionKeyword::Base))?,
435 version: version.ok_or(Error::MissingSection(SectionKeyword::Version))?,
436 description: description.unwrap_or_default(),
437 groups,
438 compressed_size: compressed_size.ok_or(Error::MissingSection(SectionKeyword::CSize))?,
439 installed_size: installed_size.ok_or(Error::MissingSection(SectionKeyword::ISize))?,
440 sha256_checksum: sha256_checksum
441 .ok_or(Error::MissingSection(SectionKeyword::Sha256Sum))?,
442 pgp_signature,
443 url: url.unwrap_or(None),
444 license,
445 arch: arch.ok_or(Error::MissingSection(SectionKeyword::Arch))?,
446 build_date: build_date.ok_or(Error::MissingSection(SectionKeyword::BuildDate))?,
447 packager: packager.ok_or(Error::MissingSection(SectionKeyword::Packager))?,
448 replaces,
449 conflicts,
450 provides,
451 dependencies,
452 optional_dependencies,
453 make_dependencies,
454 check_dependencies,
455 })
456 }
457}
458
459impl From<RepoDescFileV1> for RepoDescFileV2 {
460 fn from(v1: RepoDescFileV1) -> Self {
466 RepoDescFileV2 {
467 file_name: v1.file_name,
468 name: v1.name,
469 base: v1.base,
470 version: v1.version,
471 description: v1.description,
472 groups: v1.groups,
473 compressed_size: v1.compressed_size,
474 installed_size: v1.installed_size,
475 sha256_checksum: v1.sha256_checksum,
476 pgp_signature: Some(v1.pgp_signature),
477 url: v1.url,
478 license: v1.license,
479 arch: v1.arch,
480 build_date: v1.build_date,
481 packager: v1.packager,
482 replaces: v1.replaces,
483 conflicts: v1.conflicts,
484 provides: v1.provides,
485 dependencies: v1.dependencies,
486 optional_dependencies: v1.optional_dependencies,
487 make_dependencies: v1.make_dependencies,
488 check_dependencies: v1.check_dependencies,
489 }
490 }
491}
492
493#[cfg(test)]
494mod tests {
495 use pretty_assertions::assert_eq;
496 use rstest::*;
497 use testresult::TestResult;
498
499 use super::*;
500
501 const VALID_DESC_FILE: &str = r#"%FILENAME%
502example-1.0.0-1-any.pkg.tar.zst
503
504%NAME%
505example
506
507%BASE%
508example
509
510%VERSION%
5111.0.0-1
512
513%DESC%
514An example package
515
516%GROUPS%
517example-group
518other-group
519
520%CSIZE%
5211818463
522
523%ISIZE%
52418184634
525
526%SHA256SUM%
527b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
528
529%URL%
530https://example.org/
531
532%LICENSE%
533MIT
534Apache-2.0
535
536%ARCH%
537x86_64
538
539%BUILDDATE%
5401729181726
541
542%PACKAGER%
543Foobar McFooface <foobar@mcfooface.org>
544
545%REPLACES%
546other-pkg-replaced
547
548%CONFLICTS%
549other-pkg-conflicts
550
551%PROVIDES%
552example-component
553lib:libexample.so.1
554
555%DEPENDS%
556glibc
557gcc-libs
558libdep.so=1-64
559
560%OPTDEPENDS%
561bash: for a script
562
563%MAKEDEPENDS%
564cmake
565
566%CHECKDEPENDS%
567bats
568
569"#;
570
571 const VALID_DESC_FILE_MINIMAL: &str = r#"%FILENAME%
572example-1.0.0-1-any.pkg.tar.zst
573
574%NAME%
575example
576
577%BASE%
578example
579
580%VERSION%
5811.0.0-1
582
583%CSIZE%
5841818463
585
586%ISIZE%
58718184634
588
589%SHA256SUM%
590b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
591
592%ARCH%
593x86_64
594
595%BUILDDATE%
5961729181726
597
598%PACKAGER%
599Foobar McFooface <foobar@mcfooface.org>
600
601"#;
602
603 const VALID_DESC_FILE_EMPTY_FIELDS: &str = r#"%FILENAME%
604example-1.0.0-1-any.pkg.tar.zst
605
606%NAME%
607example
608
609%BASE%
610example
611
612%VERSION%
6131.0.0-1
614
615%DESC%
616
617%CSIZE%
6181818463
619
620%ISIZE%
62118184634
622
623%SHA256SUM%
624b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
625
626%URL%
627
628%ARCH%
629x86_64
630
631%BUILDDATE%
6321729181726
633
634%PACKAGER%
635Foobar McFooface <foobar@mcfooface.org>
636
637"#;
638
639 #[test]
640 fn parse_valid_v2_desc() -> TestResult {
641 let actual = RepoDescFileV2::from_str(VALID_DESC_FILE)?;
642 let expected = RepoDescFileV2 {
643 file_name: PackageFileName::from_str("example-1.0.0-1-any.pkg.tar.zst")?,
644 name: Name::from_str("example")?,
645 base: PackageBaseName::from_str("example")?,
646 version: FullVersion::from_str("1.0.0-1")?,
647 description: PackageDescription::from("An example package"),
648 groups: vec![
649 Group::from_str("example-group")?,
650 Group::from_str("other-group")?,
651 ],
652 compressed_size: CompressedSize::from_str("1818463")?,
653 installed_size: InstalledSize::from_str("18184634")?,
654 sha256_checksum: Sha256Checksum::from_str(
655 "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c",
656 )?,
657 pgp_signature: None,
658 url: Some(Url::from_str("https://example.org")?),
659 license: vec![License::from_str("MIT")?, License::from_str("Apache-2.0")?],
660 arch: Architecture::from_str("x86_64")?,
661 build_date: BuildDate::from_str("1729181726")?,
662 packager: Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?,
663 replaces: vec![PackageRelation::from_str("other-pkg-replaced")?],
664 conflicts: vec![PackageRelation::from_str("other-pkg-conflicts")?],
665 provides: vec![
666 RelationOrSoname::from_str("example-component")?,
667 RelationOrSoname::from_str("lib:libexample.so.1")?,
668 ],
669 dependencies: vec![
670 RelationOrSoname::from_str("glibc")?,
671 RelationOrSoname::from_str("gcc-libs")?,
672 RelationOrSoname::from_str("libdep.so=1-64")?,
673 ],
674 optional_dependencies: vec![OptionalDependency::from_str("bash: for a script")?],
675 make_dependencies: vec![PackageRelation::from_str("cmake")?],
676 check_dependencies: vec![PackageRelation::from_str("bats")?],
677 };
678 assert_eq!(actual, expected);
679 assert_eq!(VALID_DESC_FILE, actual.to_string());
680 Ok(())
681 }
682
683 #[test]
684 fn parse_valid_v2_desc_minimal() -> TestResult {
685 let actual = RepoDescFileV2::from_str(VALID_DESC_FILE_MINIMAL)?;
686 let expected = RepoDescFileV2 {
687 file_name: PackageFileName::from_str("example-1.0.0-1-any.pkg.tar.zst")?,
688 name: Name::from_str("example")?,
689 base: PackageBaseName::from_str("example")?,
690 version: FullVersion::from_str("1.0.0-1")?,
691 description: PackageDescription::from(""),
692 groups: vec![],
693 compressed_size: CompressedSize::from_str("1818463")?,
694 installed_size: InstalledSize::from_str("18184634")?,
695 sha256_checksum: Sha256Checksum::from_str(
696 "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c",
697 )?,
698 pgp_signature: None,
699 url: None,
700 license: vec![],
701 arch: Architecture::from_str("x86_64")?,
702 build_date: BuildDate::from_str("1729181726")?,
703 packager: Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?,
704 replaces: vec![],
705 conflicts: vec![],
706 provides: vec![],
707 dependencies: vec![],
708 optional_dependencies: vec![],
709 make_dependencies: vec![],
710 check_dependencies: vec![],
711 };
712 assert_eq!(actual, expected);
713 assert_eq!(VALID_DESC_FILE_MINIMAL, actual.to_string());
714 Ok(())
715 }
716
717 #[rstest]
718 #[case(VALID_DESC_FILE, VALID_DESC_FILE)]
719 #[case(VALID_DESC_FILE_MINIMAL, VALID_DESC_FILE_MINIMAL)]
720 #[case(VALID_DESC_FILE_EMPTY_FIELDS, VALID_DESC_FILE_MINIMAL)]
722 fn parser_roundtrip(#[case] input: &str, #[case] expected: &str) -> TestResult {
723 let desc = RepoDescFileV2::from_str(input)?;
724 let output = desc.to_string();
725 assert_eq!(output, expected);
726 let desc_roundtrip = RepoDescFileV2::from_str(&output)?;
727 assert_eq!(desc, desc_roundtrip);
728 Ok(())
729 }
730
731 #[rstest]
732 #[case("%UNKNOWN%\nvalue", "invalid section name")]
733 #[case("%VERSION%\n1.0.0-1\n", "Missing section: %FILENAME%")]
734 fn invalid_desc_parser(#[case] input: &str, #[case] error_snippet: &str) {
735 let result = RepoDescFileV2::from_str(input);
736 assert!(result.is_err());
737 let err = result.unwrap_err();
738 let pretty_error = err.to_string();
739 assert!(
740 pretty_error.contains(error_snippet),
741 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
742 );
743 }
744
745 #[test]
746 fn missing_required_section_should_fail() {
747 let input = "%VERSION%\n1.0.0-1\n";
748 let result = RepoDescFileV2::from_str(input);
749 assert!(matches!(result, Err(Error::MissingSection(s)) if s == SectionKeyword::Filename));
750 }
751}