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