1use std::{
2 fmt::{Display, Formatter},
3 str::FromStr,
4 string::ToString,
5};
6
7use alpm_parsers::{iter_char_context, iter_str_context};
8use serde::{Deserialize, Serialize};
9use winnow::{
10 ModalResult,
11 Parser,
12 combinator::{alt, cut_err, eof, fail, opt, peek, repeat},
13 error::{StrContext, StrContextValue::*},
14 token::one_of,
15};
16
17use crate::{Architecture, Name, Version, error::Error};
18
19fn option_bool_parser(input: &mut &str) -> ModalResult<bool> {
33 let alphanum = |c: char| c.is_ascii_alphanumeric();
34 let special_first_chars = ['-', '.', '_', '!'];
35 let valid_chars = one_of((alphanum, special_first_chars));
36
37 cut_err(peek(valid_chars))
39 .context(StrContext::Expected(CharLiteral('!')))
40 .context(StrContext::Expected(Description(
41 "ASCII alphanumeric character",
42 )))
43 .context_with(iter_char_context!(special_first_chars))
44 .parse_next(input)?;
45
46 Ok(opt('!').parse_next(input)?.is_none())
47}
48
49fn option_name_parser<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
61 let alphanum = |c: char| c.is_ascii_alphanumeric();
62
63 let special_chars = ['-', '.', '_'];
64 let valid_chars = one_of((alphanum, special_chars));
65 let name = repeat::<_, _, (), _, _>(0.., valid_chars)
66 .take()
67 .parse_next(input)?;
68
69 eof.context(StrContext::Label("character in makepkg option"))
70 .context(StrContext::Expected(Description(
71 "ASCII alphanumeric character",
72 )))
73 .context_with(iter_char_context!(special_chars))
74 .parse_next(input)?;
75
76 Ok(name)
77}
78
79#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
87#[serde(tag = "type", rename_all = "snake_case")]
88pub enum MakepkgOption {
89 BuildEnvironment(BuildEnvironmentOption),
91 Package(PackageOption),
93}
94
95impl MakepkgOption {
96 pub fn parser(input: &mut &str) -> ModalResult<Self> {
105 alt((
106 BuildEnvironmentOption::parser.map(MakepkgOption::BuildEnvironment),
107 PackageOption::parser.map(MakepkgOption::Package),
108 fail.context(StrContext::Label("packaging or build environment option"))
109 .context_with(iter_str_context!([
110 BuildEnvironmentOption::VARIANTS.to_vec(),
111 PackageOption::VARIANTS.to_vec()
112 ])),
113 ))
114 .parse_next(input)
115 }
116}
117
118impl FromStr for MakepkgOption {
119 type Err = Error;
120 fn from_str(s: &str) -> Result<Self, Self::Err> {
122 Ok(Self::parser.parse(s)?)
123 }
124}
125
126impl Display for MakepkgOption {
127 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
128 match self {
129 MakepkgOption::BuildEnvironment(option) => write!(fmt, "{option}"),
130 MakepkgOption::Package(option) => write!(fmt, "{option}"),
131 }
132 }
133}
134
135#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
158#[serde(rename_all = "lowercase")]
159pub enum BuildEnvironmentOption {
160 BuildFlags(bool),
165 Ccache(bool),
167 Check(bool),
169 Color(bool),
171 Distcc(bool),
173 Sign(bool),
175 MakeFlags(bool),
180}
181
182impl BuildEnvironmentOption {
183 pub fn new(option: &str) -> Result<Self, Error> {
189 Self::from_str(option)
190 }
191
192 pub fn name(&self) -> &str {
194 match self {
195 Self::BuildFlags(_) => "buildflags",
196 Self::Ccache(_) => "ccache",
197 Self::Check(_) => "check",
198 Self::Color(_) => "color",
199 Self::Distcc(_) => "distcc",
200 Self::MakeFlags(_) => "makeflags",
201 Self::Sign(_) => "sign",
202 }
203 }
204
205 pub fn on(&self) -> bool {
207 match self {
208 Self::BuildFlags(on)
209 | Self::Ccache(on)
210 | Self::Check(on)
211 | Self::Color(on)
212 | Self::Distcc(on)
213 | Self::MakeFlags(on)
214 | Self::Sign(on) => *on,
215 }
216 }
217
218 const VARIANTS: [&str; 7] = [
219 "buildflags",
220 "ccache",
221 "check",
222 "color",
223 "distcc",
224 "makeflags",
225 "sign",
226 ];
227
228 pub fn parser(input: &mut &str) -> ModalResult<Self> {
236 let on = option_bool_parser.parse_next(input)?;
237 let mut name = option_name_parser.parse_next(input)?;
238
239 let name = alt(BuildEnvironmentOption::VARIANTS)
240 .context(StrContext::Label("makepkg build environment option"))
241 .context_with(iter_str_context!([BuildEnvironmentOption::VARIANTS]))
242 .parse_next(&mut name)?;
243
244 match name {
245 "buildflags" => Ok(Self::BuildFlags(on)),
246 "ccache" => Ok(Self::Ccache(on)),
247 "check" => Ok(Self::Check(on)),
248 "color" => Ok(Self::Color(on)),
249 "distcc" => Ok(Self::Distcc(on)),
250 "makeflags" => Ok(Self::MakeFlags(on)),
251 "sign" => Ok(Self::Sign(on)),
252 _ => unreachable!(),
254 }
255 }
256}
257
258impl FromStr for BuildEnvironmentOption {
259 type Err = Error;
260 fn from_str(s: &str) -> Result<Self, Self::Err> {
268 Ok(Self::parser.parse(s)?)
269 }
270}
271
272impl Display for BuildEnvironmentOption {
273 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
274 write!(fmt, "{}{}", if self.on() { "" } else { "!" }, self.name())
275 }
276}
277
278#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
301#[serde(rename_all = "lowercase")]
302pub enum PackageOption {
303 AutoDeps(bool),
307
308 Debug(bool),
310
311 Docs(bool),
313
314 EmptyDirs(bool),
316
317 Libtool(bool),
319
320 Lto(bool),
322
323 Purge(bool),
325
326 StaticLibs(bool),
328
329 Strip(bool),
331
332 Zipman(bool),
334}
335
336impl PackageOption {
337 pub fn new(option: &str) -> Result<Self, Error> {
343 Self::from_str(option)
344 }
345
346 pub fn name(&self) -> &str {
348 match self {
349 Self::AutoDeps(_) => "autodeps",
350 Self::Debug(_) => "debug",
351 Self::Docs(_) => "docs",
352 Self::EmptyDirs(_) => "emptydirs",
353 Self::Libtool(_) => "libtool",
354 Self::Lto(_) => "lto",
355 Self::Purge(_) => "purge",
356 Self::StaticLibs(_) => "staticlibs",
357 Self::Strip(_) => "strip",
358 Self::Zipman(_) => "zipman",
359 }
360 }
361
362 pub fn on(&self) -> bool {
364 match self {
365 Self::AutoDeps(on)
366 | Self::Debug(on)
367 | Self::Docs(on)
368 | Self::EmptyDirs(on)
369 | Self::Libtool(on)
370 | Self::Lto(on)
371 | Self::Purge(on)
372 | Self::StaticLibs(on)
373 | Self::Strip(on)
374 | Self::Zipman(on) => *on,
375 }
376 }
377
378 const VARIANTS: [&str; 11] = [
379 "autodeps",
380 "debug",
381 "docs",
382 "emptydirs",
383 "libtool",
384 "lto",
385 "debug",
386 "purge",
387 "staticlibs",
388 "strip",
389 "zipman",
390 ];
391
392 pub fn parser(input: &mut &str) -> ModalResult<Self> {
400 let on = option_bool_parser.parse_next(input)?;
401 let mut name = option_name_parser.parse_next(input)?;
402
403 let value = alt(PackageOption::VARIANTS)
404 .context(StrContext::Label("makepkg packaging option"))
405 .context_with(iter_str_context!([PackageOption::VARIANTS]))
406 .parse_next(&mut name)?;
407
408 match value {
409 "autodeps" => Ok(Self::AutoDeps(on)),
410 "debug" => Ok(Self::Debug(on)),
411 "docs" => Ok(Self::Docs(on)),
412 "emptydirs" => Ok(Self::EmptyDirs(on)),
413 "libtool" => Ok(Self::Libtool(on)),
414 "lto" => Ok(Self::Lto(on)),
415 "purge" => Ok(Self::Purge(on)),
416 "staticlibs" => Ok(Self::StaticLibs(on)),
417 "strip" => Ok(Self::Strip(on)),
418 "zipman" => Ok(Self::Zipman(on)),
419 _ => unreachable!(),
421 }
422 }
423}
424
425impl FromStr for PackageOption {
426 type Err = Error;
427 fn from_str(s: &str) -> Result<Self, Self::Err> {
435 Ok(Self::parser.parse(s)?)
436 }
437}
438
439impl Display for PackageOption {
440 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
441 write!(fmt, "{}{}", if self.on() { "" } else { "!" }, self.name())
442 }
443}
444
445#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
462pub struct InstalledPackage {
463 name: Name,
464 version: Version,
465 architecture: Architecture,
466}
467
468impl InstalledPackage {
469 pub fn new(name: Name, version: Version, architecture: Architecture) -> Result<Self, Error> {
471 Ok(InstalledPackage {
472 name,
473 version,
474 architecture,
475 })
476 }
477}
478
479impl FromStr for InstalledPackage {
480 type Err = Error;
481 fn from_str(s: &str) -> Result<InstalledPackage, Self::Err> {
483 const DELIMITER: char = '-';
484 let mut parts = s.rsplitn(4, DELIMITER);
485
486 let architecture = parts.next().ok_or(Error::MissingComponent {
487 component: "architecture",
488 })?;
489 let architecture = architecture.parse()?;
490 let version = {
491 let Some(pkgrel) = parts.next() else {
492 return Err(Error::MissingComponent {
493 component: "pkgrel",
494 })?;
495 };
496 let Some(epoch_pkgver) = parts.next() else {
497 return Err(Error::MissingComponent {
498 component: "epoch_pkgver",
499 })?;
500 };
501 epoch_pkgver.to_string() + "-" + pkgrel
502 };
503 let name = parts
504 .next()
505 .ok_or(Error::MissingComponent { component: "name" })?
506 .to_string();
507
508 Ok(InstalledPackage {
509 name: Name::new(&name)?,
510 version: Version::with_pkgrel(version.as_str())?,
511 architecture,
512 })
513 }
514}
515
516impl Display for InstalledPackage {
517 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
518 write!(fmt, "{}-{}-{}", self.name, self.version, self.architecture)
519 }
520}
521
522#[cfg(test)]
523mod tests {
524 use rstest::rstest;
525
526 use super::*;
527
528 #[rstest]
529 #[case(
530 "!makeflags",
531 MakepkgOption::BuildEnvironment(BuildEnvironmentOption::MakeFlags(false))
532 )]
533 #[case("autodeps", MakepkgOption::Package(PackageOption::AutoDeps(true)))]
534 #[case(
535 "ccache",
536 MakepkgOption::BuildEnvironment(BuildEnvironmentOption::Ccache(true))
537 )]
538 fn makepkg_option(#[case] input: &str, #[case] expected: MakepkgOption) {
539 let result = MakepkgOption::from_str(input).expect("Parser should be successful");
540 assert_eq!(result, expected);
541 }
542
543 #[rstest]
544 #[case(
545 "!somethingelse",
546 concat!(
547 "expected `buildflags`, `ccache`, `check`, `color`, `distcc`, `makeflags`, `sign`, ",
548 "`autodeps`, `debug`, `docs`, `emptydirs`, `libtool`, `lto`, `debug`, `purge`, ",
549 "`staticlibs`, `strip`, `zipman`",
550 )
551 )]
552 #[case(
553 "#somethingelse",
554 "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
555 )]
556 fn invalid_makepkg_option(#[case] input: &str, #[case] err_snippet: &str) {
557 let Err(Error::ParseError(err_msg)) = MakepkgOption::from_str(input) else {
558 panic!("'{input}' erroneously parsed as VersionRequirement")
559 };
560 assert!(
561 err_msg.contains(err_snippet),
562 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
563 );
564 }
565
566 #[rstest]
567 #[case("autodeps", PackageOption::AutoDeps(true))]
568 #[case("debug", PackageOption::Debug(true))]
569 #[case("docs", PackageOption::Docs(true))]
570 #[case("emptydirs", PackageOption::EmptyDirs(true))]
571 #[case("!libtool", PackageOption::Libtool(false))]
572 #[case("lto", PackageOption::Lto(true))]
573 #[case("purge", PackageOption::Purge(true))]
574 #[case("staticlibs", PackageOption::StaticLibs(true))]
575 #[case("strip", PackageOption::Strip(true))]
576 #[case("zipman", PackageOption::Zipman(true))]
577 fn package_option(#[case] s: &str, #[case] expected: PackageOption) {
578 let result = PackageOption::from_str(s).expect("Parser should be successful");
579 assert_eq!(result, expected);
580 }
581
582 #[rstest]
583 #[case(
584 "!somethingelse",
585 "expected `autodeps`, `debug`, `docs`, `emptydirs`, `libtool`, `lto`, `debug`, `purge`, `staticlibs`, `strip`, `zipman`"
586 )]
587 #[case(
588 "#somethingelse",
589 "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
590 )]
591 fn invalid_package_option(#[case] input: &str, #[case] err_snippet: &str) {
592 let Err(Error::ParseError(err_msg)) = PackageOption::from_str(input) else {
593 panic!("'{input}' erroneously parsed as VersionRequirement")
594 };
595 assert!(
596 err_msg.contains(err_snippet),
597 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
598 );
599 }
600
601 #[rstest]
602 #[case("buildflags", BuildEnvironmentOption::BuildFlags(true))]
603 #[case("ccache", BuildEnvironmentOption::Ccache(true))]
604 #[case("check", BuildEnvironmentOption::Check(true))]
605 #[case("color", BuildEnvironmentOption::Color(true))]
606 #[case("distcc", BuildEnvironmentOption::Distcc(true))]
607 #[case("!makeflags", BuildEnvironmentOption::MakeFlags(false))]
608 #[case("sign", BuildEnvironmentOption::Sign(true))]
609 #[case("!sign", BuildEnvironmentOption::Sign(false))]
610 fn build_environment_option(#[case] input: &str, #[case] expected: BuildEnvironmentOption) {
611 let result = BuildEnvironmentOption::from_str(input).expect("Parser should be successful");
612 assert_eq!(result, expected);
613 }
614
615 #[rstest]
616 #[case(
617 "!somethingelse",
618 "expected `buildflags`, `ccache`, `check`, `color`, `distcc`, `makeflags`, `sign`"
619 )]
620 #[case(
621 "#somethingelse",
622 "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
623 )]
624 fn invalid_build_environment_option(#[case] input: &str, #[case] err_snippet: &str) {
625 let Err(Error::ParseError(err_msg)) = BuildEnvironmentOption::from_str(input) else {
626 panic!("'{input}' erroneously parsed as VersionRequirement")
627 };
628 assert!(
629 err_msg.contains(err_snippet),
630 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
631 );
632 }
633
634 #[rstest]
635 #[case("#test", "invalid character in makepkg option")]
636 #[case("test!", "invalid character in makepkg option")]
637 fn invalid_option(#[case] input: &str, #[case] error_snippet: &str) {
638 let result = option_name_parser.parse(input);
639 assert!(result.is_err(), "Expected makepkg option parsing to fail");
640 let err = result.unwrap_err();
641 let pretty_error = err.to_string();
642 assert!(
643 pretty_error.contains(error_snippet),
644 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
645 );
646 }
647
648 #[rstest]
649 #[case(
650 "foo-bar-1:1.0.0-1-any",
651 Ok(InstalledPackage{
652 name: Name::new("foo-bar").unwrap(),
653 version: Version::from_str("1:1.0.0-1").unwrap(),
654 architecture: Architecture::Any,
655 }),
656 )]
657 #[case("foo-bar-1:1.0.0-1", Err(strum::ParseError::VariantNotFound.into()))]
658 #[case("1:1.0.0-1-any", Err(Error::MissingComponent { component: "name" }))]
659 fn installed_new(#[case] s: &str, #[case] result: Result<InstalledPackage, Error>) {
660 assert_eq!(InstalledPackage::from_str(s), result);
661 }
662
663 #[rstest]
664 #[case("foo-1:1.0.0-bar-any", "invalid package release")]
665 #[case("packagename-30-0.1oops-any", "expected end of package release value")]
666 #[case("package$with$dollars-30-0.1-any", "invalid character in package name")]
667 fn installed_new_parse_error(#[case] input: &str, #[case] error_snippet: &str) {
668 let result = InstalledPackage::from_str(input);
669 assert!(result.is_err(), "Expected InstalledPackage parsing to fail");
670 let err = result.unwrap_err();
671 let pretty_error = err.to_string();
672 assert!(
673 pretty_error.contains(error_snippet),
674 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
675 );
676 }
677}