1use std::{
6 fmt::{Display, Formatter, Result as FmtResult, Write},
7 str::FromStr,
8};
9
10use alpm_types::{
11 Architecture,
12 Base64OpenPGPSignature,
13 BuildDate,
14 CompressedSize,
15 FullVersion,
16 Group,
17 InstalledSize,
18 License,
19 Name,
20 OptionalDependency,
21 PackageBaseName,
22 PackageDescription,
23 PackageFileName,
24 PackageRelation,
25 Packager,
26 RelationOrSoname,
27 Sha256Checksum,
28 Url,
29};
30use winnow::Parser;
31
32use crate::{
33 Error,
34 desc::{
35 RepoDescFileV1,
36 Section,
37 parser::{SectionKeyword, sections},
38 },
39};
40
41#[derive(Clone, Debug, serde::Deserialize, PartialEq, serde::Serialize)]
108#[serde(deny_unknown_fields)]
109#[serde(rename_all = "lowercase")]
110pub struct RepoDescFileV2 {
111 pub file_name: PackageFileName,
113
114 pub name: Name,
116
117 pub base: PackageBaseName,
119
120 pub version: FullVersion,
122
123 pub description: PackageDescription,
127
128 pub groups: Vec<Group>,
132
133 pub compressed_size: CompressedSize,
135
136 pub installed_size: InstalledSize,
140
141 pub sha256_checksum: Sha256Checksum,
143
144 pub pgp_signature: Option<Base64OpenPGPSignature>,
148
149 pub url: Option<Url>,
151
152 pub license: Vec<License>,
156
157 pub arch: Architecture,
159
160 pub build_date: BuildDate,
162
163 pub packager: Packager,
165
166 pub replaces: Vec<PackageRelation>,
170
171 pub conflicts: Vec<PackageRelation>,
175
176 pub provides: Vec<RelationOrSoname>,
180
181 pub dependencies: Vec<RelationOrSoname>,
185
186 pub optional_dependencies: Vec<OptionalDependency>,
190
191 pub make_dependencies: Vec<PackageRelation>,
195
196 pub check_dependencies: Vec<PackageRelation>,
200}
201
202impl Display for RepoDescFileV2 {
203 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
204 fn single<T: Display, W: Write>(f: &mut W, key: &str, val: &T) -> FmtResult {
206 writeln!(f, "%{key}%\n{val}\n")
207 }
208
209 fn section<T: Display, W: Write>(f: &mut W, key: &str, vals: &[T]) -> FmtResult {
211 if vals.is_empty() {
212 return Ok(());
213 }
214 writeln!(f, "%{key}%")?;
215 for v in vals {
216 writeln!(f, "{v}")?;
217 }
218 writeln!(f)
219 }
220
221 single(f, "FILENAME", &self.file_name)?;
222 single(f, "NAME", &self.name)?;
223 single(f, "BASE", &self.base)?;
224 single(f, "VERSION", &self.version)?;
225 if !&self.description.as_ref().is_empty() {
226 single(f, "DESC", &self.description)?;
227 }
228 section(f, "GROUPS", &self.groups)?;
229 single(f, "CSIZE", &self.compressed_size)?;
230 single(f, "ISIZE", &self.installed_size)?;
231 single(f, "SHA256SUM", &self.sha256_checksum)?;
232 if let Some(pgpsig) = &self.pgp_signature {
233 single(f, "PGPSIG", pgpsig)?;
234 }
235 if let Some(url) = &self.url {
236 single(f, "URL", url)?;
237 }
238 section(f, "LICENSE", &self.license)?;
239 single(f, "ARCH", &self.arch)?;
240 single(f, "BUILDDATE", &self.build_date)?;
241 single(f, "PACKAGER", &self.packager)?;
242 section(f, "REPLACES", &self.replaces)?;
243 section(f, "CONFLICTS", &self.conflicts)?;
244 section(f, "PROVIDES", &self.provides)?;
245 section(f, "DEPENDS", &self.dependencies)?;
246 section(f, "OPTDEPENDS", &self.optional_dependencies)?;
247 section(f, "MAKEDEPENDS", &self.make_dependencies)?;
248 section(f, "CHECKDEPENDS", &self.check_dependencies)?;
249 Ok(())
250 }
251}
252
253impl FromStr for RepoDescFileV2 {
254 type Err = Error;
255
256 fn from_str(s: &str) -> Result<Self, Self::Err> {
325 let sections = sections.parse(s)?;
326 Self::try_from(sections)
327 }
328}
329
330impl TryFrom<Vec<Section>> for RepoDescFileV2 {
331 type Error = Error;
332
333 fn try_from(sections: Vec<Section>) -> Result<Self, Self::Error> {
343 let mut file_name = None;
344 let mut name = None;
345 let mut base = None;
346 let mut version = None;
347 let mut description = None;
348 let mut groups: Vec<Group> = Vec::new();
349 let mut compressed_size = None;
350 let mut installed_size = None;
351 let mut sha256_checksum = None;
352 let mut pgp_signature = None;
353 let mut url = None;
354 let mut license: Vec<License> = Vec::new();
355 let mut arch = None;
356 let mut build_date = None;
357 let mut packager = None;
358 let mut replaces: Vec<PackageRelation> = Vec::new();
359 let mut conflicts: Vec<PackageRelation> = Vec::new();
360 let mut provides: Vec<RelationOrSoname> = Vec::new();
361 let mut dependencies: Vec<RelationOrSoname> = Vec::new();
362 let mut optional_dependencies: Vec<OptionalDependency> = Vec::new();
363 let mut make_dependencies: Vec<PackageRelation> = Vec::new();
364 let mut check_dependencies: Vec<PackageRelation> = Vec::new();
365
366 macro_rules! set_once {
368 ($field:ident, $val:expr, $kw:expr) => {{
369 if $field.is_some() {
370 return Err(Error::DuplicateSection($kw));
371 }
372 $field = Some($val);
373 }};
374 }
375
376 macro_rules! set_vec_once {
379 ($field:ident, $val:expr, $kw:expr) => {{
380 if !$field.is_empty() {
381 return Err(Error::DuplicateSection($kw));
382 }
383 if $val.is_empty() {
384 return Err(Error::EmptySection($kw));
385 }
386 $field = $val;
387 }};
388 }
389
390 for section in sections {
391 match section {
392 Section::Filename(val) => set_once!(file_name, val, SectionKeyword::Filename),
393 Section::Name(val) => set_once!(name, val, SectionKeyword::Name),
394 Section::Base(val) => set_once!(base, val, SectionKeyword::Base),
395 Section::Version(val) => set_once!(version, val, SectionKeyword::Version),
396 Section::Desc(val) => set_once!(description, val, SectionKeyword::Desc),
397 Section::Groups(val) => set_vec_once!(groups, val, SectionKeyword::Groups),
398 Section::CSize(val) => set_once!(compressed_size, val, SectionKeyword::CSize),
399 Section::ISize(val) => set_once!(installed_size, val, SectionKeyword::ISize),
400 Section::Sha256Sum(val) => {
401 set_once!(sha256_checksum, val, SectionKeyword::Sha256Sum)
402 }
403 Section::PgpSig(val) => set_once!(pgp_signature, val, SectionKeyword::PgpSig),
404 Section::Url(val) => set_once!(url, val, SectionKeyword::Url),
405 Section::License(val) => set_vec_once!(license, val, SectionKeyword::License),
406 Section::Arch(val) => set_once!(arch, val, SectionKeyword::Arch),
407 Section::BuildDate(val) => set_once!(build_date, val, SectionKeyword::BuildDate),
408 Section::Packager(val) => set_once!(packager, val, SectionKeyword::Packager),
409 Section::Replaces(val) => set_vec_once!(replaces, val, SectionKeyword::Replaces),
410 Section::Conflicts(val) => set_vec_once!(conflicts, val, SectionKeyword::Conflicts),
411 Section::Provides(val) => set_vec_once!(provides, val, SectionKeyword::Provides),
412 Section::Depends(val) => set_vec_once!(dependencies, val, SectionKeyword::Depends),
413 Section::OptDepends(val) => {
414 set_vec_once!(optional_dependencies, val, SectionKeyword::OptDepends)
415 }
416 Section::MakeDepends(val) => {
417 set_vec_once!(make_dependencies, val, SectionKeyword::MakeDepends)
418 }
419 Section::CheckDepends(val) => {
420 set_vec_once!(check_dependencies, val, SectionKeyword::CheckDepends)
421 }
422 Section::Md5Sum(_) => {
423 return Err(Error::InvalidSectionForVersion {
424 section: SectionKeyword::Md5Sum,
425 version: 2,
426 });
427 }
428 }
429 }
430
431 Ok(RepoDescFileV2 {
432 file_name: file_name.ok_or(Error::MissingSection(SectionKeyword::Filename))?,
433 name: name.ok_or(Error::MissingSection(SectionKeyword::Name))?,
434 base: base.ok_or(Error::MissingSection(SectionKeyword::Base))?,
435 version: version.ok_or(Error::MissingSection(SectionKeyword::Version))?,
436 description: description.unwrap_or_default(),
437 groups,
438 compressed_size: compressed_size.ok_or(Error::MissingSection(SectionKeyword::CSize))?,
439 installed_size: installed_size.ok_or(Error::MissingSection(SectionKeyword::ISize))?,
440 sha256_checksum: sha256_checksum
441 .ok_or(Error::MissingSection(SectionKeyword::Sha256Sum))?,
442 pgp_signature,
443 url: url.unwrap_or(None),
444 license,
445 arch: arch.ok_or(Error::MissingSection(SectionKeyword::Arch))?,
446 build_date: build_date.ok_or(Error::MissingSection(SectionKeyword::BuildDate))?,
447 packager: packager.ok_or(Error::MissingSection(SectionKeyword::Packager))?,
448 replaces,
449 conflicts,
450 provides,
451 dependencies,
452 optional_dependencies,
453 make_dependencies,
454 check_dependencies,
455 })
456 }
457}
458
459impl From<RepoDescFileV1> for RepoDescFileV2 {
460 fn from(v1: RepoDescFileV1) -> Self {
466 RepoDescFileV2 {
467 file_name: v1.file_name,
468 name: v1.name,
469 base: v1.base,
470 version: v1.version,
471 description: v1.description,
472 groups: v1.groups,
473 compressed_size: v1.compressed_size,
474 installed_size: v1.installed_size,
475 sha256_checksum: v1.sha256_checksum,
476 pgp_signature: Some(v1.pgp_signature),
477 url: v1.url,
478 license: v1.license,
479 arch: v1.arch,
480 build_date: v1.build_date,
481 packager: v1.packager,
482 replaces: v1.replaces,
483 conflicts: v1.conflicts,
484 provides: v1.provides,
485 dependencies: v1.dependencies,
486 optional_dependencies: v1.optional_dependencies,
487 make_dependencies: v1.make_dependencies,
488 check_dependencies: v1.check_dependencies,
489 }
490 }
491}
492
493#[cfg(test)]
494mod tests {
495 use rstest::*;
496
497 use super::*;
498
499 #[rstest]
500 #[case("%UNKNOWN%\nvalue", "invalid section name")]
501 #[case("%VERSION%\n1.0.0-1\n", "Missing section: %FILENAME%")]
502 fn invalid_desc_parser(#[case] input: &str, #[case] error_snippet: &str) {
503 let result = RepoDescFileV2::from_str(input);
504 assert!(result.is_err());
505 let err = result.unwrap_err();
506 let pretty_error = err.to_string();
507 assert!(
508 pretty_error.contains(error_snippet),
509 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
510 );
511 }
512
513 #[test]
514 fn missing_required_section_should_fail() {
515 let input = "%VERSION%\n1.0.0-1\n";
516 let result = RepoDescFileV2::from_str(input);
517 assert!(matches!(result, Err(Error::MissingSection(s)) if s == SectionKeyword::Filename));
518 }
519}