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 "autodeps".value(Self::AutoDeps(on)),
417 "debug".value(Self::Debug(on)),
418 "docs".value(Self::Docs(on)),
419 "emptydirs".value(Self::EmptyDirs(on)),
420 "libtool".value(Self::Libtool(on)),
421 "lto".value(Self::Lto(on)),
422 "pestrip".value(Self::PEStrip(on)),
423 "purge".value(Self::Purge(on)),
424 "staticlibs".value(Self::StaticLibs(on)),
425 "strip".value(Self::Strip(on)),
426 "zipman".value(Self::Zipman(on)),
427 fail.context(StrContext::Label("makepkg packaging option"))
428 .context_with(iter_str_context!([PackageOption::VARIANTS])),
429 ))
430 .parse_next(&mut name)
431 }
432}
433
434impl FromStr for PackageOption {
435 type Err = Error;
436 fn from_str(s: &str) -> Result<Self, Self::Err> {
444 Ok(Self::parser.parse(s)?)
445 }
446}
447
448impl Display for PackageOption {
449 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
450 write!(fmt, "{}{}", if self.on() { "" } else { "!" }, self.name())
451 }
452}
453
454#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
485pub struct InstalledPackage {
486 name: Name,
487 version: FullVersion,
488 architecture: Architecture,
489}
490
491impl InstalledPackage {
492 pub fn new(name: Name, version: FullVersion, architecture: Architecture) -> Self {
511 Self {
512 name,
513 version,
514 architecture,
515 }
516 }
517
518 pub fn name(&self) -> &Name {
536 &self.name
537 }
538
539 pub fn version(&self) -> &FullVersion {
557 &self.version
558 }
559
560 pub fn architecture(&self) -> &Architecture {
578 &self.architecture
579 }
580
581 pub fn to_package_relation(&self) -> PackageRelation {
602 PackageRelation {
603 name: self.name.clone(),
604 version_requirement: Some(VersionRequirement {
605 comparison: VersionComparison::Equal,
606 version: self.version.clone().into(),
607 }),
608 }
609 }
610
611 pub fn parser(input: &mut &str) -> ModalResult<Self> {
637 let dashes: usize = input.chars().filter(|char| char == &'-').count();
648
649 if dashes < 2 {
650 let context_error = ContextError::from_input(input)
651 .add_context(
652 input,
653 &input.checkpoint(),
654 StrContext::Label("alpm-package file name"),
655 )
656 .add_context(
657 input,
658 &input.checkpoint(),
659 StrContext::Expected(StrContextValue::Description(
660 concat!(
661 "a package name, followed by an alpm-package-version (full or full with epoch) and an architecture.",
662 "\nAll components must be delimited with a dash ('-')."
663 )
664 ))
665 );
666
667 return Err(ErrMode::Cut(context_error));
668 }
669
670 let dashes_in_name = dashes.saturating_sub(3);
672
673 let name = cut_err(
677 repeat::<_, _, (), _, _>(
678 dashes_in_name + 1,
679 (opt("-"), take_until(0.., "-"), peek("-")),
684 )
685 .take()
686 .and_then(Name::parser),
688 )
689 .context(StrContext::Label("alpm-package-name"))
690 .parse_next(input)?;
691
692 "-".parse_next(input)?;
695
696 let version: FullVersion = cut_err((take_until(0.., "-"), "-", take_until(0.., "-")))
699 .context(StrContext::Label("alpm-package-version"))
700 .context(StrContext::Expected(StrContextValue::Description(
701 "an alpm-package-version (full or full with epoch) followed by a `-` and an architecture",
702 )))
703 .take()
704 .and_then(cut_err(FullVersion::parser))
705 .parse_next(input)?;
706
707 "-".parse_next(input)?;
710
711 let architecture = rest.and_then(Architecture::parser).parse_next(input)?;
714
715 Ok(Self {
716 name,
717 version,
718 architecture,
719 })
720 }
721}
722
723impl From<PackageFileName> for InstalledPackage {
724 fn from(value: PackageFileName) -> Self {
726 Self {
727 name: value.name,
728 version: value.version,
729 architecture: value.architecture,
730 }
731 }
732}
733
734impl FromStr for InstalledPackage {
735 type Err = Error;
736
737 fn from_str(s: &str) -> Result<InstalledPackage, Self::Err> {
759 Ok(Self::parser.parse(s)?)
760 }
761}
762
763impl Display for InstalledPackage {
764 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
765 write!(fmt, "{}-{}-{}", self.name, self.version, self.architecture)
766 }
767}
768
769#[cfg(test)]
770mod tests {
771 use rstest::rstest;
772 use testresult::TestResult;
773
774 use super::*;
775 use crate::SystemArchitecture;
776
777 #[rstest]
778 #[case(
779 "!makeflags",
780 MakepkgOption::BuildEnvironment(BuildEnvironmentOption::MakeFlags(false))
781 )]
782 #[case("autodeps", MakepkgOption::Package(PackageOption::AutoDeps(true)))]
783 #[case(
784 "ccache",
785 MakepkgOption::BuildEnvironment(BuildEnvironmentOption::Ccache(true))
786 )]
787 fn makepkg_option(#[case] input: &str, #[case] expected: MakepkgOption) {
788 let result = MakepkgOption::from_str(input).expect("Parser should be successful");
789 assert_eq!(result, expected);
790 }
791
792 #[rstest]
793 #[case(
794 "!somethingelse",
795 concat!(
796 "expected `buildflags`, `ccache`, `check`, `color`, `distcc`, `sign`, `makeflags`, ",
797 "`autodeps`, `debug`, `docs`, `emptydirs`, `libtool`, `lto`, `pestrip`, `purge`, ",
798 "`staticlibs`, `strip`, `zipman`",
799 )
800 )]
801 #[case(
802 "#somethingelse",
803 "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
804 )]
805 fn invalid_makepkg_option(#[case] input: &str, #[case] err_snippet: &str) {
806 let Err(Error::ParseError(err_msg)) = MakepkgOption::from_str(input) else {
807 panic!("'{input}' erroneously parsed as VersionRequirement")
808 };
809 assert!(
810 err_msg.contains(err_snippet),
811 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
812 );
813 }
814
815 #[rstest]
816 #[case("autodeps", PackageOption::AutoDeps(true))]
817 #[case("debug", PackageOption::Debug(true))]
818 #[case("docs", PackageOption::Docs(true))]
819 #[case("emptydirs", PackageOption::EmptyDirs(true))]
820 #[case("!libtool", PackageOption::Libtool(false))]
821 #[case("lto", PackageOption::Lto(true))]
822 #[case("pestrip", PackageOption::PEStrip(true))]
823 #[case("purge", PackageOption::Purge(true))]
824 #[case("staticlibs", PackageOption::StaticLibs(true))]
825 #[case("strip", PackageOption::Strip(true))]
826 #[case("zipman", PackageOption::Zipman(true))]
827 fn package_option(#[case] s: &str, #[case] expected: PackageOption) {
828 let result = PackageOption::from_str(s).expect("Parser should be successful");
829 assert_eq!(result, expected);
830 }
831
832 #[rstest]
833 #[case(
834 "!somethingelse",
835 "expected `autodeps`, `debug`, `docs`, `emptydirs`, `libtool`, `lto`, `pestrip`, `purge`, `staticlibs`, `strip`, `zipman`"
836 )]
837 #[case(
838 "#somethingelse",
839 "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
840 )]
841 fn invalid_package_option(#[case] input: &str, #[case] err_snippet: &str) {
842 let Err(Error::ParseError(err_msg)) = PackageOption::from_str(input) else {
843 panic!("'{input}' erroneously parsed as VersionRequirement")
844 };
845 assert!(
846 err_msg.contains(err_snippet),
847 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
848 );
849 }
850
851 #[rstest]
852 #[case("buildflags", BuildEnvironmentOption::BuildFlags(true))]
853 #[case("ccache", BuildEnvironmentOption::Ccache(true))]
854 #[case("check", BuildEnvironmentOption::Check(true))]
855 #[case("color", BuildEnvironmentOption::Color(true))]
856 #[case("distcc", BuildEnvironmentOption::Distcc(true))]
857 #[case("!makeflags", BuildEnvironmentOption::MakeFlags(false))]
858 #[case("sign", BuildEnvironmentOption::Sign(true))]
859 #[case("!sign", BuildEnvironmentOption::Sign(false))]
860 fn build_environment_option(#[case] input: &str, #[case] expected: BuildEnvironmentOption) {
861 let result = BuildEnvironmentOption::from_str(input).expect("Parser should be successful");
862 assert_eq!(result, expected);
863 }
864
865 #[rstest]
866 #[case(
867 "!somethingelse",
868 "expected `buildflags`, `ccache`, `check`, `color`, `distcc`, `sign`, `makeflags`"
869 )]
870 #[case(
871 "#somethingelse",
872 "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
873 )]
874 fn invalid_build_environment_option(#[case] input: &str, #[case] err_snippet: &str) {
875 let Err(Error::ParseError(err_msg)) = BuildEnvironmentOption::from_str(input) else {
876 panic!("'{input}' erroneously parsed as VersionRequirement")
877 };
878 assert!(
879 err_msg.contains(err_snippet),
880 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
881 );
882 }
883
884 #[rstest]
885 #[case("#test", "invalid character in makepkg option")]
886 #[case("test!", "invalid character in makepkg option")]
887 fn invalid_option(#[case] input: &str, #[case] error_snippet: &str) {
888 let result = option_name_parser.parse(input);
889 assert!(result.is_err(), "Expected makepkg option parsing to fail");
890 let err = result.unwrap_err();
891 let pretty_error = err.to_string();
892 assert!(
893 pretty_error.contains(error_snippet),
894 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
895 );
896 }
897
898 #[rstest]
899 #[case(
900 "foo-bar-1:1.0.0-1-any",
901 InstalledPackage {
902 name: Name::new("foo-bar")?,
903 version: FullVersion::from_str("1:1.0.0-1")?,
904 architecture: Architecture::Any,
905 },
906 )]
907 #[case(
908 "foobar-1.0.0-1-x86_64",
909 InstalledPackage {
910 name: Name::new("foobar")?,
911 version: FullVersion::from_str("1.0.0-1")?,
912 architecture: SystemArchitecture::X86_64.into(),
913 },
914 )]
915 fn installed_from_str(#[case] s: &str, #[case] result: InstalledPackage) -> TestResult {
916 assert_eq!(InstalledPackage::from_str(s), Ok(result));
917 Ok(())
918 }
919
920 #[rstest]
921 #[case("foo-1:1.0.0-bar-any", "invalid package release")]
922 #[case(
923 "foo-1:1.0.0_any",
924 "expected a package name, followed by an alpm-package-version (full or full with epoch) and an architecture."
925 )]
926 #[case("packagename-30-0.1oops-any", "expected end of package release value")]
927 #[case("package$with$dollars-30-0.1-any", "invalid character in package name")]
928 #[case(
929 "packagename-30-0.1-any*asdf",
930 "invalid character in system architecture"
931 )]
932 fn installed_new_parse_error(#[case] input: &str, #[case] error_snippet: &str) {
933 let result = InstalledPackage::from_str(input);
934 assert!(result.is_err(), "Expected InstalledPackage parsing to fail");
935 let err = result.unwrap_err();
936 let pretty_error = err.to_string();
937 assert!(
938 pretty_error.contains(error_snippet),
939 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
940 );
941 }
942}