alpm_pkginfo/package_info/
v1.rs

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