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