alpm_db/desc/
v2.rs

1//! Representation of the database desc file v2 ([alpm-db-descv2]).
2//!
3//! [alpm-db-descv2]: https://alpm.archlinux.page/specifications/alpm-db-descv2.5.html
4
5use std::{
6    fmt::{Display, Formatter, Result as FmtResult},
7    str::FromStr,
8};
9
10use alpm_types::{
11    Architecture,
12    BuildDate,
13    ExtraData,
14    ExtraDataEntry,
15    FullVersion,
16    Group,
17    InstalledSize,
18    License,
19    Name,
20    OptionalDependency,
21    PackageBaseName,
22    PackageDescription,
23    PackageInstallReason,
24    PackageRelation,
25    PackageValidation,
26    Packager,
27    RelationOrSoname,
28    Url,
29};
30use serde_with::{TryFromInto, serde_as};
31use winnow::Parser;
32
33use crate::{
34    Error,
35    desc::{
36        DbDescFileV1,
37        Section,
38        parser::{SectionKeyword, sections},
39    },
40};
41
42/// DB desc version 2
43///
44/// `DbDescFileV2` extends [`DbDescFileV1`] according to the second revision of the
45/// [alpm-db-desc] specification. It introduces an additional `%XDATA%` section, which allows
46/// storing structured, implementation-defined metadata.
47///
48/// ## Examples
49///
50/// ```
51/// use std::str::FromStr;
52///
53/// use alpm_db::desc::DbDescFileV2;
54///
55/// # fn main() -> Result<(), alpm_db::Error> {
56/// let desc_data = r#"%NAME%
57/// foo
58///
59/// %VERSION%
60/// 1.0.0-1
61///
62/// %BASE%
63/// foo
64///
65/// %DESC%
66/// An example package
67///
68/// %URL%
69/// https://example.org/
70///
71/// %ARCH%
72/// x86_64
73///
74/// %BUILDDATE%
75/// 1733737242
76///
77/// %INSTALLDATE%
78/// 1733737243
79///
80/// %PACKAGER%
81/// Foobar McFooface <foobar@mcfooface.org>
82///
83/// %SIZE%
84/// 123
85///
86/// %GROUPS%
87/// utils
88/// cli
89///
90/// %REASON%
91/// 1
92///
93/// %LICENSE%
94/// MIT
95/// Apache-2.0
96///
97/// %VALIDATION%
98/// pgp
99///
100/// %REPLACES%
101/// pkg-old
102///
103/// %DEPENDS%
104/// glibc
105///
106/// %OPTDEPENDS%
107/// optpkg
108///
109/// %CONFLICTS%
110/// foo-old
111///
112/// %PROVIDES%
113/// foo-virtual
114///
115/// %XDATA%
116/// pkgtype=pkg
117///
118/// "#;
119///
120/// // Parse a DB DESC file in version 2 format.
121/// let db_desc = DbDescFileV2::from_str(desc_data)?;
122/// // Convert back to its canonical string representation.
123/// assert_eq!(db_desc.to_string(), desc_data);
124/// # Ok(())
125/// # }
126/// ```
127///
128/// [alpm-db-desc]: https://alpm.archlinux.page/specifications/alpm-db-desc.5.html
129#[serde_as]
130#[derive(Clone, Debug, serde::Deserialize, PartialEq, serde::Serialize)]
131#[serde(deny_unknown_fields)]
132#[serde(rename_all = "lowercase")]
133pub struct DbDescFileV2 {
134    /// The name of the package.
135    pub name: Name,
136
137    /// The version of the package.
138    pub version: FullVersion,
139
140    /// The base name of the package (used in split packages).
141    pub base: PackageBaseName,
142
143    /// The description of the package.
144    pub description: PackageDescription,
145
146    /// The URL for the project of the package.
147    pub url: Option<Url>,
148
149    /// The architecture of the package.
150    pub arch: Architecture,
151
152    /// The date at which the build of the package started.
153    pub builddate: BuildDate,
154
155    /// The date at which the package has been installed on the system.
156    pub installdate: BuildDate,
157
158    /// The User ID of the entity, that built the package.
159    pub packager: Packager,
160
161    /// The optional size of the (uncompressed and unpacked) package contents in bytes.
162    pub size: InstalledSize,
163
164    /// Groups the package belongs to.
165    pub groups: Vec<Group>,
166
167    /// Optional install reason.
168    pub reason: PackageInstallReason,
169
170    /// Licenses that apply to the package.
171    pub license: Vec<License>,
172
173    /// Validation methods used for the package archive.
174    pub validation: Vec<PackageValidation>,
175
176    /// Packages this one replaces.
177    pub replaces: Vec<PackageRelation>,
178
179    /// Required runtime dependencies.
180    pub depends: Vec<RelationOrSoname>,
181
182    /// Optional dependencies that enhance the package.
183    pub optdepends: Vec<OptionalDependency>,
184
185    /// Conflicting packages that cannot be installed together.
186    pub conflicts: Vec<PackageRelation>,
187
188    /// Virtual packages or capabilities provided by this one.
189    pub provides: Vec<RelationOrSoname>,
190
191    /// Structured extra metadata, implementation-defined.
192    #[serde_as(as = "TryFromInto<Vec<ExtraDataEntry>>")]
193    pub xdata: ExtraData,
194}
195
196impl Display for DbDescFileV2 {
197    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
198        // Reuse v1 formatting
199        let base: DbDescFileV1 = self.clone().into();
200        write!(f, "{base}")?;
201
202        // Write xdata section
203        writeln!(f, "%XDATA%")?;
204        for v in self.xdata.clone() {
205            writeln!(f, "{v}")?;
206        }
207
208        writeln!(f)
209    }
210}
211
212impl FromStr for DbDescFileV2 {
213    type Err = Error;
214
215    /// Creates a [`DbDescFileV2`] from a string slice.
216    ///
217    /// Parses the input according to the [alpm-db-descv2] specification (version 2) and constructs
218    /// a structured [`DbDescFileV2`] representation including the `%XDATA%` section.
219    ///
220    /// # Examples
221    ///
222    /// ```
223    /// use std::str::FromStr;
224    ///
225    /// use alpm_db::desc::DbDescFileV2;
226    ///
227    /// # fn main() -> Result<(), alpm_db::Error> {
228    /// let desc_data = r#"%NAME%
229    /// foo
230    ///
231    /// %VERSION%
232    /// 1.0.0-1
233    ///
234    /// %BASE%
235    /// foo
236    ///
237    /// %DESC%
238    /// An example package
239    ///
240    /// %URL%
241    /// https://example.org
242    ///
243    /// %ARCH%
244    /// x86_64
245    ///
246    /// %BUILDDATE%
247    /// 1733737242
248    ///
249    /// %INSTALLDATE%
250    /// 1733737243
251    ///
252    /// %PACKAGER%
253    /// Foobar McFooface <foobar@mcfooface.org>
254    ///
255    /// %SIZE%
256    /// 123
257    ///
258    /// %VALIDATION%
259    /// pgp
260    ///
261    /// %XDATA%
262    /// pkgtype=pkg
263    ///
264    /// "#;
265    ///
266    /// let db_desc = DbDescFileV2::from_str(desc_data)?;
267    /// assert_eq!(db_desc.name.to_string(), "foo");
268    /// # Ok(())
269    /// # }
270    /// ```
271    ///
272    /// # Errors
273    ///
274    /// Returns an error if:
275    ///
276    /// - the input cannot be parsed into valid sections,
277    /// - or required fields are missing or malformed.
278    ///
279    /// [alpm-db-descv2]: https://alpm.archlinux.page/specifications/alpm-db-descv2.5.html
280    fn from_str(s: &str) -> Result<Self, Self::Err> {
281        let sections = sections.parse(s)?;
282        Self::try_from(sections)
283    }
284}
285
286impl TryFrom<Vec<Section>> for DbDescFileV2 {
287    type Error = Error;
288
289    /// Tries to create a [`DbDescFileV2`] from a list of parsed [`Section`]s.
290    ///
291    /// Reuses the parsing logic from [`DbDescFileV1`] for all common fields, and adds support for
292    /// the `%XDATA%` section introduced in the [alpm-db-descv2] specification.
293    ///
294    /// # Errors
295    ///
296    /// Returns an error if:
297    ///
298    /// - any required field is missing,
299    /// - a section appears more than once,
300    /// - or the `%XDATA%` section is missing or malformed.
301    ///
302    /// [alpm-db-descv2]: https://alpm.archlinux.page/specifications/alpm-db-descv2.5.html
303    fn try_from(sections: Vec<Section>) -> Result<Self, Self::Error> {
304        // Reuse v1 fields
305        let v1 = DbDescFileV1::try_from(sections.clone())?;
306
307        // Find xdata section
308        let xdata = if let Some(Section::XData(v)) =
309            sections.iter().find(|s| matches!(s, Section::XData(_)))
310        {
311            v.clone()
312        } else {
313            return Err(Error::MissingSection(SectionKeyword::XData));
314        };
315
316        Ok(Self {
317            name: v1.name,
318            version: v1.version,
319            base: v1.base,
320            description: v1.description,
321            url: v1.url,
322            arch: v1.arch,
323            builddate: v1.builddate,
324            installdate: v1.installdate,
325            packager: v1.packager,
326            size: v1.size,
327            groups: v1.groups,
328            reason: v1.reason,
329            license: v1.license,
330            validation: v1.validation,
331            replaces: v1.replaces,
332            depends: v1.depends,
333            optdepends: v1.optdepends,
334            conflicts: v1.conflicts,
335            provides: v1.provides,
336            xdata,
337        })
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use pretty_assertions::assert_eq;
344    use rstest::*;
345    use testresult::TestResult;
346
347    use super::*;
348
349    const VALID_DESC_FILE: &str = r#"%NAME%
350foo
351
352%VERSION%
3531.0.0-1
354
355%BASE%
356foo
357
358%DESC%
359An example package
360
361%URL%
362https://example.org/
363
364%ARCH%
365x86_64
366
367%BUILDDATE%
3681733737242
369
370%INSTALLDATE%
3711733737243
372
373%PACKAGER%
374Foobar McFooface <foobar@mcfooface.org>
375
376%SIZE%
377123
378
379%GROUPS%
380utils
381cli
382
383%REASON%
3841
385
386%LICENSE%
387MIT
388Apache-2.0
389
390%VALIDATION%
391sha256
392pgp
393
394%REPLACES%
395pkg-old
396
397%DEPENDS%
398glibc
399libwlroots-0.19.so=libwlroots-0.19.so-64
400lib:libexample.so.1
401
402%OPTDEPENDS%
403optpkg
404
405%CONFLICTS%
406foo-old
407
408%PROVIDES%
409foo-virtual
410libwlroots-0.19.so=libwlroots-0.19.so-64
411lib:libexample.so.1
412
413%XDATA%
414pkgtype=pkg
415
416"#;
417
418    #[test]
419    fn parse_valid_v2_desc() -> TestResult {
420        let actual = DbDescFileV2::from_str(VALID_DESC_FILE)?;
421        let expected = DbDescFileV2 {
422            name: Name::new("foo")?,
423            version: FullVersion::from_str("1.0.0-1")?,
424            base: PackageBaseName::new("foo")?,
425            description: PackageDescription::from("An example package"),
426            url: Some(Url::from_str("https://example.org")?),
427            arch: Architecture::from_str("x86_64")?,
428            builddate: BuildDate::from(1733737242),
429            installdate: BuildDate::from(1733737243),
430            packager: Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?,
431            size: 123,
432            groups: vec!["utils".into(), "cli".into()],
433            reason: PackageInstallReason::Depend,
434            license: vec![License::from_str("MIT")?, License::from_str("Apache-2.0")?],
435            validation: vec![
436                PackageValidation::from_str("sha256")?,
437                PackageValidation::from_str("pgp")?,
438            ],
439            replaces: vec![PackageRelation::from_str("pkg-old")?],
440            depends: vec![
441                RelationOrSoname::from_str("glibc")?,
442                RelationOrSoname::from_str("libwlroots-0.19.so=libwlroots-0.19.so-64")?,
443                RelationOrSoname::from_str("lib:libexample.so.1")?,
444            ],
445            optdepends: vec![OptionalDependency::from_str("optpkg")?],
446            conflicts: vec![PackageRelation::from_str("foo-old")?],
447            provides: vec![
448                RelationOrSoname::from_str("foo-virtual")?,
449                RelationOrSoname::from_str("libwlroots-0.19.so=libwlroots-0.19.so-64")?,
450                RelationOrSoname::from_str("lib:libexample.so.1")?,
451            ],
452            xdata: ExtraDataEntry::from_str("pkgtype=pkg")?.try_into()?,
453        };
454        assert_eq!(actual, expected);
455        assert_eq!(VALID_DESC_FILE, actual.to_string());
456        Ok(())
457    }
458
459    #[test]
460    fn depends_and_provides_accept_sonames() -> TestResult {
461        let desc = DbDescFileV2::from_str(VALID_DESC_FILE)?;
462        assert!(matches!(desc.depends[1], RelationOrSoname::SonameV1(_)));
463        assert!(matches!(desc.depends[2], RelationOrSoname::SonameV2(_)));
464        assert!(matches!(desc.provides[1], RelationOrSoname::SonameV1(_)));
465        assert!(matches!(desc.provides[2], RelationOrSoname::SonameV2(_)));
466        Ok(())
467    }
468
469    #[rstest]
470    #[case("%UNKNOWN%\nvalue", "invalid section name")]
471    #[case("%VERSION%\n1.0.0-1\n", "Missing section: %NAME%")]
472    fn invalid_desc_parser(#[case] input: &str, #[case] error_snippet: &str) {
473        let result = DbDescFileV2::from_str(input);
474        assert!(result.is_err());
475        let err = result.unwrap_err();
476        let pretty_error = err.to_string();
477        assert!(
478            pretty_error.contains(error_snippet),
479            "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
480        );
481    }
482}