1use std::{
2 fmt::{Display, Formatter},
3 str::FromStr,
4};
5
6use alpm_parsers::{
7 iter_char_context,
8 iter_str_context,
9 traits::{AlpmParser, ParserUntil},
10};
11#[cfg(feature = "serde")]
12use serde::{Deserialize, Serialize};
13use strum::VariantNames;
14use winnow::{
15 ModalResult,
16 Parser,
17 combinator::{alt, cut_err, fail, opt, peek, repeat, repeat_till},
18 error::{
19 AddContext,
20 ContextError,
21 ErrMode,
22 ParserError,
23 StrContext,
24 StrContextValue::{self, *},
25 },
26 stream::Stream,
27 token::{any, one_of},
28};
29
30use crate::{
31 Architecture,
32 FullVersion,
33 Name,
34 PackageFileName,
35 PackageRelation,
36 VersionComparison,
37 VersionRequirement,
38 error::Error,
39};
40
41fn option_bool_parser(input: &mut &str) -> ModalResult<bool> {
55 let alphanum = |c: char| c.is_ascii_alphanumeric();
56 let special_first_chars = ['-', '.', '_', '!'];
57 let valid_chars = one_of((alphanum, special_first_chars));
58
59 cut_err(peek(valid_chars))
61 .context(StrContext::Expected(CharLiteral('!')))
62 .context(StrContext::Expected(Description(
63 "ASCII alphanumeric character",
64 )))
65 .context_with(iter_char_context!(special_first_chars))
66 .parse_next(input)?;
67
68 Ok(opt('!').parse_next(input)?.is_none())
69}
70
71pub(crate) static SPECIAL_OPTION_CHARS: [char; 3] = ['-', '.', '_'];
80
81fn option_name_parser<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
93 let alphanum = |c: char| c.is_ascii_alphanumeric();
94
95 let valid_chars = one_of((alphanum, SPECIAL_OPTION_CHARS));
96 let name = repeat::<_, _, (), _, _>(0.., valid_chars)
97 .take()
98 .parse_next(input)?;
99
100 Ok(name)
101}
102
103#[derive(Clone, Debug, Eq, PartialEq)]
111#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
112#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "snake_case"))]
113pub enum MakepkgOption {
114 BuildEnvironment(BuildEnvironmentOption),
116 Package(PackageOption),
118}
119
120impl AlpmParser for MakepkgOption {
121 fn parser(input: &mut &str) -> ModalResult<Self> {
128 alt((
129 BuildEnvironmentOption::parser.map(MakepkgOption::BuildEnvironment),
130 PackageOption::parser.map(MakepkgOption::Package),
131 fail.context(StrContext::Label("packaging or build environment option"))
132 .context_with(iter_str_context!([
133 BuildEnvironmentOption::VARIANTS.to_vec(),
134 PackageOption::VARIANTS.to_vec()
135 ])),
136 ))
137 .parse_next(input)
138 }
139
140 fn delimiter_error_context<'a, O, P>(
141 parser: P,
142 ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
143 where
144 P: Parser<&'a str, O, ErrMode<ContextError>>,
145 {
146 parser
147 .context(StrContext::Label("makepkg option"))
148 .context(StrContext::Expected(StrContextValue::Description(
149 "string consisting of alphanumeric characters or",
150 )))
151 .context_with(iter_char_context!(SPECIAL_OPTION_CHARS))
152 }
153}
154
155impl FromStr for MakepkgOption {
156 type Err = Error;
157 fn from_str(s: &str) -> Result<Self, Self::Err> {
159 Ok(Self::parser.parse(s)?)
160 }
161}
162
163impl Display for MakepkgOption {
164 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
165 match self {
166 MakepkgOption::BuildEnvironment(option) => write!(fmt, "{option}"),
167 MakepkgOption::Package(option) => write!(fmt, "{option}"),
168 }
169 }
170}
171
172#[derive(Clone, Debug, Eq, PartialEq, VariantNames)]
195#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
196#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
197pub enum BuildEnvironmentOption {
198 #[strum(serialize = "buildflags")]
203 BuildFlags(bool),
204 #[strum(serialize = "ccache")]
206 Ccache(bool),
207 #[strum(serialize = "check")]
209 Check(bool),
210 #[strum(serialize = "color")]
212 Color(bool),
213 #[strum(serialize = "distcc")]
215 Distcc(bool),
216 #[strum(serialize = "sign")]
218 Sign(bool),
219 #[strum(serialize = "makeflags")]
224 MakeFlags(bool),
225}
226
227impl BuildEnvironmentOption {
228 pub fn new(option: &str) -> Result<Self, Error> {
234 Self::from_str(option)
235 }
236
237 pub fn name(&self) -> &str {
239 match self {
240 Self::BuildFlags(_) => "buildflags",
241 Self::Ccache(_) => "ccache",
242 Self::Check(_) => "check",
243 Self::Color(_) => "color",
244 Self::Distcc(_) => "distcc",
245 Self::MakeFlags(_) => "makeflags",
246 Self::Sign(_) => "sign",
247 }
248 }
249
250 pub fn on(&self) -> bool {
252 match self {
253 Self::BuildFlags(on)
254 | Self::Ccache(on)
255 | Self::Check(on)
256 | Self::Color(on)
257 | Self::Distcc(on)
258 | Self::MakeFlags(on)
259 | Self::Sign(on) => *on,
260 }
261 }
262}
263
264impl AlpmParser for BuildEnvironmentOption {
265 fn parser(input: &mut &str) -> ModalResult<Self> {
271 let on = option_bool_parser.parse_next(input)?;
272 let mut name = option_name_parser.parse_next(input)?;
273
274 alt((
275 "buildflags".value(Self::BuildFlags(on)),
276 "ccache".value(Self::Ccache(on)),
277 "check".value(Self::Check(on)),
278 "color".value(Self::Color(on)),
279 "distcc".value(Self::Distcc(on)),
280 "makeflags".value(Self::MakeFlags(on)),
281 "sign".value(Self::Sign(on)),
282 fail.context(StrContext::Label("makepkg build environment option"))
283 .context_with(iter_str_context!([BuildEnvironmentOption::VARIANTS])),
284 ))
285 .parse_next(&mut name)
286 }
287
288 fn delimiter_error_context<'a, O, P>(
289 parser: P,
290 ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
291 where
292 P: Parser<&'a str, O, ErrMode<ContextError>>,
293 {
294 parser
295 .context(StrContext::Label("build environment option"))
296 .context(StrContext::Expected(StrContextValue::Description(
297 "string consisting of alphanumeric characters or",
298 )))
299 .context_with(iter_char_context!(SPECIAL_OPTION_CHARS))
300 }
301}
302
303impl FromStr for BuildEnvironmentOption {
304 type Err = Error;
305 fn from_str(s: &str) -> Result<Self, Self::Err> {
313 Ok(Self::parser_until_eof.parse(s)?)
314 }
315}
316
317impl Display for BuildEnvironmentOption {
318 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
319 write!(fmt, "{}{}", if self.on() { "" } else { "!" }, self.name())
320 }
321}
322
323#[derive(Clone, Debug, Eq, PartialEq, VariantNames)]
346#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
347#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
348pub enum PackageOption {
349 #[strum(serialize = "autodeps")]
353 AutoDeps(bool),
354
355 #[strum(serialize = "debug")]
357 Debug(bool),
358
359 #[strum(serialize = "docs")]
361 Docs(bool),
362
363 #[strum(serialize = "emptydirs")]
365 EmptyDirs(bool),
366
367 #[strum(serialize = "libtool")]
369 Libtool(bool),
370
371 #[strum(serialize = "lto")]
373 Lto(bool),
374
375 #[strum(serialize = "pestrip")]
377 PEStrip(bool),
378
379 #[strum(serialize = "purge")]
381 Purge(bool),
382
383 #[strum(serialize = "staticlibs")]
385 StaticLibs(bool),
386
387 #[strum(serialize = "strip")]
389 Strip(bool),
390
391 #[strum(serialize = "zipman")]
393 Zipman(bool),
394}
395
396impl PackageOption {
397 pub fn new(option: &str) -> Result<Self, Error> {
403 Self::from_str(option)
404 }
405
406 pub fn name(&self) -> &str {
408 match self {
409 Self::AutoDeps(_) => "autodeps",
410 Self::Debug(_) => "debug",
411 Self::Docs(_) => "docs",
412 Self::EmptyDirs(_) => "emptydirs",
413 Self::Libtool(_) => "libtool",
414 Self::Lto(_) => "lto",
415 Self::PEStrip(_) => "pestrip",
416 Self::Purge(_) => "purge",
417 Self::StaticLibs(_) => "staticlibs",
418 Self::Strip(_) => "strip",
419 Self::Zipman(_) => "zipman",
420 }
421 }
422
423 pub fn on(&self) -> bool {
425 match self {
426 Self::AutoDeps(on)
427 | Self::Debug(on)
428 | Self::Docs(on)
429 | Self::EmptyDirs(on)
430 | Self::Libtool(on)
431 | Self::Lto(on)
432 | Self::Purge(on)
433 | Self::PEStrip(on)
434 | Self::StaticLibs(on)
435 | Self::Strip(on)
436 | Self::Zipman(on) => *on,
437 }
438 }
439}
440
441impl AlpmParser for PackageOption {
442 fn parser(input: &mut &str) -> ModalResult<Self> {
448 let on = option_bool_parser.parse_next(input)?;
449 let mut name = option_name_parser.parse_next(input)?;
450
451 alt((
452 alt((
453 "autodeps".value(Self::AutoDeps(on)),
454 "debug".value(Self::Debug(on)),
455 "docs".value(Self::Docs(on)),
456 "emptydirs".value(Self::EmptyDirs(on)),
457 "libtool".value(Self::Libtool(on)),
458 "lto".value(Self::Lto(on)),
459 "pestrip".value(Self::PEStrip(on)),
460 "purge".value(Self::Purge(on)),
461 "staticlibs".value(Self::StaticLibs(on)),
462 )),
463 alt((
464 "strip".value(Self::Strip(on)),
465 "zipman".value(Self::Zipman(on)),
466 )),
467 fail.context(StrContext::Label("makepkg packaging option"))
468 .context_with(iter_str_context!([PackageOption::VARIANTS])),
469 ))
470 .parse_next(&mut name)
471 }
472
473 fn delimiter_error_context<'a, O, P>(
474 parser: P,
475 ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
476 where
477 P: Parser<&'a str, O, ErrMode<ContextError>>,
478 {
479 parser
480 .context(StrContext::Label("package option"))
481 .context(StrContext::Expected(StrContextValue::Description(
482 "string consisting of alphanumeric characters or",
483 )))
484 .context_with(iter_char_context!(SPECIAL_OPTION_CHARS))
485 }
486}
487
488impl FromStr for PackageOption {
489 type Err = Error;
490 fn from_str(s: &str) -> Result<Self, Self::Err> {
498 Ok(Self::parser_until_eof.parse(s)?)
499 }
500}
501
502impl Display for PackageOption {
503 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
504 write!(fmt, "{}{}", if self.on() { "" } else { "!" }, self.name())
505 }
506}
507
508#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
539#[cfg_attr(feature = "serde", derive(Serialize))]
540pub struct InstalledPackage {
541 name: Name,
542 version: FullVersion,
543 architecture: Architecture,
544}
545
546impl InstalledPackage {
547 pub fn new(name: Name, version: FullVersion, architecture: Architecture) -> Self {
566 Self {
567 name,
568 version,
569 architecture,
570 }
571 }
572
573 pub fn name(&self) -> &Name {
591 &self.name
592 }
593
594 pub fn version(&self) -> &FullVersion {
612 &self.version
613 }
614
615 pub fn architecture(&self) -> &Architecture {
633 &self.architecture
634 }
635
636 pub fn to_package_relation(&self) -> PackageRelation {
657 PackageRelation {
658 name: self.name.clone(),
659 version_requirement: Some(VersionRequirement {
660 comparison: VersionComparison::Equal,
661 version: self.version.clone().into(),
662 }),
663 }
664 }
665}
666
667impl ParserUntil for InstalledPackage {
668 fn parser_until<'a, P>(delimiter: P) -> impl Parser<&'a str, Self, ErrMode<ContextError>>
695 where
696 P: Parser<&'a str, &'a str, ErrMode<ContextError>>,
697 {
698 let mut delimiter_parser = delimiter;
701 move |input: &mut &'a str| -> ModalResult<Self> {
702 let checkpoint = input.checkpoint();
713 let dashes: usize =
714 repeat_till::<_, _, (), _, _, _, _>(0.., any, peek(delimiter_parser.by_ref()))
715 .take()
716 .map(|s| {
717 s.chars().fold(0, |acc, char| {
718 if char == '-' {
719 return acc + 1;
720 }
721 acc
722 })
723 })
724 .parse_next(input)?;
725 input.reset(&checkpoint);
726
727 if dashes < 2 {
728 let context_error = ContextError::from_input(input)
729 .add_context(
730 input,
731 &input.checkpoint(),
732 StrContext::Label("alpm-package file name"),
733 )
734 .add_context(
735 input,
736 &input.checkpoint(),
737 StrContext::Expected(StrContextValue::Description(
738 concat!(
739 "a package name, followed by an alpm-package-version (full or full with epoch) and an alpm-architecture.",
740 "\nAll components must be delimited with a dash ('-')."
741 )
742 ))
743 );
744
745 return Err(ErrMode::Backtrack(context_error));
746 }
747
748 let dashes_till_version = dashes.saturating_sub(2);
750
751 let name = Name::parse_name_followed_by_version(dashes_till_version)
755 .context(StrContext::Label("alpm-package-name"))
756 .parse_next(input)?;
757
758 "-".parse_next(input)?;
761
762 let version: FullVersion = FullVersion::parser
765 .context(StrContext::Label("alpm-package-version"))
766 .context(StrContext::Expected(StrContextValue::Description(
767 "an alpm-package-version (full or full with epoch) followed by a `-` and an alpm-architecture",
768 )))
769 .parse_next(input)?;
770
771 "-".context(StrContext::Label("alpm-package file name"))
774 .context(StrContext::Expected(StrContextValue::Description(
775 "expected a `-` followed by an alpm-architecture",
776 )))
777 .parse_next(input)?;
778
779 let architecture =
781 Architecture::parser_until(delimiter_parser.by_ref()).parse_next(input)?;
782
783 Ok(Self {
784 name,
785 version,
786 architecture,
787 })
788 }
789 }
790}
791
792impl From<PackageFileName> for InstalledPackage {
793 fn from(value: PackageFileName) -> Self {
795 Self {
796 name: value.name,
797 version: value.version,
798 architecture: value.architecture,
799 }
800 }
801}
802
803impl FromStr for InstalledPackage {
804 type Err = Error;
805
806 fn from_str(s: &str) -> Result<InstalledPackage, Self::Err> {
828 Ok(Self::parser_until_eof.parse(s)?)
829 }
830}
831
832impl Display for InstalledPackage {
833 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
834 write!(fmt, "{}-{}-{}", self.name, self.version, self.architecture)
835 }
836}
837
838#[cfg(test)]
839mod tests {
840 use insta::assert_snapshot;
841 use rstest::rstest;
842 use testresult::TestResult;
843
844 use super::*;
845 use crate::{SystemArchitecture, configure_insta};
846
847 #[rstest]
848 #[case(
849 "!makeflags",
850 MakepkgOption::BuildEnvironment(BuildEnvironmentOption::MakeFlags(false))
851 )]
852 #[case("autodeps", MakepkgOption::Package(PackageOption::AutoDeps(true)))]
853 #[case(
854 "ccache",
855 MakepkgOption::BuildEnvironment(BuildEnvironmentOption::Ccache(true))
856 )]
857 fn makepkg_option(#[case] input: &str, #[case] expected: MakepkgOption) {
858 let result = MakepkgOption::from_str(input).expect("Parser should be successful");
859 assert_eq!(result, expected);
860 }
861
862 #[rstest]
863 #[case("!somethingelse")]
864 #[case("#somethingelse")]
865 fn invalid_makepkg_option(#[case] input: &str) {
866 let Err(Error::ParseError(err_msg)) = MakepkgOption::from_str(input) else {
867 panic!("'{input}' erroneously parsed as MakepkgOption")
868 };
869
870 let (test_name, _guard) = configure_insta();
871 assert_snapshot!(test_name, err_msg.to_string());
872 }
873
874 #[rstest]
875 #[case("autodeps", PackageOption::AutoDeps(true))]
876 #[case("debug", PackageOption::Debug(true))]
877 #[case("docs", PackageOption::Docs(true))]
878 #[case("emptydirs", PackageOption::EmptyDirs(true))]
879 #[case("!libtool", PackageOption::Libtool(false))]
880 #[case("lto", PackageOption::Lto(true))]
881 #[case("pestrip", PackageOption::PEStrip(true))]
882 #[case("purge", PackageOption::Purge(true))]
883 #[case("staticlibs", PackageOption::StaticLibs(true))]
884 #[case("strip", PackageOption::Strip(true))]
885 #[case("zipman", PackageOption::Zipman(true))]
886 fn package_option(#[case] s: &str, #[case] expected: PackageOption) {
887 let result = PackageOption::from_str(s).expect("Parser should be successful");
888 assert_eq!(result, expected);
889 }
890
891 #[rstest]
892 #[case("!somethingelse")]
893 #[case("#somethingelse")]
894 fn invalid_package_option(#[case] input: &str) {
895 let Err(Error::ParseError(err_msg)) = PackageOption::from_str(input) else {
896 panic!("'{input}' erroneously parsed as PackageOption")
897 };
898
899 let (test_name, _guard) = configure_insta();
900 assert_snapshot!(test_name, err_msg.to_string());
901 }
902
903 #[rstest]
904 #[case("buildflags", BuildEnvironmentOption::BuildFlags(true))]
905 #[case("ccache", BuildEnvironmentOption::Ccache(true))]
906 #[case("check", BuildEnvironmentOption::Check(true))]
907 #[case("color", BuildEnvironmentOption::Color(true))]
908 #[case("distcc", BuildEnvironmentOption::Distcc(true))]
909 #[case("!makeflags", BuildEnvironmentOption::MakeFlags(false))]
910 #[case("sign", BuildEnvironmentOption::Sign(true))]
911 #[case("!sign", BuildEnvironmentOption::Sign(false))]
912 fn build_environment_option(#[case] input: &str, #[case] expected: BuildEnvironmentOption) {
913 let result = BuildEnvironmentOption::from_str(input).expect("Parser should be successful");
914 assert_eq!(result, expected);
915 }
916
917 #[rstest]
918 #[case("!somethingelse")]
919 #[case("#somethingelse")]
920 fn invalid_build_environment_option(#[case] input: &str) {
921 let Err(Error::ParseError(err_msg)) = BuildEnvironmentOption::from_str(input) else {
922 panic!("'{input}' erroneously parsed as BuildEnvironmentOption")
923 };
924
925 let (test_name, _guard) = configure_insta();
926 assert_snapshot!(test_name, err_msg.to_string());
927 }
928
929 #[rstest]
930 #[case(
931 "foo-bar-1:1.0.0-1-any",
932 InstalledPackage {
933 name: Name::new("foo-bar")?,
934 version: FullVersion::from_str("1:1.0.0-1")?,
935 architecture: Architecture::Any,
936 },
937 )]
938 #[case(
939 "foobar-1.0.0-1-x86_64",
940 InstalledPackage {
941 name: Name::new("foobar")?,
942 version: FullVersion::from_str("1.0.0-1")?,
943 architecture: SystemArchitecture::X86_64.into(),
944 },
945 )]
946 fn installed_from_str(#[case] s: &str, #[case] result: InstalledPackage) -> TestResult {
947 assert_eq!(InstalledPackage::from_str(s), Ok(result));
948 Ok(())
949 }
950
951 #[rstest]
952 #[case("foo-1:1.0.0-bar-any")]
953 #[case("foo-1:1.0.0_any")]
954 #[case("packagename-30-0.1oops-any")]
955 #[case("package$with$dollars-30-0.1-any")]
956 #[case("packagename-30-0.1-any*asdf")]
957 fn installed_new_parse_error(#[case] input: &str) {
958 let Err(Error::ParseError(err_msg)) = InstalledPackage::from_str(input) else {
959 panic!("'{input}' erroneously parsed as InstalledPackage")
960 };
961
962 let (test_name, _guard) = configure_insta();
963 assert_snapshot!(test_name, err_msg.to_string());
964 }
965}