alpm_pkginfo/package_info/
v2.rs

1//! The [PKGINFOv2] file format.
2//!
3//! [PKGINFOv2]: https://alpm.archlinux.page/specifications/PKGINFOv2.5.html
4
5use std::{
6    fmt::{Display, Formatter},
7    str::FromStr,
8};
9
10use alpm_types::{
11    Architecture,
12    Backup,
13    BuildDate,
14    ExtraData,
15    ExtraDataEntry,
16    FullVersion,
17    Group,
18    InstalledSize,
19    License,
20    Name,
21    OptionalDependency,
22    PackageDescription,
23    PackageRelation,
24    Packager,
25    RelationOrSoname,
26    Url,
27};
28use serde_with::{DisplayFromStr, TryFromInto, serde_as};
29
30use crate::Error;
31
32/// PKGINFO version 2
33///
34/// `PackageInfoV2` is (exclusively) compatible with data following the v2 specification of the
35/// PKGINFO file.
36///
37/// ## Examples
38///
39/// ```
40/// use std::str::FromStr;
41///
42/// use alpm_pkginfo::PackageInfoV2;
43///
44/// # fn main() -> Result<(), alpm_pkginfo::Error> {
45/// let pkginfo_data = r#"pkgname = example
46/// pkgbase = example
47/// xdata = pkgtype=pkg
48/// pkgver = 1:1.0.0-1
49/// pkgdesc = A project that does something
50/// url = https://example.org/
51/// builddate = 1729181726
52/// packager = John Doe <john@example.org>
53/// size = 181849963
54/// arch = any
55/// license = GPL-3.0-or-later
56/// license = LGPL-3.0-or-later
57/// replaces = other-package>0.9.0-3
58/// group = package-group
59/// group = other-package-group
60/// conflict = conflicting-package<1.0.0
61/// conflict = other-conflicting-package<1.0.0
62/// provides = some-component
63/// provides = some-other-component=1:1.0.0-1
64/// provides = libexample.so=1-64
65/// provides = libunversionedexample.so=libunversionedexample.so-64
66/// provides = lib:libexample.so.1
67/// backup = etc/example/config.toml
68/// backup = etc/example/other-config.txt
69/// depend = glibc
70/// depend = gcc-libs
71/// depend = libother.so=0-64
72/// depend = libunversioned.so=libunversioned.so-64
73/// depend = lib:libother.so.0
74/// optdepend = python: for special-python-script.py
75/// optdepend = ruby: for special-ruby-script.rb
76/// makedepend = cmake
77/// makedepend = python-sphinx
78/// checkdepend = extra-test-tool
79/// checkdepend = other-extra-test-tool"#;
80/// let pkginfo = PackageInfoV2::from_str(pkginfo_data)?;
81/// assert_eq!(pkginfo.to_string(), pkginfo_data);
82/// # Ok(())
83/// # }
84/// ```
85#[serde_as]
86#[derive(Clone, Debug, serde::Deserialize, PartialEq, serde::Serialize)]
87#[serde(deny_unknown_fields)]
88pub struct PackageInfoV2 {
89    /// The name of the package.
90    #[serde_as(as = "DisplayFromStr")]
91    pub pkgname: Name,
92
93    /// The base name of the package.
94    #[serde_as(as = "DisplayFromStr")]
95    pub pkgbase: Name,
96
97    /// The version of the package.
98    #[serde_as(as = "DisplayFromStr")]
99    pub pkgver: FullVersion,
100
101    /// The description of the package.
102    #[serde_as(as = "DisplayFromStr")]
103    pub pkgdesc: PackageDescription,
104
105    /// The URL of the package.
106    #[serde_as(as = "DisplayFromStr")]
107    pub url: Url,
108
109    /// The build date of the package.
110    #[serde_as(as = "DisplayFromStr")]
111    pub builddate: BuildDate,
112
113    /// The packager of the package.
114    #[serde_as(as = "DisplayFromStr")]
115    pub packager: Packager,
116
117    /// The size of the package.
118    #[serde_as(as = "DisplayFromStr")]
119    pub size: InstalledSize,
120
121    /// The architecture of the package.
122    #[serde_as(as = "DisplayFromStr")]
123    pub arch: Architecture,
124
125    /// The licenses of the package.
126    #[serde_as(as = "Vec<DisplayFromStr>")]
127    #[serde(default)]
128    pub license: Vec<License>,
129
130    /// The packages this package replaces.
131    #[serde_as(as = "Vec<DisplayFromStr>")]
132    #[serde(default)]
133    pub replaces: Vec<PackageRelation>,
134
135    /// The groups this package belongs to.
136    #[serde_as(as = "Vec<DisplayFromStr>")]
137    #[serde(default)]
138    pub group: Vec<Group>,
139
140    /// The packages this package conflicts with.
141    #[serde_as(as = "Vec<DisplayFromStr>")]
142    #[serde(default)]
143    pub conflict: Vec<PackageRelation>,
144
145    /// The packages this package provides.
146    #[serde_as(as = "Vec<DisplayFromStr>")]
147    #[serde(default)]
148    pub provides: Vec<RelationOrSoname>,
149
150    /// The backup files of the package.
151    #[serde_as(as = "Vec<DisplayFromStr>")]
152    #[serde(default)]
153    pub backup: Vec<Backup>,
154
155    /// The dependencies of the package.
156    #[serde_as(as = "Vec<DisplayFromStr>")]
157    #[serde(default)]
158    pub depend: Vec<RelationOrSoname>,
159
160    /// The optional dependencies of the package.
161    #[serde_as(as = "Vec<DisplayFromStr>")]
162    #[serde(default)]
163    pub optdepend: Vec<OptionalDependency>,
164
165    /// The packages required to build this package.
166    #[serde_as(as = "Vec<DisplayFromStr>")]
167    #[serde(default)]
168    pub makedepend: Vec<PackageRelation>,
169
170    /// The packages this package is checked with.
171    #[serde_as(as = "Vec<DisplayFromStr>")]
172    #[serde(default)]
173    pub checkdepend: Vec<PackageRelation>,
174
175    /// Extra data of the package.
176    #[serde_as(as = "TryFromInto<Vec<ExtraDataEntry>>")]
177    pub xdata: ExtraData,
178}
179
180impl FromStr for PackageInfoV2 {
181    type Err = Error;
182    /// Create a PackageInfoV2 from a &str
183    ///
184    /// ## Errors
185    ///
186    /// Returns an `Error` if any of the fields in `input` can not be validated according to
187    /// `PackageInfoV2` or their respective own specification.
188    fn from_str(input: &str) -> Result<PackageInfoV2, Self::Err> {
189        let pkg_info: PackageInfoV2 = alpm_parsers::custom_ini::from_str(input)?;
190        Ok(pkg_info)
191    }
192}
193
194impl Display for PackageInfoV2 {
195    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
196        fn format_list(label: &str, items: &[impl Display]) -> String {
197            if items.is_empty() {
198                String::new()
199            } else {
200                items
201                    .iter()
202                    .map(|v| format!("{label} = {v}"))
203                    .collect::<Vec<_>>()
204                    .join("\n")
205                    + "\n"
206            }
207        }
208        let pkg_type = self.xdata.pkg_type();
209        let other_xdata = self
210            .xdata
211            .as_ref()
212            .iter()
213            .filter(|v| v.key() != "pkgtype")
214            .collect::<Vec<_>>();
215        write!(
216            fmt,
217            "pkgname = {}\n\
218            pkgbase = {}\n\
219            xdata = pkgtype={pkg_type}\n\
220            pkgver = {}\n\
221            pkgdesc = {}\n\
222            url = {}\n\
223            builddate = {}\n\
224            packager = {}\n\
225            size = {}\n\
226            arch = {}\n\
227            {}\
228            {}\
229            {}\
230            {}\
231            {}\
232            {}\
233            {}\
234            {}\
235            {}\
236            {}{}",
237            self.pkgname,
238            self.pkgbase,
239            self.pkgver,
240            self.pkgdesc,
241            self.url,
242            self.builddate,
243            self.packager,
244            self.size,
245            self.arch,
246            format_list("license", &self.license),
247            format_list("replaces", &self.replaces),
248            format_list("group", &self.group),
249            format_list("conflict", &self.conflict),
250            format_list("provides", &self.provides),
251            format_list("backup", &self.backup),
252            format_list("depend", &self.depend),
253            format_list("optdepend", &self.optdepend),
254            format_list("makedepend", &self.makedepend),
255            format_list("checkdepend", &self.checkdepend).trim_end_matches('\n'),
256            if other_xdata.is_empty() {
257                String::new()
258            } else {
259                format!(
260                    "\n{}",
261                    other_xdata
262                        .iter()
263                        .map(|v| format!("xdata = {v}"))
264                        .collect::<Vec<_>>()
265                        .join("\n"),
266                )
267            },
268        )
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use alpm_types::PackageType;
275    use pretty_assertions::assert_eq;
276    use rstest::rstest;
277    use testresult::TestResult;
278
279    use super::*;
280
281    // Test data
282    const VALID_PKGINFOV2_CASE1: &str = r#"pkgname = example
283pkgbase = example
284xdata = pkgtype=pkg
285pkgver = 1:1.0.0-1
286pkgdesc = A project that does something
287url = https://example.org/
288builddate = 1729181726
289packager = John Doe <john@example.org>
290size = 181849963
291arch = any
292license = GPL-3.0-or-later
293license = LGPL-3.0-or-later
294replaces = other-package>0.9.0-3
295group = package-group
296group = other-package-group
297conflict = conflicting-package<1.0.0
298conflict = other-conflicting-package<1.0.0
299provides = some-component
300provides = some-other-component=1:1.0.0-1
301provides = libexample.so=1-64
302provides = libunversionedexample.so=libunversionedexample.so-64
303provides = lib:libexample.so.1
304backup = etc/example/config.toml
305backup = etc/example/other-config.txt
306depend = glibc
307depend = gcc-libs
308depend = libother.so=0-64
309depend = libunversioned.so=libunversioned.so-64
310depend = lib:libother.so.0
311optdepend = python: for special-python-script.py
312optdepend = ruby: for special-ruby-script.rb
313makedepend = cmake
314makedepend = python-sphinx
315checkdepend = extra-test-tool
316checkdepend = other-extra-test-tool"#;
317
318    // Test data without multiple values
319    const VALID_PKGINFOV2_CASE2: &str = r#"
320pkgname = example
321pkgbase = example
322xdata = pkgtype=pkg
323pkgver = 1:1.0.0-1
324pkgdesc = A project that does something
325url = https://example.org
326builddate = 1729181726
327packager = John Doe <john@example.org>
328size = 181849963
329arch = any
330license = GPL-3.0-or-later
331replaces = other-package>0.9.0-3
332group = package-group
333conflict = conflicting-package<1.0.0
334provides = some-component
335backup = etc/example/config.toml
336depend = glibc
337optdepend = python: for special-python-script.py
338makedepend = cmake
339checkdepend = extra-test-tool
340"#;
341
342    #[rstest]
343    #[case(VALID_PKGINFOV2_CASE1)]
344    #[case(VALID_PKGINFOV2_CASE2)]
345    fn pkginfov2_from_str(#[case] pkginfo: &str) -> TestResult {
346        PackageInfoV2::from_str(pkginfo)?;
347        Ok(())
348    }
349
350    fn pkg_info() -> TestResult<PackageInfoV2> {
351        let pkg_info = PackageInfoV2 {
352            pkgname: Name::new("example")?,
353            pkgbase: Name::new("example")?,
354            pkgver: FullVersion::from_str("1:1.0.0-1")?,
355            pkgdesc: PackageDescription::from("A project that does something"),
356            url: Url::from_str("https://example.org")?,
357            builddate: BuildDate::from_str("1729181726")?,
358            packager: Packager::from_str("John Doe <john@example.org>")?,
359            size: InstalledSize::from_str("181849963")?,
360            arch: Architecture::Any,
361            license: vec![
362                License::from_str("GPL-3.0-or-later")?,
363                License::from_str("LGPL-3.0-or-later")?,
364            ],
365            replaces: vec![PackageRelation::from_str("other-package>0.9.0-3")?],
366            group: vec![
367                Group::from_str("package-group")?,
368                Group::from_str("other-package-group")?,
369            ],
370            conflict: vec![
371                PackageRelation::from_str("conflicting-package<1.0.0")?,
372                PackageRelation::from_str("other-conflicting-package<1.0.0")?,
373            ],
374            provides: vec![
375                RelationOrSoname::from_str("some-component")?,
376                RelationOrSoname::from_str("some-other-component=1:1.0.0-1")?,
377                RelationOrSoname::from_str("libexample.so=1-64")?,
378                RelationOrSoname::from_str("libunversionedexample.so=libunversionedexample.so-64")?,
379                RelationOrSoname::from_str("lib:libexample.so.1")?,
380            ],
381            backup: vec![
382                Backup::from_str("etc/example/config.toml")?,
383                Backup::from_str("etc/example/other-config.txt")?,
384            ],
385            depend: vec![
386                RelationOrSoname::from_str("glibc")?,
387                RelationOrSoname::from_str("gcc-libs")?,
388                RelationOrSoname::from_str("libother.so=0-64")?,
389                RelationOrSoname::from_str("libunversioned.so=libunversioned.so-64")?,
390                RelationOrSoname::from_str("lib:libother.so.0")?,
391            ],
392            optdepend: vec![
393                OptionalDependency::from_str("python: for special-python-script.py")?,
394                OptionalDependency::from_str("ruby: for special-ruby-script.rb")?,
395            ],
396            makedepend: vec![
397                PackageRelation::from_str("cmake")?,
398                PackageRelation::from_str("python-sphinx")?,
399            ],
400            checkdepend: vec![
401                PackageRelation::from_str("extra-test-tool")?,
402                PackageRelation::from_str("other-extra-test-tool")?,
403            ],
404            xdata: ExtraDataEntry::from_str("pkgtype=pkg")?.try_into()?,
405        };
406        assert_eq!(PackageType::Package, pkg_info.xdata.pkg_type());
407        Ok(pkg_info)
408    }
409
410    #[rstest]
411    fn pkginfov2() -> TestResult {
412        let pkg_info = pkg_info()?;
413        assert_eq!(pkg_info.to_string(), VALID_PKGINFOV2_CASE1);
414        Ok(())
415    }
416
417    #[rstest]
418    fn pkginfov2_multiple_xdata() -> TestResult {
419        let mut pkg_info = pkg_info()?;
420        let mut xdata = pkg_info.xdata.into_iter().collect::<Vec<_>>();
421        xdata.push(ExtraDataEntry::from_str("foo=bar")?);
422        xdata.push(ExtraDataEntry::from_str("baz=qux")?);
423        pkg_info.xdata = xdata.try_into()?;
424        assert_eq!(
425            pkg_info.to_string(),
426            format!("{VALID_PKGINFOV2_CASE1}\nxdata = foo=bar\nxdata = baz=qux")
427        );
428        Ok(())
429    }
430
431    #[rstest]
432    #[case("pkgname = foo")]
433    #[case("pkgbase = foo")]
434    #[case("pkgver = 1:1.0.0-1")]
435    #[case("packager = Foobar McFooface <foobar@mcfooface.org>")]
436    #[case("pkgarch = any")]
437    fn pkginfov2_from_str_duplicate_fail(#[case] duplicate: &str) {
438        let mut pkginfov2 = VALID_PKGINFOV2_CASE1.to_string();
439        pkginfov2.push_str(duplicate);
440        assert!(PackageInfoV2::from_str(&pkginfov2).is_err());
441    }
442}