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 winnow::{
9 ModalResult,
10 Parser,
11 combinator::{alt, cut_err, eof, fail, opt, peek, repeat},
12 error::{
13 AddContext,
14 ContextError,
15 ErrMode,
16 ParserError,
17 StrContext,
18 StrContextValue::{self, *},
19 },
20 stream::Stream,
21 token::{one_of, rest, take_until},
22};
23
24use crate::{Architecture, FullVersion, Name, PackageFileName, error::Error};
25
26fn option_bool_parser(input: &mut &str) -> ModalResult<bool> {
40 let alphanum = |c: char| c.is_ascii_alphanumeric();
41 let special_first_chars = ['-', '.', '_', '!'];
42 let valid_chars = one_of((alphanum, special_first_chars));
43
44 cut_err(peek(valid_chars))
46 .context(StrContext::Expected(CharLiteral('!')))
47 .context(StrContext::Expected(Description(
48 "ASCII alphanumeric character",
49 )))
50 .context_with(iter_char_context!(special_first_chars))
51 .parse_next(input)?;
52
53 Ok(opt('!').parse_next(input)?.is_none())
54}
55
56fn option_name_parser<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
68 let alphanum = |c: char| c.is_ascii_alphanumeric();
69
70 let special_chars = ['-', '.', '_'];
71 let valid_chars = one_of((alphanum, special_chars));
72 let name = repeat::<_, _, (), _, _>(0.., valid_chars)
73 .take()
74 .parse_next(input)?;
75
76 eof.context(StrContext::Label("character in makepkg option"))
77 .context(StrContext::Expected(Description(
78 "ASCII alphanumeric character",
79 )))
80 .context_with(iter_char_context!(special_chars))
81 .parse_next(input)?;
82
83 Ok(name)
84}
85
86#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
94#[serde(tag = "type", rename_all = "snake_case")]
95pub enum MakepkgOption {
96 BuildEnvironment(BuildEnvironmentOption),
98 Package(PackageOption),
100}
101
102impl MakepkgOption {
103 pub fn parser(input: &mut &str) -> ModalResult<Self> {
112 alt((
113 BuildEnvironmentOption::parser.map(MakepkgOption::BuildEnvironment),
114 PackageOption::parser.map(MakepkgOption::Package),
115 fail.context(StrContext::Label("packaging or build environment option"))
116 .context_with(iter_str_context!([
117 BuildEnvironmentOption::VARIANTS.to_vec(),
118 PackageOption::VARIANTS.to_vec()
119 ])),
120 ))
121 .parse_next(input)
122 }
123}
124
125impl FromStr for MakepkgOption {
126 type Err = Error;
127 fn from_str(s: &str) -> Result<Self, Self::Err> {
129 Ok(Self::parser.parse(s)?)
130 }
131}
132
133impl Display for MakepkgOption {
134 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
135 match self {
136 MakepkgOption::BuildEnvironment(option) => write!(fmt, "{option}"),
137 MakepkgOption::Package(option) => write!(fmt, "{option}"),
138 }
139 }
140}
141
142#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
165#[serde(rename_all = "lowercase")]
166pub enum BuildEnvironmentOption {
167 BuildFlags(bool),
172 Ccache(bool),
174 Check(bool),
176 Color(bool),
178 Distcc(bool),
180 Sign(bool),
182 MakeFlags(bool),
187}
188
189impl BuildEnvironmentOption {
190 pub fn new(option: &str) -> Result<Self, Error> {
196 Self::from_str(option)
197 }
198
199 pub fn name(&self) -> &str {
201 match self {
202 Self::BuildFlags(_) => "buildflags",
203 Self::Ccache(_) => "ccache",
204 Self::Check(_) => "check",
205 Self::Color(_) => "color",
206 Self::Distcc(_) => "distcc",
207 Self::MakeFlags(_) => "makeflags",
208 Self::Sign(_) => "sign",
209 }
210 }
211
212 pub fn on(&self) -> bool {
214 match self {
215 Self::BuildFlags(on)
216 | Self::Ccache(on)
217 | Self::Check(on)
218 | Self::Color(on)
219 | Self::Distcc(on)
220 | Self::MakeFlags(on)
221 | Self::Sign(on) => *on,
222 }
223 }
224
225 const VARIANTS: [&str; 7] = [
226 "buildflags",
227 "ccache",
228 "check",
229 "color",
230 "distcc",
231 "makeflags",
232 "sign",
233 ];
234
235 pub fn parser(input: &mut &str) -> ModalResult<Self> {
243 let on = option_bool_parser.parse_next(input)?;
244 let mut name = option_name_parser.parse_next(input)?;
245
246 let name = alt(BuildEnvironmentOption::VARIANTS)
247 .context(StrContext::Label("makepkg build environment option"))
248 .context_with(iter_str_context!([BuildEnvironmentOption::VARIANTS]))
249 .parse_next(&mut name)?;
250
251 match name {
252 "buildflags" => Ok(Self::BuildFlags(on)),
253 "ccache" => Ok(Self::Ccache(on)),
254 "check" => Ok(Self::Check(on)),
255 "color" => Ok(Self::Color(on)),
256 "distcc" => Ok(Self::Distcc(on)),
257 "makeflags" => Ok(Self::MakeFlags(on)),
258 "sign" => Ok(Self::Sign(on)),
259 _ => unreachable!(),
261 }
262 }
263}
264
265impl FromStr for BuildEnvironmentOption {
266 type Err = Error;
267 fn from_str(s: &str) -> Result<Self, Self::Err> {
275 Ok(Self::parser.parse(s)?)
276 }
277}
278
279impl Display for BuildEnvironmentOption {
280 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
281 write!(fmt, "{}{}", if self.on() { "" } else { "!" }, self.name())
282 }
283}
284
285#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
308#[serde(rename_all = "lowercase")]
309pub enum PackageOption {
310 AutoDeps(bool),
314
315 Debug(bool),
317
318 Docs(bool),
320
321 EmptyDirs(bool),
323
324 Libtool(bool),
326
327 Lto(bool),
329
330 Purge(bool),
332
333 StaticLibs(bool),
335
336 Strip(bool),
338
339 Zipman(bool),
341}
342
343impl PackageOption {
344 pub fn new(option: &str) -> Result<Self, Error> {
350 Self::from_str(option)
351 }
352
353 pub fn name(&self) -> &str {
355 match self {
356 Self::AutoDeps(_) => "autodeps",
357 Self::Debug(_) => "debug",
358 Self::Docs(_) => "docs",
359 Self::EmptyDirs(_) => "emptydirs",
360 Self::Libtool(_) => "libtool",
361 Self::Lto(_) => "lto",
362 Self::Purge(_) => "purge",
363 Self::StaticLibs(_) => "staticlibs",
364 Self::Strip(_) => "strip",
365 Self::Zipman(_) => "zipman",
366 }
367 }
368
369 pub fn on(&self) -> bool {
371 match self {
372 Self::AutoDeps(on)
373 | Self::Debug(on)
374 | Self::Docs(on)
375 | Self::EmptyDirs(on)
376 | Self::Libtool(on)
377 | Self::Lto(on)
378 | Self::Purge(on)
379 | Self::StaticLibs(on)
380 | Self::Strip(on)
381 | Self::Zipman(on) => *on,
382 }
383 }
384
385 const VARIANTS: [&str; 11] = [
386 "autodeps",
387 "debug",
388 "docs",
389 "emptydirs",
390 "libtool",
391 "lto",
392 "debug",
393 "purge",
394 "staticlibs",
395 "strip",
396 "zipman",
397 ];
398
399 pub fn parser(input: &mut &str) -> ModalResult<Self> {
407 let on = option_bool_parser.parse_next(input)?;
408 let mut name = option_name_parser.parse_next(input)?;
409
410 let value = alt(PackageOption::VARIANTS)
411 .context(StrContext::Label("makepkg packaging option"))
412 .context_with(iter_str_context!([PackageOption::VARIANTS]))
413 .parse_next(&mut name)?;
414
415 match value {
416 "autodeps" => Ok(Self::AutoDeps(on)),
417 "debug" => Ok(Self::Debug(on)),
418 "docs" => Ok(Self::Docs(on)),
419 "emptydirs" => Ok(Self::EmptyDirs(on)),
420 "libtool" => Ok(Self::Libtool(on)),
421 "lto" => Ok(Self::Lto(on)),
422 "purge" => Ok(Self::Purge(on)),
423 "staticlibs" => Ok(Self::StaticLibs(on)),
424 "strip" => Ok(Self::Strip(on)),
425 "zipman" => Ok(Self::Zipman(on)),
426 _ => unreachable!(),
428 }
429 }
430}
431
432impl FromStr for PackageOption {
433 type Err = Error;
434 fn from_str(s: &str) -> Result<Self, Self::Err> {
442 Ok(Self::parser.parse(s)?)
443 }
444}
445
446impl Display for PackageOption {
447 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
448 write!(fmt, "{}{}", if self.on() { "" } else { "!" }, self.name())
449 }
450}
451
452#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
483pub struct InstalledPackage {
484 name: Name,
485 version: FullVersion,
486 architecture: Architecture,
487}
488
489impl InstalledPackage {
490 pub fn new(name: Name, version: FullVersion, architecture: Architecture) -> Self {
509 Self {
510 name,
511 version,
512 architecture,
513 }
514 }
515
516 pub fn name(&self) -> &Name {
534 &self.name
535 }
536
537 pub fn version(&self) -> &FullVersion {
555 &self.version
556 }
557
558 pub fn architecture(&self) -> Architecture {
576 self.architecture
577 }
578
579 pub fn parser(input: &mut &str) -> ModalResult<Self> {
605 let dashes: usize = input.chars().filter(|char| char == &'-').count();
616
617 if dashes < 2 {
618 let context_error = ContextError::from_input(input)
619 .add_context(
620 input,
621 &input.checkpoint(),
622 StrContext::Label("alpm-package file name"),
623 )
624 .add_context(
625 input,
626 &input.checkpoint(),
627 StrContext::Expected(StrContextValue::Description(
628 concat!(
629 "a package name, followed by an alpm-package-version (full or full with epoch) and an architecture.",
630 "\nAll components must be delimited with a dash ('-')."
631 )
632 ))
633 );
634
635 return Err(ErrMode::Cut(context_error));
636 }
637
638 let dashes_in_name = dashes.saturating_sub(3);
640
641 let name = cut_err(
645 repeat::<_, _, (), _, _>(
646 dashes_in_name + 1,
647 (opt("-"), take_until(0.., "-"), peek("-")),
652 )
653 .take()
654 .and_then(Name::parser),
656 )
657 .context(StrContext::Label("alpm-package-name"))
658 .parse_next(input)?;
659
660 "-".parse_next(input)?;
663
664 let version: FullVersion = cut_err((take_until(0.., "-"), "-", take_until(0.., "-")))
667 .context(StrContext::Label("alpm-package-version"))
668 .context(StrContext::Expected(StrContextValue::Description(
669 "an alpm-package-version (full or full with epoch) followed by a `-` and an architecture",
670 )))
671 .take()
672 .and_then(cut_err(FullVersion::parser))
673 .parse_next(input)?;
674
675 "-".parse_next(input)?;
678
679 let architecture = rest.and_then(Architecture::parser).parse_next(input)?;
682
683 Ok(Self {
684 name,
685 version,
686 architecture,
687 })
688 }
689}
690
691impl From<PackageFileName> for InstalledPackage {
692 fn from(value: PackageFileName) -> Self {
694 Self {
695 name: value.name,
696 version: value.version,
697 architecture: value.architecture,
698 }
699 }
700}
701
702impl FromStr for InstalledPackage {
703 type Err = Error;
704
705 fn from_str(s: &str) -> Result<InstalledPackage, Self::Err> {
727 Ok(Self::parser.parse(s)?)
728 }
729}
730
731impl Display for InstalledPackage {
732 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
733 write!(fmt, "{}-{}-{}", self.name, self.version, self.architecture)
734 }
735}
736
737#[cfg(test)]
738mod tests {
739 use rstest::rstest;
740 use testresult::TestResult;
741
742 use super::*;
743
744 #[rstest]
745 #[case(
746 "!makeflags",
747 MakepkgOption::BuildEnvironment(BuildEnvironmentOption::MakeFlags(false))
748 )]
749 #[case("autodeps", MakepkgOption::Package(PackageOption::AutoDeps(true)))]
750 #[case(
751 "ccache",
752 MakepkgOption::BuildEnvironment(BuildEnvironmentOption::Ccache(true))
753 )]
754 fn makepkg_option(#[case] input: &str, #[case] expected: MakepkgOption) {
755 let result = MakepkgOption::from_str(input).expect("Parser should be successful");
756 assert_eq!(result, expected);
757 }
758
759 #[rstest]
760 #[case(
761 "!somethingelse",
762 concat!(
763 "expected `buildflags`, `ccache`, `check`, `color`, `distcc`, `makeflags`, `sign`, ",
764 "`autodeps`, `debug`, `docs`, `emptydirs`, `libtool`, `lto`, `debug`, `purge`, ",
765 "`staticlibs`, `strip`, `zipman`",
766 )
767 )]
768 #[case(
769 "#somethingelse",
770 "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
771 )]
772 fn invalid_makepkg_option(#[case] input: &str, #[case] err_snippet: &str) {
773 let Err(Error::ParseError(err_msg)) = MakepkgOption::from_str(input) else {
774 panic!("'{input}' erroneously parsed as VersionRequirement")
775 };
776 assert!(
777 err_msg.contains(err_snippet),
778 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
779 );
780 }
781
782 #[rstest]
783 #[case("autodeps", PackageOption::AutoDeps(true))]
784 #[case("debug", PackageOption::Debug(true))]
785 #[case("docs", PackageOption::Docs(true))]
786 #[case("emptydirs", PackageOption::EmptyDirs(true))]
787 #[case("!libtool", PackageOption::Libtool(false))]
788 #[case("lto", PackageOption::Lto(true))]
789 #[case("purge", PackageOption::Purge(true))]
790 #[case("staticlibs", PackageOption::StaticLibs(true))]
791 #[case("strip", PackageOption::Strip(true))]
792 #[case("zipman", PackageOption::Zipman(true))]
793 fn package_option(#[case] s: &str, #[case] expected: PackageOption) {
794 let result = PackageOption::from_str(s).expect("Parser should be successful");
795 assert_eq!(result, expected);
796 }
797
798 #[rstest]
799 #[case(
800 "!somethingelse",
801 "expected `autodeps`, `debug`, `docs`, `emptydirs`, `libtool`, `lto`, `debug`, `purge`, `staticlibs`, `strip`, `zipman`"
802 )]
803 #[case(
804 "#somethingelse",
805 "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
806 )]
807 fn invalid_package_option(#[case] input: &str, #[case] err_snippet: &str) {
808 let Err(Error::ParseError(err_msg)) = PackageOption::from_str(input) else {
809 panic!("'{input}' erroneously parsed as VersionRequirement")
810 };
811 assert!(
812 err_msg.contains(err_snippet),
813 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
814 );
815 }
816
817 #[rstest]
818 #[case("buildflags", BuildEnvironmentOption::BuildFlags(true))]
819 #[case("ccache", BuildEnvironmentOption::Ccache(true))]
820 #[case("check", BuildEnvironmentOption::Check(true))]
821 #[case("color", BuildEnvironmentOption::Color(true))]
822 #[case("distcc", BuildEnvironmentOption::Distcc(true))]
823 #[case("!makeflags", BuildEnvironmentOption::MakeFlags(false))]
824 #[case("sign", BuildEnvironmentOption::Sign(true))]
825 #[case("!sign", BuildEnvironmentOption::Sign(false))]
826 fn build_environment_option(#[case] input: &str, #[case] expected: BuildEnvironmentOption) {
827 let result = BuildEnvironmentOption::from_str(input).expect("Parser should be successful");
828 assert_eq!(result, expected);
829 }
830
831 #[rstest]
832 #[case(
833 "!somethingelse",
834 "expected `buildflags`, `ccache`, `check`, `color`, `distcc`, `makeflags`, `sign`"
835 )]
836 #[case(
837 "#somethingelse",
838 "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
839 )]
840 fn invalid_build_environment_option(#[case] input: &str, #[case] err_snippet: &str) {
841 let Err(Error::ParseError(err_msg)) = BuildEnvironmentOption::from_str(input) else {
842 panic!("'{input}' erroneously parsed as VersionRequirement")
843 };
844 assert!(
845 err_msg.contains(err_snippet),
846 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
847 );
848 }
849
850 #[rstest]
851 #[case("#test", "invalid character in makepkg option")]
852 #[case("test!", "invalid character in makepkg option")]
853 fn invalid_option(#[case] input: &str, #[case] error_snippet: &str) {
854 let result = option_name_parser.parse(input);
855 assert!(result.is_err(), "Expected makepkg option parsing to fail");
856 let err = result.unwrap_err();
857 let pretty_error = err.to_string();
858 assert!(
859 pretty_error.contains(error_snippet),
860 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
861 );
862 }
863
864 #[rstest]
865 #[case(
866 "foo-bar-1:1.0.0-1-any",
867 InstalledPackage {
868 name: Name::new("foo-bar")?,
869 version: FullVersion::from_str("1:1.0.0-1")?,
870 architecture: Architecture::Any,
871 },
872 )]
873 #[case(
874 "foobar-1.0.0-1-x86_64",
875 InstalledPackage {
876 name: Name::new("foobar")?,
877 version: FullVersion::from_str("1.0.0-1")?,
878 architecture: Architecture::X86_64,
879 },
880 )]
881 fn installed_from_str(#[case] s: &str, #[case] result: InstalledPackage) -> TestResult {
882 assert_eq!(InstalledPackage::from_str(s), Ok(result));
883 Ok(())
884 }
885
886 #[rstest]
887 #[case("foo-1:1.0.0-bar-any", "invalid package release")]
888 #[case(
889 "foo-1:1.0.0_any",
890 "expected a package name, followed by an alpm-package-version (full or full with epoch) and an architecture."
891 )]
892 #[case("packagename-30-0.1oops-any", "expected end of package release value")]
893 #[case("package$with$dollars-30-0.1-any", "invalid character in package name")]
894 #[case("packagename-30-0.1-any*asdf", "invalid architecture")]
895 fn installed_new_parse_error(#[case] input: &str, #[case] error_snippet: &str) {
896 let result = InstalledPackage::from_str(input);
897 assert!(result.is_err(), "Expected InstalledPackage parsing to fail");
898 let err = result.unwrap_err();
899 let pretty_error = err.to_string();
900 assert!(
901 pretty_error.contains(error_snippet),
902 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
903 );
904 }
905}