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 Md5Checksum,
20 Name,
21 OptionalDependency,
22 PackageBaseName,
23 PackageDescription,
24 PackageFileName,
25 PackageRelation,
26 Packager,
27 RelationOrSoname,
28 Sha256Checksum,
29 Url,
30};
31use winnow::Parser;
32
33use crate::{
34 Error,
35 desc::{
36 Section,
37 parser::{SectionKeyword, sections},
38 },
39};
40
41#[derive(Clone, Debug, serde::Deserialize, PartialEq, serde::Serialize)]
114#[serde(deny_unknown_fields)]
115#[serde(rename_all = "lowercase")]
116pub struct RepoDescFileV1 {
117 pub file_name: PackageFileName,
119
120 pub name: Name,
122
123 pub base: PackageBaseName,
125
126 pub version: FullVersion,
128
129 pub description: PackageDescription,
133
134 pub groups: Vec<Group>,
138
139 pub compressed_size: CompressedSize,
141
142 pub installed_size: InstalledSize,
146
147 pub md5_checksum: Md5Checksum,
149
150 pub sha256_checksum: Sha256Checksum,
152
153 pub pgp_signature: Base64OpenPGPSignature,
155
156 pub url: Option<Url>,
158
159 pub license: Vec<License>,
163
164 pub arch: Architecture,
166
167 pub build_date: BuildDate,
169
170 pub packager: Packager,
172
173 pub replaces: Vec<PackageRelation>,
177
178 pub conflicts: Vec<PackageRelation>,
182
183 pub provides: Vec<RelationOrSoname>,
187
188 pub dependencies: Vec<RelationOrSoname>,
192
193 pub optional_dependencies: Vec<OptionalDependency>,
197
198 pub make_dependencies: Vec<PackageRelation>,
202
203 pub check_dependencies: Vec<PackageRelation>,
207}
208
209impl Display for RepoDescFileV1 {
210 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
211 fn single<T: Display, W: Write>(f: &mut W, key: &str, val: &T) -> FmtResult {
213 writeln!(f, "%{key}%\n{val}\n")
214 }
215
216 fn section<T: Display, W: Write>(f: &mut W, key: &str, vals: &[T]) -> FmtResult {
218 if vals.is_empty() {
219 return Ok(());
220 }
221 writeln!(f, "%{key}%")?;
222 for v in vals {
223 writeln!(f, "{v}")?;
224 }
225 writeln!(f)
226 }
227
228 single(f, "FILENAME", &self.file_name)?;
229 single(f, "NAME", &self.name)?;
230 single(f, "BASE", &self.base)?;
231 single(f, "VERSION", &self.version)?;
232 if !self.description.as_ref().is_empty() {
233 single(f, "DESC", &self.description)?;
234 }
235 section(f, "GROUPS", &self.groups)?;
236 single(f, "CSIZE", &self.compressed_size)?;
237 single(f, "ISIZE", &self.installed_size)?;
238 single(f, "MD5SUM", &self.md5_checksum)?;
239 single(f, "SHA256SUM", &self.sha256_checksum)?;
240 single(f, "PGPSIG", &self.pgp_signature)?;
241 if let Some(url) = &self.url {
242 single(f, "URL", url)?;
243 }
244 section(f, "LICENSE", &self.license)?;
245 single(f, "ARCH", &self.arch)?;
246 single(f, "BUILDDATE", &self.build_date)?;
247 single(f, "PACKAGER", &self.packager)?;
248 section(f, "REPLACES", &self.replaces)?;
249 section(f, "CONFLICTS", &self.conflicts)?;
250 section(f, "PROVIDES", &self.provides)?;
251 section(f, "DEPENDS", &self.dependencies)?;
252 section(f, "OPTDEPENDS", &self.optional_dependencies)?;
253 section(f, "MAKEDEPENDS", &self.make_dependencies)?;
254 section(f, "CHECKDEPENDS", &self.check_dependencies)?;
255 Ok(())
256 }
257}
258
259impl FromStr for RepoDescFileV1 {
260 type Err = Error;
261
262 fn from_str(s: &str) -> Result<Self, Self::Err> {
337 let sections = sections.parse(s)?;
338 Self::try_from(sections)
339 }
340}
341
342impl TryFrom<Vec<Section>> for RepoDescFileV1 {
343 type Error = Error;
344
345 fn try_from(sections: Vec<Section>) -> Result<Self, Self::Error> {
355 let mut file_name = None;
356 let mut name = None;
357 let mut base = None;
358 let mut version = None;
359 let mut description = None;
360 let mut groups: Vec<Group> = Vec::new();
361 let mut compressed_size = None;
362 let mut installed_size = None;
363 let mut md5_checksum = None;
364 let mut sha256_checksum = None;
365 let mut pgp_signature = None;
366 let mut url = None;
367 let mut license: Vec<License> = Vec::new();
368 let mut arch = None;
369 let mut build_date = None;
370 let mut packager = None;
371 let mut replaces: Vec<PackageRelation> = Vec::new();
372 let mut conflicts: Vec<PackageRelation> = Vec::new();
373 let mut provides: Vec<RelationOrSoname> = Vec::new();
374 let mut dependencies: Vec<RelationOrSoname> = Vec::new();
375 let mut optional_dependencies: Vec<OptionalDependency> = Vec::new();
376 let mut make_dependencies: Vec<PackageRelation> = Vec::new();
377 let mut check_dependencies: Vec<PackageRelation> = Vec::new();
378
379 macro_rules! set_once {
381 ($field:ident, $val:expr, $kw:expr) => {{
382 if $field.is_some() {
383 return Err(Error::DuplicateSection($kw));
384 }
385 $field = Some($val);
386 }};
387 }
388
389 macro_rules! set_vec_once {
392 ($field:ident, $val:expr, $kw:expr) => {{
393 if !$field.is_empty() {
394 return Err(Error::DuplicateSection($kw));
395 }
396 if $val.is_empty() {
397 return Err(Error::EmptySection($kw));
398 }
399 $field = $val;
400 }};
401 }
402
403 for section in sections {
404 match section {
405 Section::Filename(val) => set_once!(file_name, val, SectionKeyword::Filename),
406 Section::Name(val) => set_once!(name, val, SectionKeyword::Name),
407 Section::Base(val) => set_once!(base, val, SectionKeyword::Base),
408 Section::Version(val) => set_once!(version, val, SectionKeyword::Version),
409 Section::Desc(val) => set_once!(description, val, SectionKeyword::Desc),
410 Section::Groups(val) => set_vec_once!(groups, val, SectionKeyword::Groups),
411 Section::CSize(val) => set_once!(compressed_size, val, SectionKeyword::CSize),
412 Section::ISize(val) => set_once!(installed_size, val, SectionKeyword::ISize),
413 Section::Md5Sum(val) => set_once!(md5_checksum, val, SectionKeyword::Md5Sum),
414 Section::Sha256Sum(val) => {
415 set_once!(sha256_checksum, val, SectionKeyword::Sha256Sum)
416 }
417 Section::PgpSig(val) => set_once!(pgp_signature, val, SectionKeyword::PgpSig),
418 Section::Url(val) => set_once!(url, val, SectionKeyword::Url),
419 Section::License(val) => set_vec_once!(license, val, SectionKeyword::License),
420 Section::Arch(val) => set_once!(arch, val, SectionKeyword::Arch),
421 Section::BuildDate(val) => set_once!(build_date, val, SectionKeyword::BuildDate),
422 Section::Packager(val) => set_once!(packager, val, SectionKeyword::Packager),
423 Section::Replaces(val) => set_vec_once!(replaces, val, SectionKeyword::Replaces),
424 Section::Conflicts(val) => set_vec_once!(conflicts, val, SectionKeyword::Conflicts),
425 Section::Provides(val) => set_vec_once!(provides, val, SectionKeyword::Provides),
426 Section::Depends(val) => set_vec_once!(dependencies, val, SectionKeyword::Depends),
427 Section::OptDepends(val) => {
428 set_vec_once!(optional_dependencies, val, SectionKeyword::OptDepends)
429 }
430 Section::MakeDepends(val) => {
431 set_vec_once!(make_dependencies, val, SectionKeyword::MakeDepends)
432 }
433 Section::CheckDepends(val) => {
434 set_vec_once!(check_dependencies, val, SectionKeyword::CheckDepends)
435 }
436 }
437 }
438
439 Ok(RepoDescFileV1 {
440 file_name: file_name.ok_or(Error::MissingSection(SectionKeyword::Filename))?,
441 name: name.ok_or(Error::MissingSection(SectionKeyword::Name))?,
442 base: base.ok_or(Error::MissingSection(SectionKeyword::Base))?,
443 version: version.ok_or(Error::MissingSection(SectionKeyword::Version))?,
444 description: description.unwrap_or_default(),
445 groups,
446 compressed_size: compressed_size.ok_or(Error::MissingSection(SectionKeyword::CSize))?,
447 installed_size: installed_size.ok_or(Error::MissingSection(SectionKeyword::ISize))?,
448 md5_checksum: md5_checksum.ok_or(Error::MissingSection(SectionKeyword::Md5Sum))?,
449 sha256_checksum: sha256_checksum
450 .ok_or(Error::MissingSection(SectionKeyword::Sha256Sum))?,
451 pgp_signature: pgp_signature.ok_or(Error::MissingSection(SectionKeyword::PgpSig))?,
452 url: url.unwrap_or(None),
453 license,
454 arch: arch.ok_or(Error::MissingSection(SectionKeyword::Arch))?,
455 build_date: build_date.ok_or(Error::MissingSection(SectionKeyword::BuildDate))?,
456 packager: packager.ok_or(Error::MissingSection(SectionKeyword::Packager))?,
457 replaces,
458 conflicts,
459 provides,
460 dependencies,
461 optional_dependencies,
462 make_dependencies,
463 check_dependencies,
464 })
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use rstest::*;
471
472 use super::*;
473
474 #[rstest]
475 #[case("%UNKNOWN%\nvalue", "invalid section name")]
476 #[case("%VERSION%\n1.0.0-1\n", "Missing section: %FILENAME%")]
477 fn invalid_desc_parser(#[case] input: &str, #[case] error_snippet: &str) {
478 let result = RepoDescFileV1::from_str(input);
479 assert!(result.is_err());
480 let err = result.unwrap_err();
481 let pretty_error = err.to_string();
482 assert!(
483 pretty_error.contains(error_snippet),
484 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
485 );
486 }
487
488 #[test]
489 fn missing_required_section_should_fail() {
490 let input = "%VERSION%\n1.0.0-1\n";
491 let result = RepoDescFileV1::from_str(input);
492 assert!(matches!(result, Err(Error::MissingSection(s)) if s == SectionKeyword::Filename));
493 }
494}