1use std::{
6 fmt::{Display, Formatter, Result as FmtResult, Write},
7 str::FromStr,
8};
9
10use alpm_types::{
11 Architecture,
12 BuildDate,
13 Group,
14 InstalledSize,
15 License,
16 Name,
17 OptionalDependency,
18 PackageBaseName,
19 PackageDescription,
20 PackageInstallReason,
21 PackageRelation,
22 PackageValidation,
23 Packager,
24 Url,
25 Version,
26};
27use winnow::Parser;
28
29use crate::{
30 Error,
31 desc::{
32 DbDescFileV2,
33 Section,
34 parser::{SectionKeyword, sections},
35 },
36};
37
38#[derive(Clone, Debug, serde::Deserialize, PartialEq, serde::Serialize)]
125#[serde(deny_unknown_fields)]
126#[serde(rename_all = "lowercase")]
127pub struct DbDescFileV1 {
128 pub name: Name,
130
131 pub version: Version,
133
134 pub base: PackageBaseName,
136
137 pub description: PackageDescription,
139
140 pub url: Option<Url>,
142
143 pub arch: Architecture,
145
146 pub builddate: BuildDate,
148
149 pub installdate: BuildDate,
151
152 pub packager: Packager,
154
155 pub size: InstalledSize,
157
158 pub groups: Vec<Group>,
160
161 pub reason: PackageInstallReason,
163
164 pub license: Vec<License>,
166
167 pub validation: PackageValidation,
169
170 pub replaces: Vec<PackageRelation>,
172
173 pub depends: Vec<PackageRelation>,
175
176 pub optdepends: Vec<OptionalDependency>,
178
179 pub conflicts: Vec<PackageRelation>,
181
182 pub provides: Vec<PackageRelation>,
184}
185
186impl Display for DbDescFileV1 {
187 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
188 fn single<T: Display, W: Write>(f: &mut W, key: &str, val: &T) -> FmtResult {
190 writeln!(f, "%{key}%\n{val}\n")
191 }
192
193 fn section<T: Display, W: Write>(f: &mut W, key: &str, vals: &[T]) -> FmtResult {
195 if vals.is_empty() {
196 return Ok(());
197 }
198 writeln!(f, "%{key}%")?;
199 for v in vals {
200 writeln!(f, "{v}")?;
201 }
202 writeln!(f)
203 }
204
205 single(f, "NAME", &self.name)?;
206 single(f, "VERSION", &self.version)?;
207 single(f, "BASE", &self.base)?;
208 single(f, "DESC", &self.description)?;
209 single(
211 f,
212 "URL",
213 &self
214 .url
215 .as_ref()
216 .map_or(String::new(), |url| url.to_string()),
217 )?;
218 single(f, "ARCH", &self.arch)?;
219 single(f, "BUILDDATE", &self.builddate)?;
220 single(f, "INSTALLDATE", &self.installdate)?;
221 single(f, "PACKAGER", &self.packager)?;
222 if self.size != 0 {
224 single(f, "SIZE", &self.size)?;
225 }
226 section(f, "GROUPS", &self.groups)?;
227 if self.reason != PackageInstallReason::Explicit {
229 single(f, "REASON", &self.reason)?;
230 }
231 section(f, "LICENSE", &self.license)?;
232 single(f, "VALIDATION", &self.validation)?;
233 section(f, "REPLACES", &self.replaces)?;
234 section(f, "DEPENDS", &self.depends)?;
235 section(f, "OPTDEPENDS", &self.optdepends)?;
236 section(f, "CONFLICTS", &self.conflicts)?;
237 section(f, "PROVIDES", &self.provides)?;
238
239 Ok(())
240 }
241}
242
243impl FromStr for DbDescFileV1 {
244 type Err = Error;
245
246 fn from_str(s: &str) -> Result<Self, Self::Err> {
309 let sections = sections.parse(s)?;
310 Self::try_from(sections)
311 }
312}
313
314impl TryFrom<Vec<Section>> for DbDescFileV1 {
315 type Error = Error;
316
317 fn try_from(sections: Vec<Section>) -> Result<Self, Self::Error> {
327 let mut name = None;
328 let mut version = None;
329 let mut base = None;
330 let mut description = None;
331 let mut url = None;
332 let mut arch = None;
333 let mut builddate = None;
334 let mut installdate = None;
335 let mut packager = None;
336 let mut size = None;
337
338 let mut groups: Vec<Group> = Vec::new();
339 let mut reason = None;
340 let mut license: Vec<License> = Vec::new();
341 let mut validation = None;
342 let mut replaces: Vec<PackageRelation> = Vec::new();
343 let mut depends: Vec<PackageRelation> = Vec::new();
344 let mut optdepends: Vec<OptionalDependency> = Vec::new();
345 let mut conflicts: Vec<PackageRelation> = Vec::new();
346 let mut provides: Vec<PackageRelation> = Vec::new();
347
348 macro_rules! set_once {
350 ($field:ident, $val:expr, $kw:expr) => {{
351 if $field.is_some() {
352 return Err(Error::DuplicateSection($kw));
353 }
354 $field = Some($val);
355 }};
356 }
357
358 macro_rules! set_vec_once {
360 ($field:ident, $val:expr, $kw:expr) => {{
361 if !$field.is_empty() {
362 return Err(Error::DuplicateSection($kw));
363 }
364 $field = $val;
365 }};
366 }
367
368 for section in sections {
369 match section {
370 Section::Name(v) => set_once!(name, v, SectionKeyword::Name),
371 Section::Version(v) => set_once!(version, v, SectionKeyword::Version),
372 Section::Base(v) => set_once!(base, v, SectionKeyword::Base),
373 Section::Desc(v) => set_once!(description, v, SectionKeyword::Desc),
374 Section::Url(v) => set_once!(url, v, SectionKeyword::Url),
375 Section::Arch(v) => set_once!(arch, v, SectionKeyword::Arch),
376 Section::BuildDate(v) => set_once!(builddate, v, SectionKeyword::BuildDate),
377 Section::InstallDate(v) => set_once!(installdate, v, SectionKeyword::InstallDate),
378 Section::Packager(v) => set_once!(packager, v, SectionKeyword::Packager),
379 Section::Size(v) => set_once!(size, v, SectionKeyword::Size),
380 Section::Groups(v) => set_vec_once!(groups, v, SectionKeyword::Groups),
381 Section::Reason(v) => set_once!(reason, v, SectionKeyword::Reason),
382 Section::License(v) => set_vec_once!(license, v, SectionKeyword::License),
383 Section::Validation(v) => set_once!(validation, v, SectionKeyword::Validation),
384 Section::Replaces(v) => set_vec_once!(replaces, v, SectionKeyword::Replaces),
385 Section::Depends(v) => set_vec_once!(depends, v, SectionKeyword::Depends),
386 Section::OptDepends(v) => set_vec_once!(optdepends, v, SectionKeyword::OptDepends),
387 Section::Conflicts(v) => set_vec_once!(conflicts, v, SectionKeyword::Conflicts),
388 Section::Provides(v) => set_vec_once!(provides, v, SectionKeyword::Provides),
389 Section::XData(_) => {}
390 }
391 }
392
393 Ok(DbDescFileV1 {
394 name: name.ok_or(Error::MissingSection(SectionKeyword::Name))?,
395 version: version.ok_or(Error::MissingSection(SectionKeyword::Version))?,
396 base: base.ok_or(Error::MissingSection(SectionKeyword::Base))?,
397 description: description.ok_or(Error::MissingSection(SectionKeyword::Desc))?,
398 url: url.ok_or(Error::MissingSection(SectionKeyword::Url))?,
399 arch: arch.ok_or(Error::MissingSection(SectionKeyword::Arch))?,
400 builddate: builddate.ok_or(Error::MissingSection(SectionKeyword::BuildDate))?,
401 installdate: installdate.ok_or(Error::MissingSection(SectionKeyword::InstallDate))?,
402 packager: packager.ok_or(Error::MissingSection(SectionKeyword::Packager))?,
403 size: size.unwrap_or_default(),
404 groups,
405 reason: reason.unwrap_or(PackageInstallReason::Explicit),
406 license,
407 validation: validation.ok_or(Error::MissingSection(SectionKeyword::Validation))?,
408 replaces,
409 depends,
410 optdepends,
411 conflicts,
412 provides,
413 })
414 }
415}
416
417impl From<DbDescFileV2> for DbDescFileV1 {
418 fn from(v2: DbDescFileV2) -> Self {
425 DbDescFileV1 {
426 name: v2.name,
427 version: v2.version,
428 base: v2.base,
429 description: v2.description,
430 url: v2.url,
431 arch: v2.arch,
432 builddate: v2.builddate,
433 installdate: v2.installdate,
434 packager: v2.packager,
435 size: v2.size,
436 groups: v2.groups,
437 reason: v2.reason,
438 license: v2.license,
439 validation: v2.validation,
440 replaces: v2.replaces,
441 depends: v2.depends,
442 optdepends: v2.optdepends,
443 conflicts: v2.conflicts,
444 provides: v2.provides,
445 }
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use pretty_assertions::assert_eq;
452 use rstest::*;
453 use testresult::TestResult;
454
455 use super::*;
456
457 const DESC_FULL: &str = r#"%NAME%
459foo
460
461%VERSION%
4621.0.0-1
463
464%BASE%
465foo
466
467%DESC%
468An example package
469
470%URL%
471https://example.org/
472
473%ARCH%
474x86_64
475
476%BUILDDATE%
4771733737242
478
479%INSTALLDATE%
4801733737243
481
482%PACKAGER%
483Foobar McFooface <foobar@mcfooface.org>
484
485%SIZE%
486123
487
488%GROUPS%
489utils
490cli
491
492%REASON%
4931
494
495%LICENSE%
496MIT
497Apache-2.0
498
499%VALIDATION%
500pgp
501
502%REPLACES%
503pkg-old
504
505%DEPENDS%
506glibc
507
508%OPTDEPENDS%
509optpkg
510
511%CONFLICTS%
512foo-old
513
514%PROVIDES%
515foo-virtual
516
517"#;
518
519 const DESC_EMPTY_LIST_SECTIONS: &str = r#"%NAME%
521foo
522
523%VERSION%
5241.0.0-1
525
526%BASE%
527foo
528
529%DESC%
530An example package
531
532%URL%
533https://example.org/
534
535%ARCH%
536x86_64
537
538%BUILDDATE%
5391733737242
540
541%INSTALLDATE%
5421733737243
543
544%PACKAGER%
545Foobar McFooface <foobar@mcfooface.org>
546
547%GROUPS%
548
549%LICENSE%
550
551%VALIDATION%
552pgp
553
554%REPLACES%
555
556%DEPENDS%
557
558%OPTDEPENDS%
559
560%CONFLICTS%
561
562%PROVIDES%
563
564"#;
565
566 const DESC_MINIMAL: &str = r#"%NAME%
570foo
571
572%VERSION%
5731.0.0-1
574
575%BASE%
576foo
577
578%DESC%
579An example package
580
581%URL%
582https://example.org/
583
584%ARCH%
585x86_64
586
587%BUILDDATE%
5881733737242
589
590%INSTALLDATE%
5911733737243
592
593%PACKAGER%
594Foobar McFooface <foobar@mcfooface.org>
595
596%VALIDATION%
597pgp
598
599"#;
600
601 const DESC_EMPTY_DESC_AND_URL: &str = r#"%NAME%
605foo
606
607%VERSION%
6081.0.0-1
609
610%BASE%
611foo
612
613%DESC%
614
615
616%URL%
617
618
619%ARCH%
620x86_64
621
622%BUILDDATE%
6231733737242
624
625%INSTALLDATE%
6261733737243
627
628%PACKAGER%
629Foobar McFooface <foobar@mcfooface.org>
630
631%VALIDATION%
632pgp
633
634"#;
635
636 const DESC_REASON_EXPLICITLY_ZERO: &str = r#"%NAME%
640foo
641
642%VERSION%
6431.0.0-1
644
645%BASE%
646foo
647
648%DESC%
649An example package
650
651%URL%
652https://example.org/
653
654%ARCH%
655x86_64
656
657%BUILDDATE%
6581733737242
659
660%INSTALLDATE%
6611733737243
662
663%PACKAGER%
664Foobar McFooface <foobar@mcfooface.org>
665
666%REASON%
6670
668
669%VALIDATION%
670pgp
671
672"#;
673
674 const DESC_SIZE_EXPLICITLY_ZERO: &str = r#"%NAME%
678foo
679
680%VERSION%
6811.0.0-1
682
683%BASE%
684foo
685
686%DESC%
687An example package
688
689%URL%
690https://example.org/
691
692%ARCH%
693x86_64
694
695%BUILDDATE%
6961733737242
697
698%INSTALLDATE%
6991733737243
700
701%PACKAGER%
702Foobar McFooface <foobar@mcfooface.org>
703
704%SIZE%
7050
706
707%VALIDATION%
708pgp
709
710"#;
711
712 #[rstest]
713 #[case::full(
714 DESC_FULL,
715 DbDescFileV1 {
716 name: Name::new("foo")?,
717 version: Version::from_str("1.0.0-1")?,
718 base: PackageBaseName::new("foo")?,
719 description: PackageDescription::from("An example package"),
720 url: Some(Url::from_str("https://example.org/")?),
721 arch: Architecture::from_str("x86_64")?,
722 builddate: BuildDate::from(1733737242),
723 installdate: BuildDate::from(1733737243),
724 packager: Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?,
725 size: 123,
726 groups: vec!["utils".into(), "cli".into()],
727 reason: PackageInstallReason::Depend,
728 license: vec![License::from_str("MIT")?, License::from_str("Apache-2.0")?],
729 validation: PackageValidation::from_str("pgp")?,
730 replaces: vec![PackageRelation::from_str("pkg-old")?],
731 depends: vec![PackageRelation::from_str("glibc")?],
732 optdepends: vec![OptionalDependency::from_str("optpkg")?],
733 conflicts: vec![PackageRelation::from_str("foo-old")?],
734 provides: vec![PackageRelation::from_str("foo-virtual")?],
735 },
736 DESC_FULL,
737 )]
738 #[case::empty_list_sections(
739 DESC_EMPTY_LIST_SECTIONS,
740 DbDescFileV1 {
741 name: Name::new("foo")?,
742 version: Version::from_str("1.0.0-1")?,
743 base: PackageBaseName::new("foo")?,
744 description: PackageDescription::from("An example package"),
745 url: Some(Url::from_str("https://example.org/")?),
746 arch: Architecture::from_str("x86_64")?,
747 builddate: BuildDate::from(1733737242),
748 installdate: BuildDate::from(1733737243),
749 packager: Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?,
750 size: 0,
751 groups: Vec::new(),
752 reason: PackageInstallReason::Explicit,
753 license: Vec::new(),
754 validation: PackageValidation::from_str("pgp")?,
755 replaces: Vec::new(),
756 depends: Vec::new(),
757 optdepends: Vec::new(),
758 conflicts: Vec::new(),
759 provides: Vec::new(),
760 },
761 DESC_MINIMAL,
762 )]
763 #[case::minimal(
764 DESC_MINIMAL,
765 DbDescFileV1 {
766 name: Name::new("foo")?,
767 version: Version::from_str("1.0.0-1")?,
768 base: PackageBaseName::new("foo")?,
769 description: PackageDescription::from("An example package"),
770 url: Some(Url::from_str("https://example.org/")?),
771 arch: Architecture::from_str("x86_64")?,
772 builddate: BuildDate::from(1733737242),
773 installdate: BuildDate::from(1733737243),
774 packager: Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?,
775 size: 0,
776 groups: Vec::new(),
777 reason: PackageInstallReason::Explicit,
778 license: Vec::new(),
779 validation: PackageValidation::from_str("pgp")?,
780 replaces: Vec::new(),
781 depends: Vec::new(),
782 optdepends: Vec::new(),
783 conflicts: Vec::new(),
784 provides: Vec::new(),
785 },
786 DESC_MINIMAL,
787 )]
788 #[case::empty_desc_and_url(
789 DESC_EMPTY_DESC_AND_URL,
790 DbDescFileV1 {
791 name: Name::new("foo")?,
792 version: Version::from_str("1.0.0-1")?,
793 base: PackageBaseName::new("foo")?,
794 description: PackageDescription::from(""),
795 url: None,
796 arch: Architecture::from_str("x86_64")?,
797 builddate: BuildDate::from(1733737242),
798 installdate: BuildDate::from(1733737243),
799 packager: Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?,
800 size: 0,
801 groups: Vec::new(),
802 reason: PackageInstallReason::Explicit,
803 license: Vec::new(),
804 validation: PackageValidation::from_str("pgp")?,
805 replaces: Vec::new(),
806 depends: Vec::new(),
807 optdepends: Vec::new(),
808 conflicts: Vec::new(),
809 provides: Vec::new(),
810 },
811 DESC_EMPTY_DESC_AND_URL,
812 )]
813 #[case::reason_explicitly_zero(
814 DESC_REASON_EXPLICITLY_ZERO,
815 DbDescFileV1 {
816 name: Name::new("foo")?,
817 version: Version::from_str("1.0.0-1")?,
818 base: PackageBaseName::new("foo")?,
819 description: PackageDescription::from("An example package"),
820 url: Some(Url::from_str("https://example.org/")?),
821 arch: Architecture::from_str("x86_64")?,
822 builddate: BuildDate::from(1733737242),
823 installdate: BuildDate::from(1733737243),
824 packager: Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?,
825 size: 0,
826 groups: Vec::new(),
827 reason: PackageInstallReason::Explicit,
828 license: Vec::new(),
829 validation: PackageValidation::from_str("pgp")?,
830 replaces: Vec::new(),
831 depends: Vec::new(),
832 optdepends: Vec::new(),
833 conflicts: Vec::new(),
834 provides: Vec::new(),
835 },
836 DESC_MINIMAL,
837 )]
838 #[case::size_explicitly_zero(
839 DESC_SIZE_EXPLICITLY_ZERO,
840 DbDescFileV1 {
841 name: Name::new("foo")?,
842 version: Version::from_str("1.0.0-1")?,
843 base: PackageBaseName::new("foo")?,
844 description: PackageDescription::from("An example package"),
845 url: Some(Url::from_str("https://example.org/")?),
846 arch: Architecture::from_str("x86_64")?,
847 builddate: BuildDate::from(1733737242),
848 installdate: BuildDate::from(1733737243),
849 packager: Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?,
850 size: 0,
851 groups: Vec::new(),
852 reason: PackageInstallReason::Explicit,
853 license: Vec::new(),
854 validation: PackageValidation::from_str("pgp")?,
855 replaces: Vec::new(),
856 depends: Vec::new(),
857 optdepends: Vec::new(),
858 conflicts: Vec::new(),
859 provides: Vec::new(),
860 },
861 DESC_MINIMAL,
862 )]
863 fn parse_valid_v1_desc(
864 #[case] input_data: &str,
865 #[case] expected: DbDescFileV1,
866 #[case] expected_output_data: &str,
867 ) -> TestResult {
868 let desc = DbDescFileV1::from_str(input_data)?;
869 assert_eq!(desc, expected);
870 assert_eq!(expected_output_data, desc.to_string());
871 Ok(())
872 }
873
874 #[rstest]
875 #[case("%UNKNOWN%\nvalue", "invalid section name")]
876 #[case("%VERSION%\n1.0.0-1\n", "Missing section: %NAME%")]
877 fn invalid_desc_parser(#[case] input: &str, #[case] error_snippet: &str) {
878 let result = DbDescFileV1::from_str(input);
879 assert!(result.is_err());
880 let err = result.unwrap_err();
881 let pretty_error = err.to_string();
882 assert!(
883 pretty_error.contains(error_snippet),
884 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
885 );
886 }
887
888 #[test]
889 fn missing_required_section_should_fail() {
890 let input = "%VERSION%\n1.0.0-1\n";
891 let result = DbDescFileV1::from_str(input);
892 assert!(matches!(result, Err(Error::MissingSection(s)) if s == SectionKeyword::Name));
893 }
894}