alpm_pkginfo/package_info/
v2.rs

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