alpm_pkginfo/package_info/
v2.rs1use std::{
6 fmt::{Display, Formatter},
7 str::FromStr,
8};
9
10use alpm_types::{
11 Architecture,
12 Backup,
13 BuildDate,
14 ExtraData,
15 ExtraDataEntry,
16 FullVersion,
17 Group,
18 InstalledSize,
19 License,
20 Name,
21 OptionalDependency,
22 PackageDescription,
23 PackageRelation,
24 Packager,
25 RelationOrSoname,
26 Url,
27};
28use serde_with::{DisplayFromStr, TryFromInto, serde_as};
29
30use crate::Error;
31
32#[serde_as]
86#[derive(Clone, Debug, serde::Deserialize, PartialEq, serde::Serialize)]
87#[serde(deny_unknown_fields)]
88pub struct PackageInfoV2 {
89 #[serde_as(as = "DisplayFromStr")]
91 pub pkgname: Name,
92
93 #[serde_as(as = "DisplayFromStr")]
95 pub pkgbase: Name,
96
97 #[serde_as(as = "DisplayFromStr")]
99 pub pkgver: FullVersion,
100
101 #[serde_as(as = "DisplayFromStr")]
103 pub pkgdesc: PackageDescription,
104
105 #[serde_as(as = "DisplayFromStr")]
107 pub url: Url,
108
109 #[serde_as(as = "DisplayFromStr")]
111 pub builddate: BuildDate,
112
113 #[serde_as(as = "DisplayFromStr")]
115 pub packager: Packager,
116
117 #[serde_as(as = "DisplayFromStr")]
119 pub size: InstalledSize,
120
121 #[serde_as(as = "DisplayFromStr")]
123 pub arch: Architecture,
124
125 #[serde_as(as = "Vec<DisplayFromStr>")]
127 #[serde(default)]
128 pub license: Vec<License>,
129
130 #[serde_as(as = "Vec<DisplayFromStr>")]
132 #[serde(default)]
133 pub replaces: Vec<PackageRelation>,
134
135 #[serde_as(as = "Vec<DisplayFromStr>")]
137 #[serde(default)]
138 pub group: Vec<Group>,
139
140 #[serde_as(as = "Vec<DisplayFromStr>")]
142 #[serde(default)]
143 pub conflict: Vec<PackageRelation>,
144
145 #[serde_as(as = "Vec<DisplayFromStr>")]
147 #[serde(default)]
148 pub provides: Vec<RelationOrSoname>,
149
150 #[serde_as(as = "Vec<DisplayFromStr>")]
152 #[serde(default)]
153 pub backup: Vec<Backup>,
154
155 #[serde_as(as = "Vec<DisplayFromStr>")]
157 #[serde(default)]
158 pub depend: Vec<RelationOrSoname>,
159
160 #[serde_as(as = "Vec<DisplayFromStr>")]
162 #[serde(default)]
163 pub optdepend: Vec<OptionalDependency>,
164
165 #[serde_as(as = "Vec<DisplayFromStr>")]
167 #[serde(default)]
168 pub makedepend: Vec<PackageRelation>,
169
170 #[serde_as(as = "Vec<DisplayFromStr>")]
172 #[serde(default)]
173 pub checkdepend: Vec<PackageRelation>,
174
175 #[serde_as(as = "TryFromInto<Vec<ExtraDataEntry>>")]
177 pub xdata: ExtraData,
178}
179
180impl FromStr for PackageInfoV2 {
181 type Err = Error;
182 fn from_str(input: &str) -> Result<PackageInfoV2, Self::Err> {
189 let pkg_info: PackageInfoV2 = alpm_parsers::custom_ini::from_str(input)?;
190 Ok(pkg_info)
191 }
192}
193
194impl Display for PackageInfoV2 {
195 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
196 fn format_list(label: &str, items: &[impl Display]) -> String {
197 if items.is_empty() {
198 String::new()
199 } else {
200 items
201 .iter()
202 .map(|v| format!("{label} = {v}"))
203 .collect::<Vec<_>>()
204 .join("\n")
205 + "\n"
206 }
207 }
208 let pkg_type = self.xdata.pkg_type();
209 let other_xdata = self
210 .xdata
211 .as_ref()
212 .iter()
213 .filter(|v| v.key() != "pkgtype")
214 .collect::<Vec<_>>();
215 write!(
216 fmt,
217 "pkgname = {}\n\
218 pkgbase = {}\n\
219 xdata = pkgtype={pkg_type}\n\
220 pkgver = {}\n\
221 pkgdesc = {}\n\
222 url = {}\n\
223 builddate = {}\n\
224 packager = {}\n\
225 size = {}\n\
226 arch = {}\n\
227 {}\
228 {}\
229 {}\
230 {}\
231 {}\
232 {}\
233 {}\
234 {}\
235 {}\
236 {}{}",
237 self.pkgname,
238 self.pkgbase,
239 self.pkgver,
240 self.pkgdesc,
241 self.url,
242 self.builddate,
243 self.packager,
244 self.size,
245 self.arch,
246 format_list("license", &self.license),
247 format_list("replaces", &self.replaces),
248 format_list("group", &self.group),
249 format_list("conflict", &self.conflict),
250 format_list("provides", &self.provides),
251 format_list("backup", &self.backup),
252 format_list("depend", &self.depend),
253 format_list("optdepend", &self.optdepend),
254 format_list("makedepend", &self.makedepend),
255 format_list("checkdepend", &self.checkdepend).trim_end_matches('\n'),
256 if other_xdata.is_empty() {
257 String::new()
258 } else {
259 format!(
260 "\n{}",
261 other_xdata
262 .iter()
263 .map(|v| format!("xdata = {v}"))
264 .collect::<Vec<_>>()
265 .join("\n"),
266 )
267 },
268 )
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use alpm_types::PackageType;
275 use pretty_assertions::assert_eq;
276 use rstest::rstest;
277 use testresult::TestResult;
278
279 use super::*;
280
281 const VALID_PKGINFOV2_CASE1: &str = r#"pkgname = example
283pkgbase = example
284xdata = pkgtype=pkg
285pkgver = 1:1.0.0-1
286pkgdesc = A project that does something
287url = https://example.org/
288builddate = 1729181726
289packager = John Doe <john@example.org>
290size = 181849963
291arch = any
292license = GPL-3.0-or-later
293license = LGPL-3.0-or-later
294replaces = other-package>0.9.0-3
295group = package-group
296group = other-package-group
297conflict = conflicting-package<1.0.0
298conflict = other-conflicting-package<1.0.0
299provides = some-component
300provides = some-other-component=1:1.0.0-1
301provides = libexample.so=1-64
302provides = libunversionedexample.so=libunversionedexample.so-64
303provides = lib:libexample.so.1
304backup = etc/example/config.toml
305backup = etc/example/other-config.txt
306depend = glibc
307depend = gcc-libs
308depend = libother.so=0-64
309depend = libunversioned.so=libunversioned.so-64
310depend = lib:libother.so.0
311optdepend = python: for special-python-script.py
312optdepend = ruby: for special-ruby-script.rb
313makedepend = cmake
314makedepend = python-sphinx
315checkdepend = extra-test-tool
316checkdepend = other-extra-test-tool"#;
317
318 const VALID_PKGINFOV2_CASE2: &str = r#"
320pkgname = example
321pkgbase = example
322xdata = pkgtype=pkg
323pkgver = 1:1.0.0-1
324pkgdesc = A project that does something
325url = https://example.org
326builddate = 1729181726
327packager = John Doe <john@example.org>
328size = 181849963
329arch = any
330license = GPL-3.0-or-later
331replaces = other-package>0.9.0-3
332group = package-group
333conflict = conflicting-package<1.0.0
334provides = some-component
335backup = etc/example/config.toml
336depend = glibc
337optdepend = python: for special-python-script.py
338makedepend = cmake
339checkdepend = extra-test-tool
340"#;
341
342 #[rstest]
343 #[case(VALID_PKGINFOV2_CASE1)]
344 #[case(VALID_PKGINFOV2_CASE2)]
345 fn pkginfov2_from_str(#[case] pkginfo: &str) -> TestResult {
346 PackageInfoV2::from_str(pkginfo)?;
347 Ok(())
348 }
349
350 fn pkg_info() -> TestResult<PackageInfoV2> {
351 let pkg_info = PackageInfoV2 {
352 pkgname: Name::new("example")?,
353 pkgbase: Name::new("example")?,
354 pkgver: FullVersion::from_str("1:1.0.0-1")?,
355 pkgdesc: PackageDescription::from("A project that does something"),
356 url: Url::from_str("https://example.org")?,
357 builddate: BuildDate::from_str("1729181726")?,
358 packager: Packager::from_str("John Doe <john@example.org>")?,
359 size: InstalledSize::from_str("181849963")?,
360 arch: Architecture::Any,
361 license: vec![
362 License::from_str("GPL-3.0-or-later")?,
363 License::from_str("LGPL-3.0-or-later")?,
364 ],
365 replaces: vec![PackageRelation::from_str("other-package>0.9.0-3")?],
366 group: vec![
367 Group::from_str("package-group")?,
368 Group::from_str("other-package-group")?,
369 ],
370 conflict: vec![
371 PackageRelation::from_str("conflicting-package<1.0.0")?,
372 PackageRelation::from_str("other-conflicting-package<1.0.0")?,
373 ],
374 provides: vec![
375 RelationOrSoname::from_str("some-component")?,
376 RelationOrSoname::from_str("some-other-component=1:1.0.0-1")?,
377 RelationOrSoname::from_str("libexample.so=1-64")?,
378 RelationOrSoname::from_str("libunversionedexample.so=libunversionedexample.so-64")?,
379 RelationOrSoname::from_str("lib:libexample.so.1")?,
380 ],
381 backup: vec![
382 Backup::from_str("etc/example/config.toml")?,
383 Backup::from_str("etc/example/other-config.txt")?,
384 ],
385 depend: vec![
386 RelationOrSoname::from_str("glibc")?,
387 RelationOrSoname::from_str("gcc-libs")?,
388 RelationOrSoname::from_str("libother.so=0-64")?,
389 RelationOrSoname::from_str("libunversioned.so=libunversioned.so-64")?,
390 RelationOrSoname::from_str("lib:libother.so.0")?,
391 ],
392 optdepend: vec![
393 OptionalDependency::from_str("python: for special-python-script.py")?,
394 OptionalDependency::from_str("ruby: for special-ruby-script.rb")?,
395 ],
396 makedepend: vec![
397 PackageRelation::from_str("cmake")?,
398 PackageRelation::from_str("python-sphinx")?,
399 ],
400 checkdepend: vec![
401 PackageRelation::from_str("extra-test-tool")?,
402 PackageRelation::from_str("other-extra-test-tool")?,
403 ],
404 xdata: ExtraDataEntry::from_str("pkgtype=pkg")?.try_into()?,
405 };
406 assert_eq!(PackageType::Package, pkg_info.xdata.pkg_type());
407 Ok(pkg_info)
408 }
409
410 #[rstest]
411 fn pkginfov2() -> TestResult {
412 let pkg_info = pkg_info()?;
413 assert_eq!(pkg_info.to_string(), VALID_PKGINFOV2_CASE1);
414 Ok(())
415 }
416
417 #[rstest]
418 fn pkginfov2_multiple_xdata() -> TestResult {
419 let mut pkg_info = pkg_info()?;
420 let mut xdata = pkg_info.xdata.into_iter().collect::<Vec<_>>();
421 xdata.push(ExtraDataEntry::from_str("foo=bar")?);
422 xdata.push(ExtraDataEntry::from_str("baz=qux")?);
423 pkg_info.xdata = xdata.try_into()?;
424 assert_eq!(
425 pkg_info.to_string(),
426 format!("{VALID_PKGINFOV2_CASE1}\nxdata = foo=bar\nxdata = baz=qux")
427 );
428 Ok(())
429 }
430
431 #[rstest]
432 #[case("pkgname = foo")]
433 #[case("pkgbase = foo")]
434 #[case("pkgver = 1:1.0.0-1")]
435 #[case("packager = Foobar McFooface <foobar@mcfooface.org>")]
436 #[case("pkgarch = any")]
437 fn pkginfov2_from_str_duplicate_fail(#[case] duplicate: &str) {
438 let mut pkginfov2 = VALID_PKGINFOV2_CASE1.to_string();
439 pkginfov2.push_str(duplicate);
440 assert!(PackageInfoV2::from_str(&pkginfov2).is_err());
441 }
442}