alpm_db/desc/
v1.rs

1//! Representation of the database desc file v1 ([alpm-db-descv1]).
2//!
3//! [alpm-db-descv1]: https://alpm.archlinux.page/specifications/alpm-db-descv1.5.html
4
5use 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/// DB DESC version 1
40///
41/// `DbDescFileV1` represents the [alpm-db-descv1] specification which is the
42/// canonical format of a single package entry within an ALPM database.
43///
44/// It includes information such as the package's name, version, architecture,
45/// and dependency relationships.
46///
47/// ## Examples
48///
49/// ```
50/// use std::str::FromStr;
51///
52/// use alpm_db::desc::DbDescFileV1;
53///
54/// # fn main() -> Result<(), alpm_db::Error> {
55/// let desc_data = r#"%NAME%
56/// foo
57///
58/// %VERSION%
59/// 1.0.0-1
60///
61/// %BASE%
62/// foo
63///
64/// %DESC%
65/// An example package
66///
67/// %URL%
68/// https://example.org/
69///
70/// %ARCH%
71/// x86_64
72///
73/// %BUILDDATE%
74/// 1733737242
75///
76/// %INSTALLDATE%
77/// 1733737243
78///
79/// %PACKAGER%
80/// Foobar McFooface <foobar@mcfooface.org>
81///
82/// %SIZE%
83/// 123
84///
85/// %GROUPS%
86/// utils
87/// cli
88///
89/// %REASON%
90/// 1
91///
92/// %LICENSE%
93/// MIT
94/// Apache-2.0
95///
96/// %VALIDATION%
97/// pgp
98///
99/// %REPLACES%
100/// pkg-old
101///
102/// %DEPENDS%
103/// glibc
104///
105/// %OPTDEPENDS%
106/// optpkg
107///
108/// %CONFLICTS%
109/// foo-old
110///
111/// %PROVIDES%
112/// foo-virtual
113///
114/// "#;
115///
116/// // Parse a DB DESC file in version 1 format.
117/// let db_desc = DbDescFileV1::from_str(desc_data)?;
118/// // Convert back to its canonical string representation.
119/// assert_eq!(db_desc.to_string(), desc_data);
120/// # Ok(())
121/// # }
122/// ```
123///
124/// [alpm-db-descv1]: https://alpm.archlinux.page/specifications/alpm-db-descv1.5.html
125#[derive(Clone, Debug, serde::Deserialize, PartialEq, serde::Serialize)]
126#[serde(deny_unknown_fields)]
127#[serde(rename_all = "lowercase")]
128pub struct DbDescFileV1 {
129    /// The name of the package.
130    pub name: Name,
131
132    /// The version of the package.
133    pub version: FullVersion,
134
135    /// The base name of the package (used in split packages).
136    pub base: PackageBaseName,
137
138    /// The description of the package.
139    pub description: PackageDescription,
140
141    /// The URL for the project of the package.
142    pub url: Option<Url>,
143
144    /// The architecture of the package.
145    pub arch: Architecture,
146
147    /// The date at which the build of the package started.
148    pub builddate: BuildDate,
149
150    /// The date at which the package has been installed on the system.
151    pub installdate: BuildDate,
152
153    /// The User ID of the entity, that built the package.
154    pub packager: Packager,
155
156    /// The optional size of the (uncompressed and unpacked) package contents in bytes.
157    pub size: InstalledSize,
158
159    /// Groups the package belongs to.
160    pub groups: Vec<Group>,
161
162    /// The reason for installing the package.
163    pub reason: PackageInstallReason,
164
165    /// Licenses that apply to the package.
166    pub license: Vec<License>,
167
168    /// Validation methods used for the package.
169    pub validation: Vec<PackageValidation>,
170
171    /// Packages this one replaces.
172    pub replaces: Vec<PackageRelation>,
173
174    /// Required runtime dependencies.
175    pub depends: Vec<RelationOrSoname>,
176
177    /// Optional dependencies that enhance the package.
178    pub optdepends: Vec<OptionalDependency>,
179
180    /// Conflicting packages that cannot be installed together.
181    pub conflicts: Vec<PackageRelation>,
182
183    /// Virtual packages or capabilities provided by this one.
184    pub provides: Vec<RelationOrSoname>,
185}
186
187impl Display for DbDescFileV1 {
188    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
189        // Helper function to write a single value section
190        fn single<T: Display, W: Write>(f: &mut W, key: &str, val: &T) -> FmtResult {
191            writeln!(f, "%{key}%\n{val}\n")
192        }
193
194        // Helper function to write a multi-value section
195        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        // Write an empty string if there is no URL value.
211        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        // Omit %SIZE% section if its value is "0"
224        if self.size != 0 {
225            single(f, "SIZE", &self.size)?;
226        }
227        section(f, "GROUPS", &self.groups)?;
228        // Omit %REASON% section if its value is "PackageInstallReason::Explicit"
229        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    /// Creates a [`DbDescFileV1`] from a string slice.
248    ///
249    /// Parses the input according to the [alpm-db-descv1] specification and constructs a
250    /// structured [`DbDescFileV1`] representation.
251    ///
252    /// # Examples
253    ///
254    /// ```
255    /// use std::str::FromStr;
256    ///
257    /// use alpm_db::desc::DbDescFileV1;
258    ///
259    /// # fn main() -> Result<(), alpm_db::Error> {
260    /// let desc_data = r#"%NAME%
261    /// foo
262    ///
263    /// %VERSION%
264    /// 1.0.0-1
265    ///
266    /// %BASE%
267    /// foo
268    ///
269    /// %DESC%
270    /// An example package
271    ///
272    /// %URL%
273    /// https://example.org
274    ///
275    /// %ARCH%
276    /// x86_64
277    ///
278    /// %BUILDDATE%
279    /// 1733737242
280    ///
281    /// %INSTALLDATE%
282    /// 1733737243
283    ///
284    /// %PACKAGER%
285    /// Foobar McFooface <foobar@mcfooface.org>
286    ///
287    /// %SIZE%
288    /// 123
289    ///
290    /// %VALIDATION%
291    /// pgp
292    ///
293    /// "#;
294    ///
295    /// let db_desc = DbDescFileV1::from_str(desc_data)?;
296    /// assert_eq!(db_desc.name.to_string(), "foo");
297    /// # Ok(())
298    /// # }
299    /// ```
300    ///
301    /// # Errors
302    ///
303    /// Returns an error if:
304    ///
305    /// - the input cannot be parsed into valid sections,
306    /// - or required fields are missing or malformed.
307    ///
308    /// [alpm-db-descv1]: https://alpm.archlinux.page/specifications/alpm-db-descv1.5.html
309    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    /// Tries to create a [`DbDescFileV1`] from a list of parsed [`Section`]s.
319    ///
320    /// # Errors
321    ///
322    /// Returns an error if:
323    ///
324    /// - any required field is missing,
325    /// - a section appears more than once,
326    /// - or a section violates the expected format for version 1.
327    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        /// Helper macro to set a field only once, returning an error if it was already set.
350        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        /// Helper macro to set a vector field only once, returning an error if it was already set.
360        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    /// Converts a [`DbDescFileV2`] into a [`DbDescFileV1`].
422    ///
423    /// # Note
424    ///
425    /// This drops the `xdata` field of the [`DbDescFileV2`], which provides additional information
426    /// about a package.
427    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    /// An alpm-db-desc string with all sections explicitly populated.
461    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    /// An alpm-db-desc string with all list sections set, but empty.
528    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    /// An alpm-db-desc string with the minimum set of sections.
575    ///
576    /// All list sections and sections that can be omitted, are omitted.
577    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    /// A minimal alpm-db-desc string with empty `%DESC%` and `%URL%` sections.
610    ///
611    /// All list sections and sections that can be omitted, are omitted for brevity.
612    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    /// A minimal alpm-db-desc string with the `%REASON%` section explicitly set to "0".
645    ///
646    /// All list sections and sections that can be omitted, are omitted for brevity.
647    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    /// A minimal alpm-db-desc string with the `%SIZE%` section explicitly set to "0".
683    ///
684    /// All list sections and sections that can be omitted, are omitted for brevity.
685    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}