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    FullVersion,
16    Group,
17    InstalledSize,
18    License,
19    Name,
20    OptionalDependency,
21    PackageDescription,
22    PackageRelation,
23    PackageType,
24    Packager,
25    Url,
26};
27use serde_with::{DisplayFromStr, serde_as};
28
29use crate::{Error, RelationOrSoname, package_info::v1::generate_pkginfo};
30
31generate_pkginfo! {
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    PackageInfoV2 {
86        #[serde_as(as = "Vec<DisplayFromStr>")]
87        xdata: Vec<ExtraData>,
88    }
89}
90
91impl PackageInfoV2 {
92    /// Create a new PackageInfoV2 from all required components
93    #[allow(clippy::too_many_arguments)]
94    pub fn new(
95        pkgname: Name,
96        pkgbase: Name,
97        pkgver: FullVersion,
98        pkgdesc: PackageDescription,
99        url: Url,
100        builddate: BuildDate,
101        packager: Packager,
102        size: InstalledSize,
103        arch: Architecture,
104        license: Vec<License>,
105        replaces: Vec<PackageRelation>,
106        group: Vec<Group>,
107        conflict: Vec<PackageRelation>,
108        provides: Vec<RelationOrSoname>,
109        backup: Vec<Backup>,
110        depend: Vec<RelationOrSoname>,
111        optdepend: Vec<OptionalDependency>,
112        makedepend: Vec<PackageRelation>,
113        checkdepend: Vec<PackageRelation>,
114        xdata: Vec<ExtraData>,
115    ) -> Result<Self, Error> {
116        let pkg_info = Self {
117            pkgname,
118            pkgbase,
119            pkgver,
120            pkgdesc,
121            url,
122            builddate,
123            packager,
124            size,
125            arch,
126            license,
127            replaces,
128            group,
129            conflict,
130            provides,
131            backup,
132            depend,
133            optdepend,
134            makedepend,
135            checkdepend,
136            xdata,
137        };
138        pkg_info.check_pkg_type()?;
139        Ok(pkg_info)
140    }
141
142    /// Get the extra data
143    pub fn xdata(&self) -> &Vec<ExtraData> {
144        &self.xdata
145    }
146
147    /// Returns the package type.
148    ///
149    /// # Panics
150    ///
151    /// This function panics if the `xdata` field does not contain a `pkgtype` key.
152    pub fn pkg_type(&self) -> PackageType {
153        self.xdata
154            .iter()
155            .find(|v| v.key() == "pkgtype")
156            .map(|v| PackageType::from_str(v.value()).expect("Invalid package type"))
157            .unwrap_or_else(|| panic!("Missing extra data"))
158    }
159
160    /// Checks if the package type exists.
161    ///
162    /// # Errors
163    ///
164    /// This function returns an error in the following cases:
165    ///
166    /// - if the `xdata` field does not contain a `pkgtype` key.
167    /// - if the `pkgtype` key does not contain a valid package type.
168    fn check_pkg_type(&self) -> Result<(), Error> {
169        if let Some(pkg_type) = self.xdata.iter().find(|v| v.key() == "pkgtype") {
170            let _ = PackageType::from_str(pkg_type.value())?;
171            Ok(())
172        } else {
173            Err(Error::MissingExtraData)
174        }
175    }
176}
177
178impl FromStr for PackageInfoV2 {
179    type Err = Error;
180    /// Create a PackageInfoV2 from a &str
181    ///
182    /// ## Errors
183    ///
184    /// Returns an `Error` if any of the fields in `input` can not be validated according to
185    /// `PackageInfoV2` or their respective own specification.
186    fn from_str(input: &str) -> Result<PackageInfoV2, Self::Err> {
187        let pkg_info: PackageInfoV2 = alpm_parsers::custom_ini::from_str(input)?;
188        pkg_info.check_pkg_type()?;
189        Ok(pkg_info)
190    }
191}
192
193impl Display for PackageInfoV2 {
194    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
195        fn format_list(label: &str, items: &[impl Display]) -> String {
196            if items.is_empty() {
197                String::new()
198            } else {
199                items
200                    .iter()
201                    .map(|v| format!("{label} = {v}"))
202                    .collect::<Vec<_>>()
203                    .join("\n")
204                    + "\n"
205            }
206        }
207        let pkg_type = self
208            .xdata
209            .iter()
210            .find(|v| v.key() == "pkgtype")
211            .ok_or(std::fmt::Error)?;
212        let other_xdata = self
213            .xdata
214            .iter()
215            .filter(|v| v.key() != "pkgtype")
216            .collect::<Vec<_>>();
217        write!(
218            fmt,
219            "pkgname = {}\n\
220            pkgbase = {}\n\
221            xdata = {pkg_type}\n\
222            pkgver = {}\n\
223            pkgdesc = {}\n\
224            url = {}\n\
225            builddate = {}\n\
226            packager = {}\n\
227            size = {}\n\
228            arch = {}\n\
229            {}\
230            {}\
231            {}\
232            {}\
233            {}\
234            {}\
235            {}\
236            {}\
237            {}\
238            {}{}",
239            self.pkgname(),
240            self.pkgbase(),
241            self.pkgver(),
242            self.pkgdesc(),
243            self.url(),
244            self.builddate(),
245            self.packager(),
246            self.size(),
247            self.arch(),
248            format_list("license", self.license()),
249            format_list("replaces", self.replaces()),
250            format_list("group", self.group()),
251            format_list("conflict", self.conflict()),
252            format_list("provides", self.provides()),
253            format_list("backup", self.backup()),
254            format_list("depend", self.depend()),
255            format_list("optdepend", self.optdepend()),
256            format_list("makedepend", self.makedepend()),
257            format_list("checkdepend", self.checkdepend()).trim_end_matches('\n'),
258            if other_xdata.is_empty() {
259                String::new()
260            } else {
261                format!(
262                    "\n{}",
263                    other_xdata
264                        .iter()
265                        .map(|v| format!("xdata = {v}"))
266                        .collect::<Vec<_>>()
267                        .join("\n"),
268                )
269            },
270        )
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use pretty_assertions::assert_eq;
277    use rstest::rstest;
278    use testresult::TestResult;
279
280    use super::*;
281
282    // Test data
283    const VALID_PKGINFOV2_CASE1: &str = r#"pkgname = example
284pkgbase = example
285xdata = pkgtype=pkg
286pkgver = 1:1.0.0-1
287pkgdesc = A project that does something
288url = https://example.org/
289builddate = 1729181726
290packager = John Doe <john@example.org>
291size = 181849963
292arch = any
293license = GPL-3.0-or-later
294license = LGPL-3.0-or-later
295replaces = other-package>0.9.0-3
296group = package-group
297group = other-package-group
298conflict = conflicting-package<1.0.0
299conflict = other-conflicting-package<1.0.0
300provides = some-component
301provides = some-other-component=1:1.0.0-1
302provides = libexample.so=1-64
303provides = libunversionedexample.so=libunversionedexample.so-64
304provides = lib:libexample.so.1
305backup = etc/example/config.toml
306backup = etc/example/other-config.txt
307depend = glibc
308depend = gcc-libs
309depend = libother.so=0-64
310depend = libunversioned.so=libunversioned.so-64
311depend = lib:libother.so.0
312optdepend = python: for special-python-script.py
313optdepend = ruby: for special-ruby-script.rb
314makedepend = cmake
315makedepend = python-sphinx
316checkdepend = extra-test-tool
317checkdepend = other-extra-test-tool"#;
318
319    // Test data without multiple values
320    const VALID_PKGINFOV2_CASE2: &str = r#"
321pkgname = example
322pkgbase = example
323xdata = pkgtype=pkg
324pkgver = 1:1.0.0-1
325pkgdesc = A project that does something
326url = https://example.org
327builddate = 1729181726
328packager = John Doe <john@example.org>
329size = 181849963
330arch = any
331license = GPL-3.0-or-later
332replaces = other-package>0.9.0-3
333group = package-group
334conflict = conflicting-package<1.0.0
335provides = some-component
336backup = etc/example/config.toml
337depend = glibc
338optdepend = python: for special-python-script.py
339makedepend = cmake
340checkdepend = extra-test-tool
341"#;
342
343    #[rstest]
344    #[case(VALID_PKGINFOV2_CASE1)]
345    #[case(VALID_PKGINFOV2_CASE2)]
346    fn pkginfov2_from_str(#[case] pkginfo: &str) -> TestResult {
347        PackageInfoV2::from_str(pkginfo)?;
348        Ok(())
349    }
350
351    fn pkg_info() -> TestResult<PackageInfoV2> {
352        let pkg_info = PackageInfoV2::new(
353            Name::new("example")?,
354            Name::new("example")?,
355            FullVersion::from_str("1:1.0.0-1")?,
356            PackageDescription::from("A project that does something"),
357            Url::from_str("https://example.org")?,
358            BuildDate::from_str("1729181726")?,
359            Packager::from_str("John Doe <john@example.org>")?,
360            InstalledSize::from_str("181849963")?,
361            Architecture::Any,
362            vec![
363                License::from_str("GPL-3.0-or-later")?,
364                License::from_str("LGPL-3.0-or-later")?,
365            ],
366            vec![PackageRelation::from_str("other-package>0.9.0-3")?],
367            vec![
368                Group::from_str("package-group")?,
369                Group::from_str("other-package-group")?,
370            ],
371            vec![
372                PackageRelation::from_str("conflicting-package<1.0.0")?,
373                PackageRelation::from_str("other-conflicting-package<1.0.0")?,
374            ],
375            vec![
376                RelationOrSoname::from_str("some-component")?,
377                RelationOrSoname::from_str("some-other-component=1:1.0.0-1")?,
378                RelationOrSoname::from_str("libexample.so=1-64")?,
379                RelationOrSoname::from_str("libunversionedexample.so=libunversionedexample.so-64")?,
380                RelationOrSoname::from_str("lib:libexample.so.1")?,
381            ],
382            vec![
383                Backup::from_str("etc/example/config.toml")?,
384                Backup::from_str("etc/example/other-config.txt")?,
385            ],
386            vec![
387                RelationOrSoname::from_str("glibc")?,
388                RelationOrSoname::from_str("gcc-libs")?,
389                RelationOrSoname::from_str("libother.so=0-64")?,
390                RelationOrSoname::from_str("libunversioned.so=libunversioned.so-64")?,
391                RelationOrSoname::from_str("lib:libother.so.0")?,
392            ],
393            vec![
394                OptionalDependency::from_str("python: for special-python-script.py")?,
395                OptionalDependency::from_str("ruby: for special-ruby-script.rb")?,
396            ],
397            vec![
398                PackageRelation::from_str("cmake")?,
399                PackageRelation::from_str("python-sphinx")?,
400            ],
401            vec![
402                PackageRelation::from_str("extra-test-tool")?,
403                PackageRelation::from_str("other-extra-test-tool")?,
404            ],
405            vec![ExtraData::from_str("pkgtype=pkg")?],
406        )?;
407        assert_eq!(PackageType::Package, pkg_info.pkg_type());
408        Ok(pkg_info)
409    }
410
411    #[rstest]
412    fn pkginfov2() -> TestResult {
413        let pkg_info = pkg_info()?;
414        assert_eq!(pkg_info.to_string(), VALID_PKGINFOV2_CASE1);
415        Ok(())
416    }
417
418    #[rstest]
419    fn pkginfov2_invalid_xdata_fail() -> TestResult {
420        let mut pkg_info = pkg_info()?;
421        pkg_info.xdata = vec![];
422        assert!(pkg_info.check_pkg_type().is_err());
423
424        pkg_info.xdata = vec![ExtraData::from_str("pkgtype=foo")?];
425        assert!(pkg_info.check_pkg_type().is_err());
426        Ok(())
427    }
428
429    #[rstest]
430    fn pkginfov2_multiple_xdata() -> TestResult {
431        let mut pkg_info = pkg_info()?;
432        pkg_info.xdata.push(ExtraData::from_str("foo=bar")?);
433        pkg_info.xdata.push(ExtraData::from_str("baz=qux")?);
434        assert_eq!(
435            pkg_info.to_string(),
436            format!("{VALID_PKGINFOV2_CASE1}\nxdata = foo=bar\nxdata = baz=qux")
437        );
438        Ok(())
439    }
440
441    #[rstest]
442    fn pkginfov2_missing_xdata_fail() -> TestResult {
443        let mut pkg_info_str = VALID_PKGINFOV2_CASE1.to_string();
444        pkg_info_str = pkg_info_str.replace("xdata = pkgtype=pkg\n", "");
445        assert!(PackageInfoV2::from_str(&pkg_info_str).is_err());
446
447        let pkg_info = pkg_info()?;
448        assert!(
449            PackageInfoV2::new(
450                pkg_info.pkgname,
451                pkg_info.pkgbase,
452                pkg_info.pkgver,
453                pkg_info.pkgdesc,
454                pkg_info.url,
455                pkg_info.builddate,
456                pkg_info.packager,
457                pkg_info.size,
458                pkg_info.arch,
459                pkg_info.license,
460                pkg_info.replaces,
461                pkg_info.group,
462                pkg_info.conflict,
463                pkg_info.provides,
464                pkg_info.backup,
465                pkg_info.depend,
466                pkg_info.optdepend,
467                pkg_info.makedepend,
468                pkg_info.checkdepend,
469                vec![]
470            )
471            .is_err()
472        );
473        Ok(())
474    }
475
476    #[rstest]
477    #[case("pkgname = foo")]
478    #[case("pkgbase = foo")]
479    #[case("pkgver = 1:1.0.0-1")]
480    #[case("packager = Foobar McFooface <foobar@mcfooface.org>")]
481    #[case("pkgarch = any")]
482    fn pkginfov2_from_str_duplicate_fail(#[case] duplicate: &str) {
483        let mut pkginfov2 = VALID_PKGINFOV2_CASE1.to_string();
484        pkginfov2.push_str(duplicate);
485        assert!(PackageInfoV2::from_str(&pkginfov2).is_err());
486    }
487}