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 PackageInfoV2 {
82 #[serde_as(as = "Vec<DisplayFromStr>")]
83 xdata: Vec<ExtraData>,
84 }
85}
86
87impl PackageInfoV2 {
88 #[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 pub fn xdata(&self) -> &Vec<ExtraData> {
140 &self.xdata
141 }
142
143 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 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 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 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 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}