Skip to main content

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 rstest::*;
455    use testresult::TestResult;
456
457    use super::*;
458
459    /// An alpm-db-desc string with all sections explicitly populated.
460    const DESC_FULL: &str = include_str!("../../tests/correct/desc/v1/full.desc");
461
462    #[test]
463    fn depends_and_provides_accept_sonames() -> TestResult {
464        let desc = DbDescFileV1::from_str(DESC_FULL)?;
465        assert!(matches!(desc.depends[1], RelationOrSoname::SonameV1(_)));
466        assert!(matches!(desc.depends[2], RelationOrSoname::SonameV2(_)));
467        assert!(matches!(desc.provides[1], RelationOrSoname::SonameV1(_)));
468        assert!(matches!(desc.provides[2], RelationOrSoname::SonameV2(_)));
469        Ok(())
470    }
471
472    #[rstest]
473    #[case("%UNKNOWN%\nvalue", "invalid section name")]
474    #[case("%VERSION%\n1.0.0-1\n", "Missing section: %NAME%")]
475    fn invalid_desc_parser(#[case] input: &str, #[case] error_snippet: &str) {
476        let result = DbDescFileV1::from_str(input);
477        assert!(result.is_err());
478        let err = result.unwrap_err();
479        let pretty_error = err.to_string();
480        assert!(
481            pretty_error.contains(error_snippet),
482            "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
483        );
484    }
485
486    #[test]
487    fn missing_required_section_should_fail() {
488        let input = "%VERSION%\n1.0.0-1\n";
489        let result = DbDescFileV1::from_str(input);
490        assert!(matches!(result, Err(Error::MissingSection(s)) if s == SectionKeyword::Name));
491    }
492}