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 = "purge")]
341 Purge(bool),
342
343 #[strum(serialize = "staticlibs")]
345 StaticLibs(bool),
346
347 #[strum(serialize = "strip")]
349 Strip(bool),
350
351 #[strum(serialize = "zipman")]
353 Zipman(bool),
354}
355
356impl PackageOption {
357 pub fn new(option: &str) -> Result<Self, Error> {
363 Self::from_str(option)
364 }
365
366 pub fn name(&self) -> &str {
368 match self {
369 Self::AutoDeps(_) => "autodeps",
370 Self::Debug(_) => "debug",
371 Self::Docs(_) => "docs",
372 Self::EmptyDirs(_) => "emptydirs",
373 Self::Libtool(_) => "libtool",
374 Self::Lto(_) => "lto",
375 Self::Purge(_) => "purge",
376 Self::StaticLibs(_) => "staticlibs",
377 Self::Strip(_) => "strip",
378 Self::Zipman(_) => "zipman",
379 }
380 }
381
382 pub fn on(&self) -> bool {
384 match self {
385 Self::AutoDeps(on)
386 | Self::Debug(on)
387 | Self::Docs(on)
388 | Self::EmptyDirs(on)
389 | Self::Libtool(on)
390 | Self::Lto(on)
391 | Self::Purge(on)
392 | Self::StaticLibs(on)
393 | Self::Strip(on)
394 | Self::Zipman(on) => *on,
395 }
396 }
397
398 pub fn parser(input: &mut &str) -> ModalResult<Self> {
406 let on = option_bool_parser.parse_next(input)?;
407 let mut name = option_name_parser.parse_next(input)?;
408
409 alt((
410 "autodeps".value(Self::AutoDeps(on)),
411 "debug".value(Self::Debug(on)),
412 "docs".value(Self::Docs(on)),
413 "emptydirs".value(Self::EmptyDirs(on)),
414 "libtool".value(Self::Libtool(on)),
415 "lto".value(Self::Lto(on)),
416 "purge".value(Self::Purge(on)),
417 "staticlibs".value(Self::StaticLibs(on)),
418 "strip".value(Self::Strip(on)),
419 "zipman".value(Self::Zipman(on)),
420 fail.context(StrContext::Label("makepkg packaging option"))
421 .context_with(iter_str_context!([PackageOption::VARIANTS])),
422 ))
423 .parse_next(&mut name)
424 }
425}
426
427impl FromStr for PackageOption {
428 type Err = Error;
429 fn from_str(s: &str) -> Result<Self, Self::Err> {
437 Ok(Self::parser.parse(s)?)
438 }
439}
440
441impl Display for PackageOption {
442 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
443 write!(fmt, "{}{}", if self.on() { "" } else { "!" }, self.name())
444 }
445}
446
447#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
478pub struct InstalledPackage {
479 name: Name,
480 version: FullVersion,
481 architecture: Architecture,
482}
483
484impl InstalledPackage {
485 pub fn new(name: Name, version: FullVersion, architecture: Architecture) -> Self {
504 Self {
505 name,
506 version,
507 architecture,
508 }
509 }
510
511 pub fn name(&self) -> &Name {
529 &self.name
530 }
531
532 pub fn version(&self) -> &FullVersion {
550 &self.version
551 }
552
553 pub fn architecture(&self) -> &Architecture {
571 &self.architecture
572 }
573
574 pub fn to_package_relation(&self) -> PackageRelation {
595 PackageRelation {
596 name: self.name.clone(),
597 version_requirement: Some(VersionRequirement {
598 comparison: VersionComparison::Equal,
599 version: self.version.clone().into(),
600 }),
601 }
602 }
603
604 pub fn parser(input: &mut &str) -> ModalResult<Self> {
630 let dashes: usize = input.chars().filter(|char| char == &'-').count();
641
642 if dashes < 2 {
643 let context_error = ContextError::from_input(input)
644 .add_context(
645 input,
646 &input.checkpoint(),
647 StrContext::Label("alpm-package file name"),
648 )
649 .add_context(
650 input,
651 &input.checkpoint(),
652 StrContext::Expected(StrContextValue::Description(
653 concat!(
654 "a package name, followed by an alpm-package-version (full or full with epoch) and an architecture.",
655 "\nAll components must be delimited with a dash ('-')."
656 )
657 ))
658 );
659
660 return Err(ErrMode::Cut(context_error));
661 }
662
663 let dashes_in_name = dashes.saturating_sub(3);
665
666 let name = cut_err(
670 repeat::<_, _, (), _, _>(
671 dashes_in_name + 1,
672 (opt("-"), take_until(0.., "-"), peek("-")),
677 )
678 .take()
679 .and_then(Name::parser),
681 )
682 .context(StrContext::Label("alpm-package-name"))
683 .parse_next(input)?;
684
685 "-".parse_next(input)?;
688
689 let version: FullVersion = cut_err((take_until(0.., "-"), "-", take_until(0.., "-")))
692 .context(StrContext::Label("alpm-package-version"))
693 .context(StrContext::Expected(StrContextValue::Description(
694 "an alpm-package-version (full or full with epoch) followed by a `-` and an architecture",
695 )))
696 .take()
697 .and_then(cut_err(FullVersion::parser))
698 .parse_next(input)?;
699
700 "-".parse_next(input)?;
703
704 let architecture = rest.and_then(Architecture::parser).parse_next(input)?;
707
708 Ok(Self {
709 name,
710 version,
711 architecture,
712 })
713 }
714}
715
716impl From<PackageFileName> for InstalledPackage {
717 fn from(value: PackageFileName) -> Self {
719 Self {
720 name: value.name,
721 version: value.version,
722 architecture: value.architecture,
723 }
724 }
725}
726
727impl FromStr for InstalledPackage {
728 type Err = Error;
729
730 fn from_str(s: &str) -> Result<InstalledPackage, Self::Err> {
752 Ok(Self::parser.parse(s)?)
753 }
754}
755
756impl Display for InstalledPackage {
757 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
758 write!(fmt, "{}-{}-{}", self.name, self.version, self.architecture)
759 }
760}
761
762#[cfg(test)]
763mod tests {
764 use rstest::rstest;
765 use testresult::TestResult;
766
767 use super::*;
768 use crate::SystemArchitecture;
769
770 #[rstest]
771 #[case(
772 "!makeflags",
773 MakepkgOption::BuildEnvironment(BuildEnvironmentOption::MakeFlags(false))
774 )]
775 #[case("autodeps", MakepkgOption::Package(PackageOption::AutoDeps(true)))]
776 #[case(
777 "ccache",
778 MakepkgOption::BuildEnvironment(BuildEnvironmentOption::Ccache(true))
779 )]
780 fn makepkg_option(#[case] input: &str, #[case] expected: MakepkgOption) {
781 let result = MakepkgOption::from_str(input).expect("Parser should be successful");
782 assert_eq!(result, expected);
783 }
784
785 #[rstest]
786 #[case(
787 "!somethingelse",
788 concat!(
789 "expected `buildflags`, `ccache`, `check`, `color`, `distcc`, `sign`, `makeflags`, ",
790 "`autodeps`, `debug`, `docs`, `emptydirs`, `libtool`, `lto`, `purge`, ",
791 "`staticlibs`, `strip`, `zipman`",
792 )
793 )]
794 #[case(
795 "#somethingelse",
796 "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
797 )]
798 fn invalid_makepkg_option(#[case] input: &str, #[case] err_snippet: &str) {
799 let Err(Error::ParseError(err_msg)) = MakepkgOption::from_str(input) else {
800 panic!("'{input}' erroneously parsed as VersionRequirement")
801 };
802 assert!(
803 err_msg.contains(err_snippet),
804 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
805 );
806 }
807
808 #[rstest]
809 #[case("autodeps", PackageOption::AutoDeps(true))]
810 #[case("debug", PackageOption::Debug(true))]
811 #[case("docs", PackageOption::Docs(true))]
812 #[case("emptydirs", PackageOption::EmptyDirs(true))]
813 #[case("!libtool", PackageOption::Libtool(false))]
814 #[case("lto", PackageOption::Lto(true))]
815 #[case("purge", PackageOption::Purge(true))]
816 #[case("staticlibs", PackageOption::StaticLibs(true))]
817 #[case("strip", PackageOption::Strip(true))]
818 #[case("zipman", PackageOption::Zipman(true))]
819 fn package_option(#[case] s: &str, #[case] expected: PackageOption) {
820 let result = PackageOption::from_str(s).expect("Parser should be successful");
821 assert_eq!(result, expected);
822 }
823
824 #[rstest]
825 #[case(
826 "!somethingelse",
827 "expected `autodeps`, `debug`, `docs`, `emptydirs`, `libtool`, `lto`, `purge`, `staticlibs`, `strip`, `zipman`"
828 )]
829 #[case(
830 "#somethingelse",
831 "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
832 )]
833 fn invalid_package_option(#[case] input: &str, #[case] err_snippet: &str) {
834 let Err(Error::ParseError(err_msg)) = PackageOption::from_str(input) else {
835 panic!("'{input}' erroneously parsed as VersionRequirement")
836 };
837 assert!(
838 err_msg.contains(err_snippet),
839 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
840 );
841 }
842
843 #[rstest]
844 #[case("buildflags", BuildEnvironmentOption::BuildFlags(true))]
845 #[case("ccache", BuildEnvironmentOption::Ccache(true))]
846 #[case("check", BuildEnvironmentOption::Check(true))]
847 #[case("color", BuildEnvironmentOption::Color(true))]
848 #[case("distcc", BuildEnvironmentOption::Distcc(true))]
849 #[case("!makeflags", BuildEnvironmentOption::MakeFlags(false))]
850 #[case("sign", BuildEnvironmentOption::Sign(true))]
851 #[case("!sign", BuildEnvironmentOption::Sign(false))]
852 fn build_environment_option(#[case] input: &str, #[case] expected: BuildEnvironmentOption) {
853 let result = BuildEnvironmentOption::from_str(input).expect("Parser should be successful");
854 assert_eq!(result, expected);
855 }
856
857 #[rstest]
858 #[case(
859 "!somethingelse",
860 "expected `buildflags`, `ccache`, `check`, `color`, `distcc`, `sign`, `makeflags`"
861 )]
862 #[case(
863 "#somethingelse",
864 "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
865 )]
866 fn invalid_build_environment_option(#[case] input: &str, #[case] err_snippet: &str) {
867 let Err(Error::ParseError(err_msg)) = BuildEnvironmentOption::from_str(input) else {
868 panic!("'{input}' erroneously parsed as VersionRequirement")
869 };
870 assert!(
871 err_msg.contains(err_snippet),
872 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
873 );
874 }
875
876 #[rstest]
877 #[case("#test", "invalid character in makepkg option")]
878 #[case("test!", "invalid character in makepkg option")]
879 fn invalid_option(#[case] input: &str, #[case] error_snippet: &str) {
880 let result = option_name_parser.parse(input);
881 assert!(result.is_err(), "Expected makepkg option parsing to fail");
882 let err = result.unwrap_err();
883 let pretty_error = err.to_string();
884 assert!(
885 pretty_error.contains(error_snippet),
886 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
887 );
888 }
889
890 #[rstest]
891 #[case(
892 "foo-bar-1:1.0.0-1-any",
893 InstalledPackage {
894 name: Name::new("foo-bar")?,
895 version: FullVersion::from_str("1:1.0.0-1")?,
896 architecture: Architecture::Any,
897 },
898 )]
899 #[case(
900 "foobar-1.0.0-1-x86_64",
901 InstalledPackage {
902 name: Name::new("foobar")?,
903 version: FullVersion::from_str("1.0.0-1")?,
904 architecture: SystemArchitecture::X86_64.into(),
905 },
906 )]
907 fn installed_from_str(#[case] s: &str, #[case] result: InstalledPackage) -> TestResult {
908 assert_eq!(InstalledPackage::from_str(s), Ok(result));
909 Ok(())
910 }
911
912 #[rstest]
913 #[case("foo-1:1.0.0-bar-any", "invalid package release")]
914 #[case(
915 "foo-1:1.0.0_any",
916 "expected a package name, followed by an alpm-package-version (full or full with epoch) and an architecture."
917 )]
918 #[case("packagename-30-0.1oops-any", "expected end of package release value")]
919 #[case("package$with$dollars-30-0.1-any", "invalid character in package name")]
920 #[case(
921 "packagename-30-0.1-any*asdf",
922 "invalid character in system architecture"
923 )]
924 fn installed_new_parse_error(#[case] input: &str, #[case] error_snippet: &str) {
925 let result = InstalledPackage::from_str(input);
926 assert!(result.is_err(), "Expected InstalledPackage parsing to fail");
927 let err = result.unwrap_err();
928 let pretty_error = err.to_string();
929 assert!(
930 pretty_error.contains(error_snippet),
931 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
932 );
933 }
934}