1use std::{
6 fmt::{Display, Formatter, Result as FmtResult},
7 str::FromStr,
8};
9
10use alpm_types::{
11 Architecture,
12 BuildDate,
13 ExtraData,
14 ExtraDataEntry,
15 FullVersion,
16 Group,
17 InstalledSize,
18 License,
19 Name,
20 OptionalDependency,
21 PackageBaseName,
22 PackageDescription,
23 PackageInstallReason,
24 PackageRelation,
25 PackageValidation,
26 Packager,
27 RelationOrSoname,
28 Url,
29};
30use serde_with::{TryFromInto, serde_as};
31use winnow::Parser;
32
33use crate::{
34 Error,
35 desc::{
36 DbDescFileV1,
37 Section,
38 parser::{SectionKeyword, sections},
39 },
40};
41
42#[serde_as]
130#[derive(Clone, Debug, serde::Deserialize, PartialEq, serde::Serialize)]
131#[serde(deny_unknown_fields)]
132#[serde(rename_all = "lowercase")]
133pub struct DbDescFileV2 {
134 pub name: Name,
136
137 pub version: FullVersion,
139
140 pub base: PackageBaseName,
142
143 pub description: PackageDescription,
145
146 pub url: Option<Url>,
148
149 pub arch: Architecture,
151
152 pub builddate: BuildDate,
154
155 pub installdate: BuildDate,
157
158 pub packager: Packager,
160
161 pub size: InstalledSize,
163
164 pub groups: Vec<Group>,
166
167 pub reason: PackageInstallReason,
169
170 pub license: Vec<License>,
172
173 pub validation: Vec<PackageValidation>,
175
176 pub replaces: Vec<PackageRelation>,
178
179 pub depends: Vec<RelationOrSoname>,
181
182 pub optdepends: Vec<OptionalDependency>,
184
185 pub conflicts: Vec<PackageRelation>,
187
188 pub provides: Vec<RelationOrSoname>,
190
191 #[serde_as(as = "TryFromInto<Vec<ExtraDataEntry>>")]
193 pub xdata: ExtraData,
194}
195
196impl Display for DbDescFileV2 {
197 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
198 let base: DbDescFileV1 = self.clone().into();
200 write!(f, "{base}")?;
201
202 writeln!(f, "%XDATA%")?;
204 for v in self.xdata.clone() {
205 writeln!(f, "{v}")?;
206 }
207
208 writeln!(f)
209 }
210}
211
212impl FromStr for DbDescFileV2 {
213 type Err = Error;
214
215 fn from_str(s: &str) -> Result<Self, Self::Err> {
281 let sections = sections.parse(s)?;
282 Self::try_from(sections)
283 }
284}
285
286impl TryFrom<Vec<Section>> for DbDescFileV2 {
287 type Error = Error;
288
289 fn try_from(sections: Vec<Section>) -> Result<Self, Self::Error> {
304 let v1 = DbDescFileV1::try_from(sections.clone())?;
306
307 let xdata = if let Some(Section::XData(v)) =
309 sections.iter().find(|s| matches!(s, Section::XData(_)))
310 {
311 v.clone()
312 } else {
313 return Err(Error::MissingSection(SectionKeyword::XData));
314 };
315
316 Ok(Self {
317 name: v1.name,
318 version: v1.version,
319 base: v1.base,
320 description: v1.description,
321 url: v1.url,
322 arch: v1.arch,
323 builddate: v1.builddate,
324 installdate: v1.installdate,
325 packager: v1.packager,
326 size: v1.size,
327 groups: v1.groups,
328 reason: v1.reason,
329 license: v1.license,
330 validation: v1.validation,
331 replaces: v1.replaces,
332 depends: v1.depends,
333 optdepends: v1.optdepends,
334 conflicts: v1.conflicts,
335 provides: v1.provides,
336 xdata,
337 })
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use pretty_assertions::assert_eq;
344 use rstest::*;
345 use testresult::TestResult;
346
347 use super::*;
348
349 const VALID_DESC_FILE: &str = r#"%NAME%
350foo
351
352%VERSION%
3531.0.0-1
354
355%BASE%
356foo
357
358%DESC%
359An example package
360
361%URL%
362https://example.org/
363
364%ARCH%
365x86_64
366
367%BUILDDATE%
3681733737242
369
370%INSTALLDATE%
3711733737243
372
373%PACKAGER%
374Foobar McFooface <foobar@mcfooface.org>
375
376%SIZE%
377123
378
379%GROUPS%
380utils
381cli
382
383%REASON%
3841
385
386%LICENSE%
387MIT
388Apache-2.0
389
390%VALIDATION%
391sha256
392pgp
393
394%REPLACES%
395pkg-old
396
397%DEPENDS%
398glibc
399libwlroots-0.19.so=libwlroots-0.19.so-64
400lib:libexample.so.1
401
402%OPTDEPENDS%
403optpkg
404
405%CONFLICTS%
406foo-old
407
408%PROVIDES%
409foo-virtual
410libwlroots-0.19.so=libwlroots-0.19.so-64
411lib:libexample.so.1
412
413%XDATA%
414pkgtype=pkg
415
416"#;
417
418 #[test]
419 fn parse_valid_v2_desc() -> TestResult {
420 let actual = DbDescFileV2::from_str(VALID_DESC_FILE)?;
421 let expected = DbDescFileV2 {
422 name: Name::new("foo")?,
423 version: FullVersion::from_str("1.0.0-1")?,
424 base: PackageBaseName::new("foo")?,
425 description: PackageDescription::from("An example package"),
426 url: Some(Url::from_str("https://example.org")?),
427 arch: Architecture::from_str("x86_64")?,
428 builddate: BuildDate::from(1733737242),
429 installdate: BuildDate::from(1733737243),
430 packager: Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?,
431 size: 123,
432 groups: vec!["utils".into(), "cli".into()],
433 reason: PackageInstallReason::Depend,
434 license: vec![License::from_str("MIT")?, License::from_str("Apache-2.0")?],
435 validation: vec![
436 PackageValidation::from_str("sha256")?,
437 PackageValidation::from_str("pgp")?,
438 ],
439 replaces: vec![PackageRelation::from_str("pkg-old")?],
440 depends: vec![
441 RelationOrSoname::from_str("glibc")?,
442 RelationOrSoname::from_str("libwlroots-0.19.so=libwlroots-0.19.so-64")?,
443 RelationOrSoname::from_str("lib:libexample.so.1")?,
444 ],
445 optdepends: vec![OptionalDependency::from_str("optpkg")?],
446 conflicts: vec![PackageRelation::from_str("foo-old")?],
447 provides: vec![
448 RelationOrSoname::from_str("foo-virtual")?,
449 RelationOrSoname::from_str("libwlroots-0.19.so=libwlroots-0.19.so-64")?,
450 RelationOrSoname::from_str("lib:libexample.so.1")?,
451 ],
452 xdata: ExtraDataEntry::from_str("pkgtype=pkg")?.try_into()?,
453 };
454 assert_eq!(actual, expected);
455 assert_eq!(VALID_DESC_FILE, actual.to_string());
456 Ok(())
457 }
458
459 #[test]
460 fn depends_and_provides_accept_sonames() -> TestResult {
461 let desc = DbDescFileV2::from_str(VALID_DESC_FILE)?;
462 assert!(matches!(desc.depends[1], RelationOrSoname::SonameV1(_)));
463 assert!(matches!(desc.depends[2], RelationOrSoname::SonameV2(_)));
464 assert!(matches!(desc.provides[1], RelationOrSoname::SonameV1(_)));
465 assert!(matches!(desc.provides[2], RelationOrSoname::SonameV2(_)));
466 Ok(())
467 }
468
469 #[rstest]
470 #[case("%UNKNOWN%\nvalue", "invalid section name")]
471 #[case("%VERSION%\n1.0.0-1\n", "Missing section: %NAME%")]
472 fn invalid_desc_parser(#[case] input: &str, #[case] error_snippet: &str) {
473 let result = DbDescFileV2::from_str(input);
474 assert!(result.is_err());
475 let err = result.unwrap_err();
476 let pretty_error = err.to_string();
477 assert!(
478 pretty_error.contains(error_snippet),
479 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
480 );
481 }
482}