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