1use std::{
2 fmt::{Display, Formatter},
3 str::FromStr,
4};
5
6use alpm_parsers::prelude::*;
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9use strum::VariantNames;
10use winnow::{
11 combinator::{alt, cut_err, fail, opt, peek, repeat, repeat_till},
12 error::ErrMode,
13 stream::Stream,
14 token::{any, one_of},
15};
16
17use crate::{
18 Architecture,
19 FullVersion,
20 Name,
21 PackageFileName,
22 PackageRelation,
23 VersionComparison,
24 VersionRequirement,
25 error::Error,
26};
27
28fn option_bool_parser<'a>(input: &mut Input<'a>) -> PResult<'a, bool> {
42 let alphanum = |c: char| c.is_ascii_alphanumeric();
43 let special_first_chars = ['-', '.', '_', '!'];
44 let valid_chars = one_of((alphanum, special_first_chars));
45
46 cut_err(peek(valid_chars))
48 .expected_char('!')
49 .expected_text("ASCII alphanumeric character")
50 .expected_chars(special_first_chars)
51 .parse_next(input)?;
52
53 Ok(opt('!').parse_next(input)?.is_none())
54}
55
56pub(crate) static SPECIAL_OPTION_CHARS: [char; 3] = ['-', '.', '_'];
65
66fn option_name_parser<'s>(input: &mut Input<'s>) -> PResult<'s, &'s str> {
78 let alphanum = |c: char| c.is_ascii_alphanumeric();
79
80 let valid_chars = one_of((alphanum, SPECIAL_OPTION_CHARS));
81 let name = repeat::<_, _, (), _, _>(0.., valid_chars)
82 .take()
83 .parse_next(input)?;
84
85 Ok(name)
86}
87
88#[derive(Clone, Debug, Eq, PartialEq)]
96#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
97#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "snake_case"))]
98pub enum MakepkgOption {
99 BuildEnvironment(BuildEnvironmentOption),
101 Package(PackageOption),
103}
104
105impl AlpmParser for MakepkgOption {
106 fn parser<'a>(input: &mut Input<'a>) -> PResult<'a, Self> {
113 alt((
114 BuildEnvironmentOption::parser.map(MakepkgOption::BuildEnvironment),
115 PackageOption::parser.map(MakepkgOption::Package),
116 fail.label("packaging or build environment option")
117 .expected_strings(
118 [BuildEnvironmentOption::VARIANTS, PackageOption::VARIANTS]
119 .into_iter()
120 .flatten(),
121 ),
122 ))
123 .layer("makepkg option")
124 .parse_next(input)
125 }
126
127 fn delimiter_error_context<'a, O, P>(
128 parser: P,
129 ) -> impl Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>
130 where
131 P: Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>,
132 {
133 parser
134 .expected_text("string consisting of alphanumeric characters or")
135 .expected_chars(SPECIAL_OPTION_CHARS)
136 .layer("makepkg option")
137 }
138}
139
140impl FromStr for MakepkgOption {
141 type Err = Error;
142 fn from_str(s: &str) -> Result<Self, Self::Err> {
144 Ok(Self::parser.parse(Input::new(s))?)
145 }
146}
147
148impl Display for MakepkgOption {
149 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
150 match self {
151 MakepkgOption::BuildEnvironment(option) => write!(fmt, "{option}"),
152 MakepkgOption::Package(option) => write!(fmt, "{option}"),
153 }
154 }
155}
156
157#[derive(Clone, Debug, Eq, PartialEq, VariantNames)]
180#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
181#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
182pub enum BuildEnvironmentOption {
183 #[strum(serialize = "buildflags")]
188 BuildFlags(bool),
189 #[strum(serialize = "ccache")]
191 Ccache(bool),
192 #[strum(serialize = "check")]
194 Check(bool),
195 #[strum(serialize = "color")]
197 Color(bool),
198 #[strum(serialize = "distcc")]
200 Distcc(bool),
201 #[strum(serialize = "sign")]
203 Sign(bool),
204 #[strum(serialize = "makeflags")]
209 MakeFlags(bool),
210}
211
212impl BuildEnvironmentOption {
213 pub fn new(option: &str) -> Result<Self, Error> {
219 Self::from_str(option)
220 }
221
222 pub fn name(&self) -> &str {
224 match self {
225 Self::BuildFlags(_) => "buildflags",
226 Self::Ccache(_) => "ccache",
227 Self::Check(_) => "check",
228 Self::Color(_) => "color",
229 Self::Distcc(_) => "distcc",
230 Self::MakeFlags(_) => "makeflags",
231 Self::Sign(_) => "sign",
232 }
233 }
234
235 pub fn on(&self) -> bool {
237 match self {
238 Self::BuildFlags(on)
239 | Self::Ccache(on)
240 | Self::Check(on)
241 | Self::Color(on)
242 | Self::Distcc(on)
243 | Self::MakeFlags(on)
244 | Self::Sign(on) => *on,
245 }
246 }
247}
248
249impl AlpmParser for BuildEnvironmentOption {
250 fn parser<'a>(input: &mut Input<'a>) -> PResult<'a, Self> {
256 let parser = move |input: &mut Input<'a>| -> PResult<'a, Self> {
257 let on = option_bool_parser.parse_next(input)?;
258 let name = option_name_parser.parse_next(input)?;
259
260 alt((
261 "buildflags".value(Self::BuildFlags(on)),
262 "ccache".value(Self::Ccache(on)),
263 "check".value(Self::Check(on)),
264 "color".value(Self::Color(on)),
265 "distcc".value(Self::Distcc(on)),
266 "makeflags".value(Self::MakeFlags(on)),
267 "sign".value(Self::Sign(on)),
268 ))
269 .expected_strings(BuildEnvironmentOption::VARIANTS)
270 .parse_next(&mut Input::new(name))
271 };
272
273 parser.layer("build environment option").parse_next(input)
274 }
275
276 fn delimiter_error_context<'a, O, P>(
277 parser: P,
278 ) -> impl Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>
279 where
280 P: Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>,
281 {
282 parser
283 .expected_text("string consisting of alphanumeric characters or")
284 .layer("build environment option")
285 .expected_chars(SPECIAL_OPTION_CHARS)
286 }
287}
288
289impl FromStr for BuildEnvironmentOption {
290 type Err = Error;
291 fn from_str(s: &str) -> Result<Self, Self::Err> {
299 Ok(Self::parser_until_eof.parse(Input::new(s))?)
300 }
301}
302
303impl Display for BuildEnvironmentOption {
304 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
305 write!(fmt, "{}{}", if self.on() { "" } else { "!" }, self.name())
306 }
307}
308
309#[derive(Clone, Debug, Eq, PartialEq, VariantNames)]
332#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
333#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
334pub enum PackageOption {
335 #[strum(serialize = "autodeps")]
339 AutoDeps(bool),
340
341 #[strum(serialize = "debug")]
343 Debug(bool),
344
345 #[strum(serialize = "docs")]
347 Docs(bool),
348
349 #[strum(serialize = "emptydirs")]
351 EmptyDirs(bool),
352
353 #[strum(serialize = "libtool")]
355 Libtool(bool),
356
357 #[strum(serialize = "lto")]
359 Lto(bool),
360
361 #[strum(serialize = "pestrip")]
363 PEStrip(bool),
364
365 #[strum(serialize = "purge")]
367 Purge(bool),
368
369 #[strum(serialize = "staticlibs")]
371 StaticLibs(bool),
372
373 #[strum(serialize = "strip")]
375 Strip(bool),
376
377 #[strum(serialize = "zipman")]
379 Zipman(bool),
380}
381
382impl PackageOption {
383 pub fn new(option: &str) -> Result<Self, Error> {
389 Self::from_str(option)
390 }
391
392 pub fn name(&self) -> &str {
394 match self {
395 Self::AutoDeps(_) => "autodeps",
396 Self::Debug(_) => "debug",
397 Self::Docs(_) => "docs",
398 Self::EmptyDirs(_) => "emptydirs",
399 Self::Libtool(_) => "libtool",
400 Self::Lto(_) => "lto",
401 Self::PEStrip(_) => "pestrip",
402 Self::Purge(_) => "purge",
403 Self::StaticLibs(_) => "staticlibs",
404 Self::Strip(_) => "strip",
405 Self::Zipman(_) => "zipman",
406 }
407 }
408
409 pub fn on(&self) -> bool {
411 match self {
412 Self::AutoDeps(on)
413 | Self::Debug(on)
414 | Self::Docs(on)
415 | Self::EmptyDirs(on)
416 | Self::Libtool(on)
417 | Self::Lto(on)
418 | Self::Purge(on)
419 | Self::PEStrip(on)
420 | Self::StaticLibs(on)
421 | Self::Strip(on)
422 | Self::Zipman(on) => *on,
423 }
424 }
425}
426
427impl AlpmParser for PackageOption {
428 fn parser<'a>(input: &mut Input<'a>) -> PResult<'a, Self> {
434 let on = option_bool_parser.parse_next(input)?;
435 let name = option_name_parser.parse_next(input)?;
436
437 alt((
438 alt((
439 "autodeps".value(Self::AutoDeps(on)),
440 "debug".value(Self::Debug(on)),
441 "docs".value(Self::Docs(on)),
442 "emptydirs".value(Self::EmptyDirs(on)),
443 "libtool".value(Self::Libtool(on)),
444 "lto".value(Self::Lto(on)),
445 "pestrip".value(Self::PEStrip(on)),
446 "purge".value(Self::Purge(on)),
447 "staticlibs".value(Self::StaticLibs(on)),
448 )),
449 alt((
450 "strip".value(Self::Strip(on)),
451 "zipman".value(Self::Zipman(on)),
452 )),
453 fail,
454 ))
455 .expected_strings(PackageOption::VARIANTS)
456 .layer("makepkg packaging option")
457 .parse_next(&mut Input::new(name))
458 }
459
460 fn delimiter_error_context<'a, O, P>(
461 parser: P,
462 ) -> impl Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>
463 where
464 P: Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>,
465 {
466 parser
467 .expected_text("string consisting of alphanumeric characters or")
468 .expected_chars(SPECIAL_OPTION_CHARS)
469 .layer("package option")
470 }
471}
472
473impl FromStr for PackageOption {
474 type Err = Error;
475 fn from_str(s: &str) -> Result<Self, Self::Err> {
483 Ok(Self::parser_until_eof.parse(Input::new(s))?)
484 }
485}
486
487impl Display for PackageOption {
488 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
489 write!(fmt, "{}{}", if self.on() { "" } else { "!" }, self.name())
490 }
491}
492
493#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
524#[cfg_attr(feature = "serde", derive(Serialize))]
525pub struct InstalledPackage {
526 name: Name,
527 version: FullVersion,
528 architecture: Architecture,
529}
530
531impl InstalledPackage {
532 pub fn new(name: Name, version: FullVersion, architecture: Architecture) -> Self {
551 Self {
552 name,
553 version,
554 architecture,
555 }
556 }
557
558 pub fn name(&self) -> &Name {
576 &self.name
577 }
578
579 pub fn version(&self) -> &FullVersion {
597 &self.version
598 }
599
600 pub fn architecture(&self) -> &Architecture {
618 &self.architecture
619 }
620
621 pub fn to_package_relation(&self) -> PackageRelation {
642 PackageRelation {
643 name: self.name.clone(),
644 version_requirement: Some(VersionRequirement {
645 comparison: VersionComparison::Equal,
646 version: self.version.clone().into(),
647 }),
648 }
649 }
650}
651
652impl ParserUntil for InstalledPackage {
653 #[expect(clippy::arithmetic_side_effects)]
683 fn parser_until<'a, P>(delimiter: P) -> impl Parser<Input<'a>, Self, ErrMode<ParseStack<'a>>>
684 where
685 P: Parser<Input<'a>, &'a str, ErrMode<ParseStack<'a>>>,
686 {
687 let mut delimiter_parser = delimiter;
690 let parser = move |input: &mut Input<'a>| -> PResult<'a, Self> {
691 let checkpoint = input.checkpoint();
702 let dashes: usize =
703 repeat_till::<_, _, (), _, _, _, _>(0.., any, peek(delimiter_parser.by_ref()))
704 .take()
705 .map(|s| {
706 s.chars().fold(0, |acc, char| {
707 if char == '-' {
708 return acc + 1;
709 }
710 acc
711 })
712 })
713 .parse_next(input)?;
714 input.reset(&checkpoint);
715
716 if dashes < 3 {
717 return fail
718 .label("alpm-package file name")
719 .description(concat!(
720 "Expected a package name, followed by an alpm-package-version (full or full with epoch) and an alpm-architecture.",
721 "\nAll components must be delimited with a dash ('-')."
722 ))
723 .parse_next(input);
724 }
725
726 let dashes_till_version = dashes.saturating_sub(2);
728
729 let name =
733 Name::parse_name_followed_by_version(dashes_till_version).parse_next(input)?;
734
735 "-".parse_next(input)?;
738
739 let version: FullVersion = FullVersion::parser
742 .expected_text("an alpm-package-version (full or full with epoch) followed by a `-` and an alpm-architecture")
743 .parse_next(input)?;
744
745 "-".label("delimiter")
748 .expected_text("expected a `-` followed by an alpm-architecture")
749 .parse_next(input)?;
750
751 let architecture =
753 Architecture::parser_until(delimiter_parser.by_ref()).parse_next(input)?;
754
755 Ok(Self {
756 name,
757 version,
758 architecture,
759 })
760 };
761
762 parser.layer("installed package name")
763 }
764}
765
766impl From<PackageFileName> for InstalledPackage {
767 fn from(value: PackageFileName) -> Self {
769 Self {
770 name: value.name,
771 version: value.version,
772 architecture: value.architecture,
773 }
774 }
775}
776
777impl FromStr for InstalledPackage {
778 type Err = Error;
779
780 fn from_str(s: &str) -> Result<InstalledPackage, Self::Err> {
802 Ok(Self::parser_until_eof.parse(Input::new(s))?)
803 }
804}
805
806impl Display for InstalledPackage {
807 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
808 write!(fmt, "{}-{}-{}", self.name, self.version, self.architecture)
809 }
810}
811
812#[cfg(test)]
813mod tests {
814 use insta::assert_snapshot;
815 use rstest::rstest;
816 use testresult::TestResult;
817
818 use super::*;
819 use crate::{SystemArchitecture, configure_insta};
820
821 #[rstest]
822 #[case(
823 "!makeflags",
824 MakepkgOption::BuildEnvironment(BuildEnvironmentOption::MakeFlags(false))
825 )]
826 #[case("autodeps", MakepkgOption::Package(PackageOption::AutoDeps(true)))]
827 #[case(
828 "ccache",
829 MakepkgOption::BuildEnvironment(BuildEnvironmentOption::Ccache(true))
830 )]
831 fn makepkg_option(#[case] input: &str, #[case] expected: MakepkgOption) {
832 let result = MakepkgOption::from_str(input).expect("Parser should be successful");
833 assert_eq!(result, expected);
834 }
835
836 #[rstest]
837 #[case("!somethingelse")]
838 #[case("#somethingelse")]
839 fn invalid_makepkg_option(#[case] input: &str) {
840 let (test_name, _guard) = configure_insta();
841
842 let Err(Error::ParseError(err_msg)) = MakepkgOption::from_str(input) else {
843 panic!("'{input}' erroneously parsed as MakepkgOption")
844 };
845
846 assert_snapshot!(test_name, err_msg.to_string());
847 }
848
849 #[rstest]
850 #[case("autodeps", PackageOption::AutoDeps(true))]
851 #[case("debug", PackageOption::Debug(true))]
852 #[case("docs", PackageOption::Docs(true))]
853 #[case("emptydirs", PackageOption::EmptyDirs(true))]
854 #[case("!libtool", PackageOption::Libtool(false))]
855 #[case("lto", PackageOption::Lto(true))]
856 #[case("pestrip", PackageOption::PEStrip(true))]
857 #[case("purge", PackageOption::Purge(true))]
858 #[case("staticlibs", PackageOption::StaticLibs(true))]
859 #[case("strip", PackageOption::Strip(true))]
860 #[case("zipman", PackageOption::Zipman(true))]
861 fn package_option(#[case] s: &str, #[case] expected: PackageOption) {
862 let result = PackageOption::from_str(s).expect("Parser should be successful");
863 assert_eq!(result, expected);
864 }
865
866 #[rstest]
867 #[case("!somethingelse")]
868 #[case("#somethingelse")]
869 fn invalid_package_option(#[case] input: &str) {
870 let (test_name, _guard) = configure_insta();
871
872 let Err(Error::ParseError(err_msg)) = PackageOption::from_str(input) else {
873 panic!("'{input}' erroneously parsed as PackageOption")
874 };
875
876 assert_snapshot!(test_name, err_msg.to_string());
877 }
878
879 #[rstest]
880 #[case("buildflags", BuildEnvironmentOption::BuildFlags(true))]
881 #[case("ccache", BuildEnvironmentOption::Ccache(true))]
882 #[case("check", BuildEnvironmentOption::Check(true))]
883 #[case("color", BuildEnvironmentOption::Color(true))]
884 #[case("distcc", BuildEnvironmentOption::Distcc(true))]
885 #[case("!makeflags", BuildEnvironmentOption::MakeFlags(false))]
886 #[case("sign", BuildEnvironmentOption::Sign(true))]
887 #[case("!sign", BuildEnvironmentOption::Sign(false))]
888 fn build_environment_option(#[case] input: &str, #[case] expected: BuildEnvironmentOption) {
889 let result = BuildEnvironmentOption::from_str(input).expect("Parser should be successful");
890 assert_eq!(result, expected);
891 }
892
893 #[rstest]
894 #[case("!somethingelse")]
895 #[case("#somethingelse")]
896 fn invalid_build_environment_option(#[case] input: &str) {
897 let (test_name, _guard) = configure_insta();
898
899 let Err(Error::ParseError(err_msg)) = BuildEnvironmentOption::from_str(input) else {
900 panic!("'{input}' erroneously parsed as BuildEnvironmentOption")
901 };
902
903 assert_snapshot!(test_name, err_msg.to_string());
904 }
905
906 #[rstest]
908 #[case(
909 "foo-bar-1:1.0.0-1-any",
910 InstalledPackage {
911 name: Name::new("foo-bar")?,
912 version: FullVersion::from_str("1:1.0.0-1")?,
913 architecture: Architecture::Any,
914 },
915 )]
916 #[case(
917 "foobar-1.0.0-1-x86_64",
918 InstalledPackage {
919 name: Name::new("foobar")?,
920 version: FullVersion::from_str("1.0.0-1")?,
921 architecture: SystemArchitecture::X86_64.into(),
922 },
923 )]
924 fn installed_from_str_succeeds(
925 #[case] s: &str,
926 #[case] expected: InstalledPackage,
927 ) -> TestResult {
928 assert_eq!(InstalledPackage::from_str(s)?, expected);
929 Ok(())
930 }
931
932 #[rstest]
933 #[case::missing_arch("foo-1:1.0.0-bar")]
934 #[case::any_arch("foo-1:1.0.0-bar-any")]
935 #[case::missing_rel_and_arch("foo-1:1.0.0_any")]
936 #[case::missing_rel_and_dash_in_name("packagename-30-0.1oops-any")]
937 #[case::invalid_chars("package$with$dollars-30-0.1-any")]
938 #[case::trailing_content("packagename-30-0.1-any*asdf")]
939 fn installed_new_parse_error(#[case] input: &str) {
940 let (test_name, _guard) = configure_insta();
941
942 let Err(Error::ParseError(err_msg)) = InstalledPackage::from_str(input) else {
943 panic!("'{input}' erroneously parsed as InstalledPackage")
944 };
945
946 assert_snapshot!(test_name, err_msg.to_string());
947 }
948}