1use std::{
6 fmt::{Display, Formatter, Result as FmtResult, Write},
7 str::FromStr,
8};
9
10use alpm_types::{
11 Architecture,
12 BuildDate,
13 FullVersion,
14 Group,
15 InstalledSize,
16 License,
17 Name,
18 OptionalDependency,
19 PackageBaseName,
20 PackageDescription,
21 PackageInstallReason,
22 PackageRelation,
23 PackageValidation,
24 Packager,
25 RelationOrSoname,
26 Url,
27};
28use winnow::Parser;
29
30use crate::{
31 Error,
32 desc::{
33 DbDescFileV2,
34 Section,
35 parser::{SectionKeyword, sections},
36 },
37};
38
39#[derive(Clone, Debug, serde::Deserialize, PartialEq, serde::Serialize)]
126#[serde(deny_unknown_fields)]
127#[serde(rename_all = "lowercase")]
128pub struct DbDescFileV1 {
129 pub name: Name,
131
132 pub version: FullVersion,
134
135 pub base: PackageBaseName,
137
138 pub description: PackageDescription,
140
141 pub url: Option<Url>,
143
144 pub arch: Architecture,
146
147 pub builddate: BuildDate,
149
150 pub installdate: BuildDate,
152
153 pub packager: Packager,
155
156 pub size: InstalledSize,
158
159 pub groups: Vec<Group>,
161
162 pub reason: PackageInstallReason,
164
165 pub license: Vec<License>,
167
168 pub validation: Vec<PackageValidation>,
170
171 pub replaces: Vec<PackageRelation>,
173
174 pub depends: Vec<RelationOrSoname>,
176
177 pub optdepends: Vec<OptionalDependency>,
179
180 pub conflicts: Vec<PackageRelation>,
182
183 pub provides: Vec<RelationOrSoname>,
185}
186
187impl Display for DbDescFileV1 {
188 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
189 fn single<T: Display, W: Write>(f: &mut W, key: &str, val: &T) -> FmtResult {
191 writeln!(f, "%{key}%\n{val}\n")
192 }
193
194 fn section<T: Display, W: Write>(f: &mut W, key: &str, vals: &[T]) -> FmtResult {
196 if vals.is_empty() {
197 return Ok(());
198 }
199 writeln!(f, "%{key}%")?;
200 for v in vals {
201 writeln!(f, "{v}")?;
202 }
203 writeln!(f)
204 }
205
206 single(f, "NAME", &self.name)?;
207 single(f, "VERSION", &self.version)?;
208 single(f, "BASE", &self.base)?;
209 single(f, "DESC", &self.description)?;
210 single(
212 f,
213 "URL",
214 &self
215 .url
216 .as_ref()
217 .map_or(String::new(), |url| url.to_string()),
218 )?;
219 single(f, "ARCH", &self.arch)?;
220 single(f, "BUILDDATE", &self.builddate)?;
221 single(f, "INSTALLDATE", &self.installdate)?;
222 single(f, "PACKAGER", &self.packager)?;
223 if self.size != 0 {
225 single(f, "SIZE", &self.size)?;
226 }
227 section(f, "GROUPS", &self.groups)?;
228 if self.reason != PackageInstallReason::Explicit {
230 single(f, "REASON", &self.reason)?;
231 }
232 section(f, "LICENSE", &self.license)?;
233 section(f, "VALIDATION", &self.validation)?;
234 section(f, "REPLACES", &self.replaces)?;
235 section(f, "DEPENDS", &self.depends)?;
236 section(f, "OPTDEPENDS", &self.optdepends)?;
237 section(f, "CONFLICTS", &self.conflicts)?;
238 section(f, "PROVIDES", &self.provides)?;
239
240 Ok(())
241 }
242}
243
244impl FromStr for DbDescFileV1 {
245 type Err = Error;
246
247 fn from_str(s: &str) -> Result<Self, Self::Err> {
310 let sections = sections.parse(s)?;
311 Self::try_from(sections)
312 }
313}
314
315impl TryFrom<Vec<Section>> for DbDescFileV1 {
316 type Error = Error;
317
318 fn try_from(sections: Vec<Section>) -> Result<Self, Self::Error> {
328 let mut name = None;
329 let mut version = None;
330 let mut base = None;
331 let mut description = None;
332 let mut url = None;
333 let mut arch = None;
334 let mut builddate = None;
335 let mut installdate = None;
336 let mut packager = None;
337 let mut size = None;
338
339 let mut groups: Vec<Group> = Vec::new();
340 let mut reason = None;
341 let mut license: Vec<License> = Vec::new();
342 let mut validation = None;
343 let mut replaces: Vec<PackageRelation> = Vec::new();
344 let mut depends: Vec<RelationOrSoname> = Vec::new();
345 let mut optdepends: Vec<OptionalDependency> = Vec::new();
346 let mut conflicts: Vec<PackageRelation> = Vec::new();
347 let mut provides: Vec<RelationOrSoname> = Vec::new();
348
349 macro_rules! set_once {
351 ($field:ident, $val:expr, $kw:expr) => {{
352 if $field.is_some() {
353 return Err(Error::DuplicateSection($kw));
354 }
355 $field = Some($val);
356 }};
357 }
358
359 macro_rules! set_vec_once {
361 ($field:ident, $val:expr, $kw:expr) => {{
362 if !$field.is_empty() {
363 return Err(Error::DuplicateSection($kw));
364 }
365 $field = $val;
366 }};
367 }
368
369 for section in sections {
370 match section {
371 Section::Name(v) => set_once!(name, v, SectionKeyword::Name),
372 Section::Version(v) => set_once!(version, v, SectionKeyword::Version),
373 Section::Base(v) => set_once!(base, v, SectionKeyword::Base),
374 Section::Desc(v) => set_once!(description, v, SectionKeyword::Desc),
375 Section::Url(v) => set_once!(url, v, SectionKeyword::Url),
376 Section::Arch(v) => set_once!(arch, v, SectionKeyword::Arch),
377 Section::BuildDate(v) => set_once!(builddate, v, SectionKeyword::BuildDate),
378 Section::InstallDate(v) => set_once!(installdate, v, SectionKeyword::InstallDate),
379 Section::Packager(v) => set_once!(packager, v, SectionKeyword::Packager),
380 Section::Size(v) => set_once!(size, v, SectionKeyword::Size),
381 Section::Groups(v) => set_vec_once!(groups, v, SectionKeyword::Groups),
382 Section::Reason(v) => set_once!(reason, v, SectionKeyword::Reason),
383 Section::License(v) => set_vec_once!(license, v, SectionKeyword::License),
384 Section::Validation(v) => set_once!(validation, v, SectionKeyword::Validation),
385 Section::Replaces(v) => set_vec_once!(replaces, v, SectionKeyword::Replaces),
386 Section::Depends(v) => set_vec_once!(depends, v, SectionKeyword::Depends),
387 Section::OptDepends(v) => set_vec_once!(optdepends, v, SectionKeyword::OptDepends),
388 Section::Conflicts(v) => set_vec_once!(conflicts, v, SectionKeyword::Conflicts),
389 Section::Provides(v) => set_vec_once!(provides, v, SectionKeyword::Provides),
390 Section::XData(_) => {}
391 }
392 }
393
394 Ok(DbDescFileV1 {
395 name: name.ok_or(Error::MissingSection(SectionKeyword::Name))?,
396 version: version.ok_or(Error::MissingSection(SectionKeyword::Version))?,
397 base: base.ok_or(Error::MissingSection(SectionKeyword::Base))?,
398 description: description.ok_or(Error::MissingSection(SectionKeyword::Desc))?,
399 url: url.ok_or(Error::MissingSection(SectionKeyword::Url))?,
400 arch: arch.ok_or(Error::MissingSection(SectionKeyword::Arch))?,
401 builddate: builddate.ok_or(Error::MissingSection(SectionKeyword::BuildDate))?,
402 installdate: installdate.ok_or(Error::MissingSection(SectionKeyword::InstallDate))?,
403 packager: packager.ok_or(Error::MissingSection(SectionKeyword::Packager))?,
404 size: size.unwrap_or_default(),
405 groups,
406 reason: reason.unwrap_or(PackageInstallReason::Explicit),
407 license,
408 validation: validation
409 .filter(|v| !v.is_empty())
410 .ok_or(Error::MissingSection(SectionKeyword::Validation))?,
411 replaces,
412 depends,
413 optdepends,
414 conflicts,
415 provides,
416 })
417 }
418}
419
420impl From<DbDescFileV2> for DbDescFileV1 {
421 fn from(v2: DbDescFileV2) -> Self {
428 DbDescFileV1 {
429 name: v2.name,
430 version: v2.version,
431 base: v2.base,
432 description: v2.description,
433 url: v2.url,
434 arch: v2.arch,
435 builddate: v2.builddate,
436 installdate: v2.installdate,
437 packager: v2.packager,
438 size: v2.size,
439 groups: v2.groups,
440 reason: v2.reason,
441 license: v2.license,
442 validation: v2.validation,
443 replaces: v2.replaces,
444 depends: v2.depends,
445 optdepends: v2.optdepends,
446 conflicts: v2.conflicts,
447 provides: v2.provides,
448 }
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use pretty_assertions::assert_eq;
455 use rstest::*;
456 use testresult::TestResult;
457
458 use super::*;
459
460 const DESC_FULL: &str = r#"%NAME%
462foo
463
464%VERSION%
4651.0.0-1
466
467%BASE%
468foo
469
470%DESC%
471An example package
472
473%URL%
474https://example.org/
475
476%ARCH%
477x86_64
478
479%BUILDDATE%
4801733737242
481
482%INSTALLDATE%
4831733737243
484
485%PACKAGER%
486Foobar McFooface <foobar@mcfooface.org>
487
488%SIZE%
489123
490
491%GROUPS%
492utils
493cli
494
495%REASON%
4961
497
498%LICENSE%
499MIT
500Apache-2.0
501
502%VALIDATION%
503sha256
504pgp
505
506%REPLACES%
507pkg-old
508
509%DEPENDS%
510glibc
511libwlroots-0.19.so=libwlroots-0.19.so-64
512lib:libexample.so.1
513
514%OPTDEPENDS%
515optpkg
516
517%CONFLICTS%
518foo-old
519
520%PROVIDES%
521foo-virtual
522libwlroots-0.19.so=libwlroots-0.19.so-64
523lib:libexample.so.1
524
525"#;
526
527 const DESC_EMPTY_LIST_SECTIONS: &str = r#"%NAME%
529foo
530
531%VERSION%
5321.0.0-1
533
534%BASE%
535foo
536
537%DESC%
538An example package
539
540%URL%
541https://example.org/
542
543%ARCH%
544x86_64
545
546%BUILDDATE%
5471733737242
548
549%INSTALLDATE%
5501733737243
551
552%PACKAGER%
553Foobar McFooface <foobar@mcfooface.org>
554
555%GROUPS%
556
557%LICENSE%
558
559%VALIDATION%
560pgp
561
562%REPLACES%
563
564%DEPENDS%
565
566%OPTDEPENDS%
567
568%CONFLICTS%
569
570%PROVIDES%
571
572"#;
573
574 const DESC_MINIMAL: &str = r#"%NAME%
578foo
579
580%VERSION%
5811.0.0-1
582
583%BASE%
584foo
585
586%DESC%
587An example package
588
589%URL%
590https://example.org/
591
592%ARCH%
593x86_64
594
595%BUILDDATE%
5961733737242
597
598%INSTALLDATE%
5991733737243
600
601%PACKAGER%
602Foobar McFooface <foobar@mcfooface.org>
603
604%VALIDATION%
605pgp
606
607"#;
608
609 const DESC_EMPTY_DESC_AND_URL: &str = r#"%NAME%
613foo
614
615%VERSION%
6161.0.0-1
617
618%BASE%
619foo
620
621%DESC%
622
623
624%URL%
625
626
627%ARCH%
628x86_64
629
630%BUILDDATE%
6311733737242
632
633%INSTALLDATE%
6341733737243
635
636%PACKAGER%
637Foobar McFooface <foobar@mcfooface.org>
638
639%VALIDATION%
640pgp
641
642"#;
643
644 const DESC_REASON_EXPLICITLY_ZERO: &str = r#"%NAME%
648foo
649
650%VERSION%
6511.0.0-1
652
653%BASE%
654foo
655
656%DESC%
657An example package
658
659%URL%
660https://example.org/
661
662%ARCH%
663x86_64
664
665%BUILDDATE%
6661733737242
667
668%INSTALLDATE%
6691733737243
670
671%PACKAGER%
672Foobar McFooface <foobar@mcfooface.org>
673
674%REASON%
6750
676
677%VALIDATION%
678pgp
679
680"#;
681
682 const DESC_SIZE_EXPLICITLY_ZERO: &str = r#"%NAME%
686foo
687
688%VERSION%
6891.0.0-1
690
691%BASE%
692foo
693
694%DESC%
695An example package
696
697%URL%
698https://example.org/
699
700%ARCH%
701x86_64
702
703%BUILDDATE%
7041733737242
705
706%INSTALLDATE%
7071733737243
708
709%PACKAGER%
710Foobar McFooface <foobar@mcfooface.org>
711
712%SIZE%
7130
714
715%VALIDATION%
716pgp
717
718"#;
719
720 #[rstest]
721 #[case::full(
722 DESC_FULL,
723 DbDescFileV1 {
724 name: Name::new("foo")?,
725 version: FullVersion::from_str("1.0.0-1")?,
726 base: PackageBaseName::new("foo")?,
727 description: PackageDescription::from("An example package"),
728 url: Some(Url::from_str("https://example.org/")?),
729 arch: Architecture::from_str("x86_64")?,
730 builddate: BuildDate::from(1733737242),
731 installdate: BuildDate::from(1733737243),
732 packager: Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?,
733 size: 123,
734 groups: vec!["utils".into(), "cli".into()],
735 reason: PackageInstallReason::Depend,
736 license: vec![License::from_str("MIT")?, License::from_str("Apache-2.0")?],
737 validation: vec![
738 PackageValidation::from_str("sha256")?,
739 PackageValidation::from_str("pgp")?,
740 ],
741 replaces: vec![PackageRelation::from_str("pkg-old")?],
742 depends: vec![
743 RelationOrSoname::from_str("glibc")?,
744 RelationOrSoname::from_str("libwlroots-0.19.so=libwlroots-0.19.so-64")?,
745 RelationOrSoname::from_str("lib:libexample.so.1")?,
746 ],
747 optdepends: vec![OptionalDependency::from_str("optpkg")?],
748 conflicts: vec![PackageRelation::from_str("foo-old")?],
749 provides: vec![
750 RelationOrSoname::from_str("foo-virtual")?,
751 RelationOrSoname::from_str("libwlroots-0.19.so=libwlroots-0.19.so-64")?,
752 RelationOrSoname::from_str("lib:libexample.so.1")?,
753 ],
754 },
755 DESC_FULL,
756 )]
757 #[case::empty_list_sections(
758 DESC_EMPTY_LIST_SECTIONS,
759 DbDescFileV1 {
760 name: Name::new("foo")?,
761 version: FullVersion::from_str("1.0.0-1")?,
762 base: PackageBaseName::new("foo")?,
763 description: PackageDescription::from("An example package"),
764 url: Some(Url::from_str("https://example.org/")?),
765 arch: Architecture::from_str("x86_64")?,
766 builddate: BuildDate::from(1733737242),
767 installdate: BuildDate::from(1733737243),
768 packager: Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?,
769 size: 0,
770 groups: Vec::new(),
771 reason: PackageInstallReason::Explicit,
772 license: Vec::new(),
773 validation: vec![PackageValidation::from_str("pgp")?],
774 replaces: Vec::new(),
775 depends: Vec::new(),
776 optdepends: Vec::new(),
777 conflicts: Vec::new(),
778 provides: Vec::new(),
779 },
780 DESC_MINIMAL,
781 )]
782 #[case::minimal(
783 DESC_MINIMAL,
784 DbDescFileV1 {
785 name: Name::new("foo")?,
786 version: FullVersion::from_str("1.0.0-1")?,
787 base: PackageBaseName::new("foo")?,
788 description: PackageDescription::from("An example package"),
789 url: Some(Url::from_str("https://example.org/")?),
790 arch: Architecture::from_str("x86_64")?,
791 builddate: BuildDate::from(1733737242),
792 installdate: BuildDate::from(1733737243),
793 packager: Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?,
794 size: 0,
795 groups: Vec::new(),
796 reason: PackageInstallReason::Explicit,
797 license: Vec::new(),
798 validation: vec![PackageValidation::from_str("pgp")?],
799 replaces: Vec::new(),
800 depends: Vec::new(),
801 optdepends: Vec::new(),
802 conflicts: Vec::new(),
803 provides: Vec::new(),
804 },
805 DESC_MINIMAL,
806 )]
807 #[case::empty_desc_and_url(
808 DESC_EMPTY_DESC_AND_URL,
809 DbDescFileV1 {
810 name: Name::new("foo")?,
811 version: FullVersion::from_str("1.0.0-1")?,
812 base: PackageBaseName::new("foo")?,
813 description: PackageDescription::from(""),
814 url: None,
815 arch: Architecture::from_str("x86_64")?,
816 builddate: BuildDate::from(1733737242),
817 installdate: BuildDate::from(1733737243),
818 packager: Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?,
819 size: 0,
820 groups: Vec::new(),
821 reason: PackageInstallReason::Explicit,
822 license: Vec::new(),
823 validation: vec![PackageValidation::from_str("pgp")?],
824 replaces: Vec::new(),
825 depends: Vec::new(),
826 optdepends: Vec::new(),
827 conflicts: Vec::new(),
828 provides: Vec::new(),
829 },
830 DESC_EMPTY_DESC_AND_URL,
831 )]
832 #[case::reason_explicitly_zero(
833 DESC_REASON_EXPLICITLY_ZERO,
834 DbDescFileV1 {
835 name: Name::new("foo")?,
836 version: FullVersion::from_str("1.0.0-1")?,
837 base: PackageBaseName::new("foo")?,
838 description: PackageDescription::from("An example package"),
839 url: Some(Url::from_str("https://example.org/")?),
840 arch: Architecture::from_str("x86_64")?,
841 builddate: BuildDate::from(1733737242),
842 installdate: BuildDate::from(1733737243),
843 packager: Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?,
844 size: 0,
845 groups: Vec::new(),
846 reason: PackageInstallReason::Explicit,
847 license: Vec::new(),
848 validation: vec![PackageValidation::from_str("pgp")?],
849 replaces: Vec::new(),
850 depends: Vec::new(),
851 optdepends: Vec::new(),
852 conflicts: Vec::new(),
853 provides: Vec::new(),
854 },
855 DESC_MINIMAL,
856 )]
857 #[case::size_explicitly_zero(
858 DESC_SIZE_EXPLICITLY_ZERO,
859 DbDescFileV1 {
860 name: Name::new("foo")?,
861 version: FullVersion::from_str("1.0.0-1")?,
862 base: PackageBaseName::new("foo")?,
863 description: PackageDescription::from("An example package"),
864 url: Some(Url::from_str("https://example.org/")?),
865 arch: Architecture::from_str("x86_64")?,
866 builddate: BuildDate::from(1733737242),
867 installdate: BuildDate::from(1733737243),
868 packager: Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?,
869 size: 0,
870 groups: Vec::new(),
871 reason: PackageInstallReason::Explicit,
872 license: Vec::new(),
873 validation: vec![PackageValidation::from_str("pgp")?],
874 replaces: Vec::new(),
875 depends: Vec::new(),
876 optdepends: Vec::new(),
877 conflicts: Vec::new(),
878 provides: Vec::new(),
879 },
880 DESC_MINIMAL,
881 )]
882 fn parse_valid_v1_desc(
883 #[case] input_data: &str,
884 #[case] expected: DbDescFileV1,
885 #[case] expected_output_data: &str,
886 ) -> TestResult {
887 let desc = DbDescFileV1::from_str(input_data)?;
888 assert_eq!(desc, expected);
889 assert_eq!(expected_output_data, desc.to_string());
890 Ok(())
891 }
892
893 #[test]
894 fn depends_and_provides_accept_sonames() -> TestResult {
895 let desc = DbDescFileV1::from_str(DESC_FULL)?;
896 assert!(matches!(desc.depends[1], RelationOrSoname::SonameV1(_)));
897 assert!(matches!(desc.depends[2], RelationOrSoname::SonameV2(_)));
898 assert!(matches!(desc.provides[1], RelationOrSoname::SonameV1(_)));
899 assert!(matches!(desc.provides[2], RelationOrSoname::SonameV2(_)));
900 Ok(())
901 }
902
903 #[rstest]
904 #[case("%UNKNOWN%\nvalue", "invalid section name")]
905 #[case("%VERSION%\n1.0.0-1\n", "Missing section: %NAME%")]
906 fn invalid_desc_parser(#[case] input: &str, #[case] error_snippet: &str) {
907 let result = DbDescFileV1::from_str(input);
908 assert!(result.is_err());
909 let err = result.unwrap_err();
910 let pretty_error = err.to_string();
911 assert!(
912 pretty_error.contains(error_snippet),
913 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
914 );
915 }
916
917 #[test]
918 fn missing_required_section_should_fail() {
919 let input = "%VERSION%\n1.0.0-1\n";
920 let result = DbDescFileV1::from_str(input);
921 assert!(matches!(result, Err(Error::MissingSection(s)) if s == SectionKeyword::Name));
922 }
923}