alpm_pkginfo/package_info/
v1.rs

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