1use std::{
2 fmt::{Display, Formatter},
3 str::FromStr,
4};
5
6use alpm_parsers::{iter_char_context, iter_str_context};
7use serde::{Deserialize, Serialize};
8use strum::VariantNames;
9use winnow::{
10 ModalResult,
11 Parser,
12 combinator::{alt, cut_err, eof, fail, opt, peek, repeat},
13 error::{
14 AddContext,
15 ContextError,
16 ErrMode,
17 ParserError,
18 StrContext,
19 StrContextValue::{self, *},
20 },
21 stream::Stream,
22 token::{one_of, rest, take_until},
23};
24
25use crate::{
26 Architecture,
27 FullVersion,
28 Name,
29 PackageFileName,
30 PackageRelation,
31 VersionComparison,
32 VersionRequirement,
33 error::Error,
34};
35
36fn option_bool_parser(input: &mut &str) -> ModalResult<bool> {
50 let alphanum = |c: char| c.is_ascii_alphanumeric();
51 let special_first_chars = ['-', '.', '_', '!'];
52 let valid_chars = one_of((alphanum, special_first_chars));
53
54 cut_err(peek(valid_chars))
56 .context(StrContext::Expected(CharLiteral('!')))
57 .context(StrContext::Expected(Description(
58 "ASCII alphanumeric character",
59 )))
60 .context_with(iter_char_context!(special_first_chars))
61 .parse_next(input)?;
62
63 Ok(opt('!').parse_next(input)?.is_none())
64}
65
66fn option_name_parser<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
78 let alphanum = |c: char| c.is_ascii_alphanumeric();
79
80 let special_chars = ['-', '.', '_'];
81 let valid_chars = one_of((alphanum, special_chars));
82 let name = repeat::<_, _, (), _, _>(0.., valid_chars)
83 .take()
84 .parse_next(input)?;
85
86 eof.context(StrContext::Label("character in makepkg option"))
87 .context(StrContext::Expected(Description(
88 "ASCII alphanumeric character",
89 )))
90 .context_with(iter_char_context!(special_chars))
91 .parse_next(input)?;
92
93 Ok(name)
94}
95
96#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
104#[serde(tag = "type", rename_all = "snake_case")]
105pub enum MakepkgOption {
106 BuildEnvironment(BuildEnvironmentOption),
108 Package(PackageOption),
110}
111
112impl MakepkgOption {
113 pub fn parser(input: &mut &str) -> ModalResult<Self> {
122 alt((
123 BuildEnvironmentOption::parser.map(MakepkgOption::BuildEnvironment),
124 PackageOption::parser.map(MakepkgOption::Package),
125 fail.context(StrContext::Label("packaging or build environment option"))
126 .context_with(iter_str_context!([
127 BuildEnvironmentOption::VARIANTS.to_vec(),
128 PackageOption::VARIANTS.to_vec()
129 ])),
130 ))
131 .parse_next(input)
132 }
133}
134
135impl FromStr for MakepkgOption {
136 type Err = Error;
137 fn from_str(s: &str) -> Result<Self, Self::Err> {
139 Ok(Self::parser.parse(s)?)
140 }
141}
142
143impl Display for MakepkgOption {
144 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
145 match self {
146 MakepkgOption::BuildEnvironment(option) => write!(fmt, "{option}"),
147 MakepkgOption::Package(option) => write!(fmt, "{option}"),
148 }
149 }
150}
151
152#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, VariantNames)]
175#[serde(rename_all = "lowercase")]
176pub enum BuildEnvironmentOption {
177 #[strum(serialize = "buildflags")]
182 BuildFlags(bool),
183 #[strum(serialize = "ccache")]
185 Ccache(bool),
186 #[strum(serialize = "check")]
188 Check(bool),
189 #[strum(serialize = "color")]
191 Color(bool),
192 #[strum(serialize = "distcc")]
194 Distcc(bool),
195 #[strum(serialize = "sign")]
197 Sign(bool),
198 #[strum(serialize = "makeflags")]
203 MakeFlags(bool),
204}
205
206impl BuildEnvironmentOption {
207 pub fn new(option: &str) -> Result<Self, Error> {
213 Self::from_str(option)
214 }
215
216 pub fn name(&self) -> &str {
218 match self {
219 Self::BuildFlags(_) => "buildflags",
220 Self::Ccache(_) => "ccache",
221 Self::Check(_) => "check",
222 Self::Color(_) => "color",
223 Self::Distcc(_) => "distcc",
224 Self::MakeFlags(_) => "makeflags",
225 Self::Sign(_) => "sign",
226 }
227 }
228
229 pub fn on(&self) -> bool {
231 match self {
232 Self::BuildFlags(on)
233 | Self::Ccache(on)
234 | Self::Check(on)
235 | Self::Color(on)
236 | Self::Distcc(on)
237 | Self::MakeFlags(on)
238 | Self::Sign(on) => *on,
239 }
240 }
241
242 pub fn parser(input: &mut &str) -> ModalResult<Self> {
250 let on = option_bool_parser.parse_next(input)?;
251 let mut name = option_name_parser.parse_next(input)?;
252
253 alt((
254 "buildflags".value(Self::BuildFlags(on)),
255 "ccache".value(Self::Ccache(on)),
256 "check".value(Self::Check(on)),
257 "color".value(Self::Color(on)),
258 "distcc".value(Self::Distcc(on)),
259 "makeflags".value(Self::MakeFlags(on)),
260 "sign".value(Self::Sign(on)),
261 fail.context(StrContext::Label("makepkg build environment option"))
262 .context_with(iter_str_context!([BuildEnvironmentOption::VARIANTS])),
263 ))
264 .parse_next(&mut name)
265 }
266}
267
268impl FromStr for BuildEnvironmentOption {
269 type Err = Error;
270 fn from_str(s: &str) -> Result<Self, Self::Err> {
278 Ok(Self::parser.parse(s)?)
279 }
280}
281
282impl Display for BuildEnvironmentOption {
283 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
284 write!(fmt, "{}{}", if self.on() { "" } else { "!" }, self.name())
285 }
286}
287
288#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, VariantNames)]
311#[serde(rename_all = "lowercase")]
312pub enum PackageOption {
313 #[strum(serialize = "autodeps")]
317 AutoDeps(bool),
318
319 #[strum(serialize = "debug")]
321 Debug(bool),
322
323 #[strum(serialize = "docs")]
325 Docs(bool),
326
327 #[strum(serialize = "emptydirs")]
329 EmptyDirs(bool),
330
331 #[strum(serialize = "libtool")]
333 Libtool(bool),
334
335 #[strum(serialize = "lto")]
337 Lto(bool),
338
339 #[strum(serialize = "pestrip")]
341 PEStrip(bool),
342
343 #[strum(serialize = "purge")]
345 Purge(bool),
346
347 #[strum(serialize = "staticlibs")]
349 StaticLibs(bool),
350
351 #[strum(serialize = "strip")]
353 Strip(bool),
354
355 #[strum(serialize = "zipman")]
357 Zipman(bool),
358}
359
360impl PackageOption {
361 pub fn new(option: &str) -> Result<Self, Error> {
367 Self::from_str(option)
368 }
369
370 pub fn name(&self) -> &str {
372 match self {
373 Self::AutoDeps(_) => "autodeps",
374 Self::Debug(_) => "debug",
375 Self::Docs(_) => "docs",
376 Self::EmptyDirs(_) => "emptydirs",
377 Self::Libtool(_) => "libtool",
378 Self::Lto(_) => "lto",
379 Self::PEStrip(_) => "pestrip",
380 Self::Purge(_) => "purge",
381 Self::StaticLibs(_) => "staticlibs",
382 Self::Strip(_) => "strip",
383 Self::Zipman(_) => "zipman",
384 }
385 }
386
387 pub fn on(&self) -> bool {
389 match self {
390 Self::AutoDeps(on)
391 | Self::Debug(on)
392 | Self::Docs(on)
393 | Self::EmptyDirs(on)
394 | Self::Libtool(on)
395 | Self::Lto(on)
396 | Self::Purge(on)
397 | Self::PEStrip(on)
398 | Self::StaticLibs(on)
399 | Self::Strip(on)
400 | Self::Zipman(on) => *on,
401 }
402 }
403
404 pub fn parser(input: &mut &str) -> ModalResult<Self> {
412 let on = option_bool_parser.parse_next(input)?;
413 let mut name = option_name_parser.parse_next(input)?;
414
415 alt((
416 alt((
417 "autodeps".value(Self::AutoDeps(on)),
418 "debug".value(Self::Debug(on)),
419 "docs".value(Self::Docs(on)),
420 "emptydirs".value(Self::EmptyDirs(on)),
421 "libtool".value(Self::Libtool(on)),
422 "lto".value(Self::Lto(on)),
423 "pestrip".value(Self::PEStrip(on)),
424 "purge".value(Self::Purge(on)),
425 "staticlibs".value(Self::StaticLibs(on)),
426 )),
427 alt((
428 "strip".value(Self::Strip(on)),
429 "zipman".value(Self::Zipman(on)),
430 )),
431 fail.context(StrContext::Label("makepkg packaging option"))
432 .context_with(iter_str_context!([PackageOption::VARIANTS])),
433 ))
434 .parse_next(&mut name)
435 }
436}
437
438impl FromStr for PackageOption {
439 type Err = Error;
440 fn from_str(s: &str) -> Result<Self, Self::Err> {
448 Ok(Self::parser.parse(s)?)
449 }
450}
451
452impl Display for PackageOption {
453 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
454 write!(fmt, "{}{}", if self.on() { "" } else { "!" }, self.name())
455 }
456}
457
458#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
489pub struct InstalledPackage {
490 name: Name,
491 version: FullVersion,
492 architecture: Architecture,
493}
494
495impl InstalledPackage {
496 pub fn new(name: Name, version: FullVersion, architecture: Architecture) -> Self {
515 Self {
516 name,
517 version,
518 architecture,
519 }
520 }
521
522 pub fn name(&self) -> &Name {
540 &self.name
541 }
542
543 pub fn version(&self) -> &FullVersion {
561 &self.version
562 }
563
564 pub fn architecture(&self) -> &Architecture {
582 &self.architecture
583 }
584
585 pub fn to_package_relation(&self) -> PackageRelation {
606 PackageRelation {
607 name: self.name.clone(),
608 version_requirement: Some(VersionRequirement {
609 comparison: VersionComparison::Equal,
610 version: self.version.clone().into(),
611 }),
612 }
613 }
614
615 pub fn parser(input: &mut &str) -> ModalResult<Self> {
641 let dashes: usize = input.chars().filter(|char| char == &'-').count();
652
653 if dashes < 2 {
654 let context_error = ContextError::from_input(input)
655 .add_context(
656 input,
657 &input.checkpoint(),
658 StrContext::Label("alpm-package file name"),
659 )
660 .add_context(
661 input,
662 &input.checkpoint(),
663 StrContext::Expected(StrContextValue::Description(
664 concat!(
665 "a package name, followed by an alpm-package-version (full or full with epoch) and an architecture.",
666 "\nAll components must be delimited with a dash ('-')."
667 )
668 ))
669 );
670
671 return Err(ErrMode::Cut(context_error));
672 }
673
674 let dashes_in_name = dashes.saturating_sub(3);
676
677 let name = cut_err(
681 repeat::<_, _, (), _, _>(
682 dashes_in_name + 1,
683 (opt("-"), take_until(0.., "-"), peek("-")),
688 )
689 .take()
690 .and_then(Name::parser),
692 )
693 .context(StrContext::Label("alpm-package-name"))
694 .parse_next(input)?;
695
696 "-".parse_next(input)?;
699
700 let version: FullVersion = cut_err((take_until(0.., "-"), "-", take_until(0.., "-")))
703 .context(StrContext::Label("alpm-package-version"))
704 .context(StrContext::Expected(StrContextValue::Description(
705 "an alpm-package-version (full or full with epoch) followed by a `-` and an architecture",
706 )))
707 .take()
708 .and_then(cut_err(FullVersion::parser))
709 .parse_next(input)?;
710
711 "-".parse_next(input)?;
714
715 let architecture = rest.and_then(Architecture::parser).parse_next(input)?;
718
719 Ok(Self {
720 name,
721 version,
722 architecture,
723 })
724 }
725}
726
727impl From<PackageFileName> for InstalledPackage {
728 fn from(value: PackageFileName) -> Self {
730 Self {
731 name: value.name,
732 version: value.version,
733 architecture: value.architecture,
734 }
735 }
736}
737
738impl FromStr for InstalledPackage {
739 type Err = Error;
740
741 fn from_str(s: &str) -> Result<InstalledPackage, Self::Err> {
763 Ok(Self::parser.parse(s)?)
764 }
765}
766
767impl Display for InstalledPackage {
768 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
769 write!(fmt, "{}-{}-{}", self.name, self.version, self.architecture)
770 }
771}
772
773#[cfg(test)]
774mod tests {
775 use rstest::rstest;
776 use testresult::TestResult;
777
778 use super::*;
779 use crate::SystemArchitecture;
780
781 #[rstest]
782 #[case(
783 "!makeflags",
784 MakepkgOption::BuildEnvironment(BuildEnvironmentOption::MakeFlags(false))
785 )]
786 #[case("autodeps", MakepkgOption::Package(PackageOption::AutoDeps(true)))]
787 #[case(
788 "ccache",
789 MakepkgOption::BuildEnvironment(BuildEnvironmentOption::Ccache(true))
790 )]
791 fn makepkg_option(#[case] input: &str, #[case] expected: MakepkgOption) {
792 let result = MakepkgOption::from_str(input).expect("Parser should be successful");
793 assert_eq!(result, expected);
794 }
795
796 #[rstest]
797 #[case(
798 "!somethingelse",
799 concat!(
800 "expected `buildflags`, `ccache`, `check`, `color`, `distcc`, `sign`, `makeflags`, ",
801 "`autodeps`, `debug`, `docs`, `emptydirs`, `libtool`, `lto`, `pestrip`, `purge`, ",
802 "`staticlibs`, `strip`, `zipman`",
803 )
804 )]
805 #[case(
806 "#somethingelse",
807 "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
808 )]
809 fn invalid_makepkg_option(#[case] input: &str, #[case] err_snippet: &str) {
810 let Err(Error::ParseError(err_msg)) = MakepkgOption::from_str(input) else {
811 panic!("'{input}' erroneously parsed as VersionRequirement")
812 };
813 assert!(
814 err_msg.contains(err_snippet),
815 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
816 );
817 }
818
819 #[rstest]
820 #[case("autodeps", PackageOption::AutoDeps(true))]
821 #[case("debug", PackageOption::Debug(true))]
822 #[case("docs", PackageOption::Docs(true))]
823 #[case("emptydirs", PackageOption::EmptyDirs(true))]
824 #[case("!libtool", PackageOption::Libtool(false))]
825 #[case("lto", PackageOption::Lto(true))]
826 #[case("pestrip", PackageOption::PEStrip(true))]
827 #[case("purge", PackageOption::Purge(true))]
828 #[case("staticlibs", PackageOption::StaticLibs(true))]
829 #[case("strip", PackageOption::Strip(true))]
830 #[case("zipman", PackageOption::Zipman(true))]
831 fn package_option(#[case] s: &str, #[case] expected: PackageOption) {
832 let result = PackageOption::from_str(s).expect("Parser should be successful");
833 assert_eq!(result, expected);
834 }
835
836 #[rstest]
837 #[case(
838 "!somethingelse",
839 "expected `autodeps`, `debug`, `docs`, `emptydirs`, `libtool`, `lto`, `pestrip`, `purge`, `staticlibs`, `strip`, `zipman`"
840 )]
841 #[case(
842 "#somethingelse",
843 "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
844 )]
845 fn invalid_package_option(#[case] input: &str, #[case] err_snippet: &str) {
846 let Err(Error::ParseError(err_msg)) = PackageOption::from_str(input) else {
847 panic!("'{input}' erroneously parsed as VersionRequirement")
848 };
849 assert!(
850 err_msg.contains(err_snippet),
851 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
852 );
853 }
854
855 #[rstest]
856 #[case("buildflags", BuildEnvironmentOption::BuildFlags(true))]
857 #[case("ccache", BuildEnvironmentOption::Ccache(true))]
858 #[case("check", BuildEnvironmentOption::Check(true))]
859 #[case("color", BuildEnvironmentOption::Color(true))]
860 #[case("distcc", BuildEnvironmentOption::Distcc(true))]
861 #[case("!makeflags", BuildEnvironmentOption::MakeFlags(false))]
862 #[case("sign", BuildEnvironmentOption::Sign(true))]
863 #[case("!sign", BuildEnvironmentOption::Sign(false))]
864 fn build_environment_option(#[case] input: &str, #[case] expected: BuildEnvironmentOption) {
865 let result = BuildEnvironmentOption::from_str(input).expect("Parser should be successful");
866 assert_eq!(result, expected);
867 }
868
869 #[rstest]
870 #[case(
871 "!somethingelse",
872 "expected `buildflags`, `ccache`, `check`, `color`, `distcc`, `sign`, `makeflags`"
873 )]
874 #[case(
875 "#somethingelse",
876 "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
877 )]
878 fn invalid_build_environment_option(#[case] input: &str, #[case] err_snippet: &str) {
879 let Err(Error::ParseError(err_msg)) = BuildEnvironmentOption::from_str(input) else {
880 panic!("'{input}' erroneously parsed as VersionRequirement")
881 };
882 assert!(
883 err_msg.contains(err_snippet),
884 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
885 );
886 }
887
888 #[rstest]
889 #[case("#test", "invalid character in makepkg option")]
890 #[case("test!", "invalid character in makepkg option")]
891 fn invalid_option(#[case] input: &str, #[case] error_snippet: &str) {
892 let result = option_name_parser.parse(input);
893 assert!(result.is_err(), "Expected makepkg option parsing to fail");
894 let err = result.unwrap_err();
895 let pretty_error = err.to_string();
896 assert!(
897 pretty_error.contains(error_snippet),
898 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
899 );
900 }
901
902 #[rstest]
903 #[case(
904 "foo-bar-1:1.0.0-1-any",
905 InstalledPackage {
906 name: Name::new("foo-bar")?,
907 version: FullVersion::from_str("1:1.0.0-1")?,
908 architecture: Architecture::Any,
909 },
910 )]
911 #[case(
912 "foobar-1.0.0-1-x86_64",
913 InstalledPackage {
914 name: Name::new("foobar")?,
915 version: FullVersion::from_str("1.0.0-1")?,
916 architecture: SystemArchitecture::X86_64.into(),
917 },
918 )]
919 fn installed_from_str(#[case] s: &str, #[case] result: InstalledPackage) -> TestResult {
920 assert_eq!(InstalledPackage::from_str(s), Ok(result));
921 Ok(())
922 }
923
924 #[rstest]
925 #[case("foo-1:1.0.0-bar-any", "invalid package release")]
926 #[case(
927 "foo-1:1.0.0_any",
928 "expected a package name, followed by an alpm-package-version (full or full with epoch) and an architecture."
929 )]
930 #[case("packagename-30-0.1oops-any", "expected end of package release value")]
931 #[case("package$with$dollars-30-0.1-any", "invalid character in package name")]
932 #[case(
933 "packagename-30-0.1-any*asdf",
934 "invalid character in system architecture"
935 )]
936 fn installed_new_parse_error(#[case] input: &str, #[case] error_snippet: &str) {
937 let result = InstalledPackage::from_str(input);
938 assert!(result.is_err(), "Expected InstalledPackage parsing to fail");
939 let err = result.unwrap_err();
940 let pretty_error = err.to_string();
941 assert!(
942 pretty_error.contains(error_snippet),
943 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
944 );
945 }
946}