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