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::{Architecture, FullVersion, Name, PackageFileName, error::Error};
26
27fn option_bool_parser(input: &mut &str) -> ModalResult<bool> {
41 let alphanum = |c: char| c.is_ascii_alphanumeric();
42 let special_first_chars = ['-', '.', '_', '!'];
43 let valid_chars = one_of((alphanum, special_first_chars));
44
45 cut_err(peek(valid_chars))
47 .context(StrContext::Expected(CharLiteral('!')))
48 .context(StrContext::Expected(Description(
49 "ASCII alphanumeric character",
50 )))
51 .context_with(iter_char_context!(special_first_chars))
52 .parse_next(input)?;
53
54 Ok(opt('!').parse_next(input)?.is_none())
55}
56
57fn option_name_parser<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
69 let alphanum = |c: char| c.is_ascii_alphanumeric();
70
71 let special_chars = ['-', '.', '_'];
72 let valid_chars = one_of((alphanum, special_chars));
73 let name = repeat::<_, _, (), _, _>(0.., valid_chars)
74 .take()
75 .parse_next(input)?;
76
77 eof.context(StrContext::Label("character in makepkg option"))
78 .context(StrContext::Expected(Description(
79 "ASCII alphanumeric character",
80 )))
81 .context_with(iter_char_context!(special_chars))
82 .parse_next(input)?;
83
84 Ok(name)
85}
86
87#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
95#[serde(tag = "type", rename_all = "snake_case")]
96pub enum MakepkgOption {
97 BuildEnvironment(BuildEnvironmentOption),
99 Package(PackageOption),
101}
102
103impl MakepkgOption {
104 pub fn parser(input: &mut &str) -> ModalResult<Self> {
113 alt((
114 BuildEnvironmentOption::parser.map(MakepkgOption::BuildEnvironment),
115 PackageOption::parser.map(MakepkgOption::Package),
116 fail.context(StrContext::Label("packaging or build environment option"))
117 .context_with(iter_str_context!([
118 BuildEnvironmentOption::VARIANTS.to_vec(),
119 PackageOption::VARIANTS.to_vec()
120 ])),
121 ))
122 .parse_next(input)
123 }
124}
125
126impl FromStr for MakepkgOption {
127 type Err = Error;
128 fn from_str(s: &str) -> Result<Self, Self::Err> {
130 Ok(Self::parser.parse(s)?)
131 }
132}
133
134impl Display for MakepkgOption {
135 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
136 match self {
137 MakepkgOption::BuildEnvironment(option) => write!(fmt, "{option}"),
138 MakepkgOption::Package(option) => write!(fmt, "{option}"),
139 }
140 }
141}
142
143#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, VariantNames)]
166#[serde(rename_all = "lowercase")]
167pub enum BuildEnvironmentOption {
168 #[strum(serialize = "buildflags")]
173 BuildFlags(bool),
174 #[strum(serialize = "ccache")]
176 Ccache(bool),
177 #[strum(serialize = "check")]
179 Check(bool),
180 #[strum(serialize = "color")]
182 Color(bool),
183 #[strum(serialize = "distcc")]
185 Distcc(bool),
186 #[strum(serialize = "sign")]
188 Sign(bool),
189 #[strum(serialize = "makeflags")]
194 MakeFlags(bool),
195}
196
197impl BuildEnvironmentOption {
198 pub fn new(option: &str) -> Result<Self, Error> {
204 Self::from_str(option)
205 }
206
207 pub fn name(&self) -> &str {
209 match self {
210 Self::BuildFlags(_) => "buildflags",
211 Self::Ccache(_) => "ccache",
212 Self::Check(_) => "check",
213 Self::Color(_) => "color",
214 Self::Distcc(_) => "distcc",
215 Self::MakeFlags(_) => "makeflags",
216 Self::Sign(_) => "sign",
217 }
218 }
219
220 pub fn on(&self) -> bool {
222 match self {
223 Self::BuildFlags(on)
224 | Self::Ccache(on)
225 | Self::Check(on)
226 | Self::Color(on)
227 | Self::Distcc(on)
228 | Self::MakeFlags(on)
229 | Self::Sign(on) => *on,
230 }
231 }
232
233 pub fn parser(input: &mut &str) -> ModalResult<Self> {
241 let on = option_bool_parser.parse_next(input)?;
242 let mut name = option_name_parser.parse_next(input)?;
243
244 alt((
245 "buildflags".value(Self::BuildFlags(on)),
246 "ccache".value(Self::Ccache(on)),
247 "check".value(Self::Check(on)),
248 "color".value(Self::Color(on)),
249 "distcc".value(Self::Distcc(on)),
250 "makeflags".value(Self::MakeFlags(on)),
251 "sign".value(Self::Sign(on)),
252 fail.context(StrContext::Label("makepkg build environment option"))
253 .context_with(iter_str_context!([BuildEnvironmentOption::VARIANTS])),
254 ))
255 .parse_next(&mut name)
256 }
257}
258
259impl FromStr for BuildEnvironmentOption {
260 type Err = Error;
261 fn from_str(s: &str) -> Result<Self, Self::Err> {
269 Ok(Self::parser.parse(s)?)
270 }
271}
272
273impl Display for BuildEnvironmentOption {
274 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
275 write!(fmt, "{}{}", if self.on() { "" } else { "!" }, self.name())
276 }
277}
278
279#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, VariantNames)]
302#[serde(rename_all = "lowercase")]
303pub enum PackageOption {
304 #[strum(serialize = "autodeps")]
308 AutoDeps(bool),
309
310 #[strum(serialize = "debug")]
312 Debug(bool),
313
314 #[strum(serialize = "docs")]
316 Docs(bool),
317
318 #[strum(serialize = "emptydirs")]
320 EmptyDirs(bool),
321
322 #[strum(serialize = "libtool")]
324 Libtool(bool),
325
326 #[strum(serialize = "lto")]
328 Lto(bool),
329
330 #[strum(serialize = "purge")]
332 Purge(bool),
333
334 #[strum(serialize = "staticlibs")]
336 StaticLibs(bool),
337
338 #[strum(serialize = "strip")]
340 Strip(bool),
341
342 #[strum(serialize = "zipman")]
344 Zipman(bool),
345}
346
347impl PackageOption {
348 pub fn new(option: &str) -> Result<Self, Error> {
354 Self::from_str(option)
355 }
356
357 pub fn name(&self) -> &str {
359 match self {
360 Self::AutoDeps(_) => "autodeps",
361 Self::Debug(_) => "debug",
362 Self::Docs(_) => "docs",
363 Self::EmptyDirs(_) => "emptydirs",
364 Self::Libtool(_) => "libtool",
365 Self::Lto(_) => "lto",
366 Self::Purge(_) => "purge",
367 Self::StaticLibs(_) => "staticlibs",
368 Self::Strip(_) => "strip",
369 Self::Zipman(_) => "zipman",
370 }
371 }
372
373 pub fn on(&self) -> bool {
375 match self {
376 Self::AutoDeps(on)
377 | Self::Debug(on)
378 | Self::Docs(on)
379 | Self::EmptyDirs(on)
380 | Self::Libtool(on)
381 | Self::Lto(on)
382 | Self::Purge(on)
383 | Self::StaticLibs(on)
384 | Self::Strip(on)
385 | Self::Zipman(on) => *on,
386 }
387 }
388
389 pub fn parser(input: &mut &str) -> ModalResult<Self> {
397 let on = option_bool_parser.parse_next(input)?;
398 let mut name = option_name_parser.parse_next(input)?;
399
400 alt((
401 "autodeps".value(Self::AutoDeps(on)),
402 "debug".value(Self::Debug(on)),
403 "docs".value(Self::Docs(on)),
404 "emptydirs".value(Self::EmptyDirs(on)),
405 "libtool".value(Self::Libtool(on)),
406 "lto".value(Self::Lto(on)),
407 "purge".value(Self::Purge(on)),
408 "staticlibs".value(Self::StaticLibs(on)),
409 "strip".value(Self::Strip(on)),
410 "zipman".value(Self::Zipman(on)),
411 fail.context(StrContext::Label("makepkg packaging option"))
412 .context_with(iter_str_context!([PackageOption::VARIANTS])),
413 ))
414 .parse_next(&mut name)
415 }
416}
417
418impl FromStr for PackageOption {
419 type Err = Error;
420 fn from_str(s: &str) -> Result<Self, Self::Err> {
428 Ok(Self::parser.parse(s)?)
429 }
430}
431
432impl Display for PackageOption {
433 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
434 write!(fmt, "{}{}", if self.on() { "" } else { "!" }, self.name())
435 }
436}
437
438#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
469pub struct InstalledPackage {
470 name: Name,
471 version: FullVersion,
472 architecture: Architecture,
473}
474
475impl InstalledPackage {
476 pub fn new(name: Name, version: FullVersion, architecture: Architecture) -> Self {
495 Self {
496 name,
497 version,
498 architecture,
499 }
500 }
501
502 pub fn name(&self) -> &Name {
520 &self.name
521 }
522
523 pub fn version(&self) -> &FullVersion {
541 &self.version
542 }
543
544 pub fn architecture(&self) -> Architecture {
562 self.architecture
563 }
564
565 pub fn parser(input: &mut &str) -> ModalResult<Self> {
591 let dashes: usize = input.chars().filter(|char| char == &'-').count();
602
603 if dashes < 2 {
604 let context_error = ContextError::from_input(input)
605 .add_context(
606 input,
607 &input.checkpoint(),
608 StrContext::Label("alpm-package file name"),
609 )
610 .add_context(
611 input,
612 &input.checkpoint(),
613 StrContext::Expected(StrContextValue::Description(
614 concat!(
615 "a package name, followed by an alpm-package-version (full or full with epoch) and an architecture.",
616 "\nAll components must be delimited with a dash ('-')."
617 )
618 ))
619 );
620
621 return Err(ErrMode::Cut(context_error));
622 }
623
624 let dashes_in_name = dashes.saturating_sub(3);
626
627 let name = cut_err(
631 repeat::<_, _, (), _, _>(
632 dashes_in_name + 1,
633 (opt("-"), take_until(0.., "-"), peek("-")),
638 )
639 .take()
640 .and_then(Name::parser),
642 )
643 .context(StrContext::Label("alpm-package-name"))
644 .parse_next(input)?;
645
646 "-".parse_next(input)?;
649
650 let version: FullVersion = cut_err((take_until(0.., "-"), "-", take_until(0.., "-")))
653 .context(StrContext::Label("alpm-package-version"))
654 .context(StrContext::Expected(StrContextValue::Description(
655 "an alpm-package-version (full or full with epoch) followed by a `-` and an architecture",
656 )))
657 .take()
658 .and_then(cut_err(FullVersion::parser))
659 .parse_next(input)?;
660
661 "-".parse_next(input)?;
664
665 let architecture = rest.and_then(Architecture::parser).parse_next(input)?;
668
669 Ok(Self {
670 name,
671 version,
672 architecture,
673 })
674 }
675}
676
677impl From<PackageFileName> for InstalledPackage {
678 fn from(value: PackageFileName) -> Self {
680 Self {
681 name: value.name,
682 version: value.version,
683 architecture: value.architecture,
684 }
685 }
686}
687
688impl FromStr for InstalledPackage {
689 type Err = Error;
690
691 fn from_str(s: &str) -> Result<InstalledPackage, Self::Err> {
713 Ok(Self::parser.parse(s)?)
714 }
715}
716
717impl Display for InstalledPackage {
718 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
719 write!(fmt, "{}-{}-{}", self.name, self.version, self.architecture)
720 }
721}
722
723#[cfg(test)]
724mod tests {
725 use rstest::rstest;
726 use testresult::TestResult;
727
728 use super::*;
729
730 #[rstest]
731 #[case(
732 "!makeflags",
733 MakepkgOption::BuildEnvironment(BuildEnvironmentOption::MakeFlags(false))
734 )]
735 #[case("autodeps", MakepkgOption::Package(PackageOption::AutoDeps(true)))]
736 #[case(
737 "ccache",
738 MakepkgOption::BuildEnvironment(BuildEnvironmentOption::Ccache(true))
739 )]
740 fn makepkg_option(#[case] input: &str, #[case] expected: MakepkgOption) {
741 let result = MakepkgOption::from_str(input).expect("Parser should be successful");
742 assert_eq!(result, expected);
743 }
744
745 #[rstest]
746 #[case(
747 "!somethingelse",
748 concat!(
749 "expected `buildflags`, `ccache`, `check`, `color`, `distcc`, `sign`, `makeflags`, ",
750 "`autodeps`, `debug`, `docs`, `emptydirs`, `libtool`, `lto`, `purge`, ",
751 "`staticlibs`, `strip`, `zipman`",
752 )
753 )]
754 #[case(
755 "#somethingelse",
756 "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
757 )]
758 fn invalid_makepkg_option(#[case] input: &str, #[case] err_snippet: &str) {
759 let Err(Error::ParseError(err_msg)) = MakepkgOption::from_str(input) else {
760 panic!("'{input}' erroneously parsed as VersionRequirement")
761 };
762 assert!(
763 err_msg.contains(err_snippet),
764 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
765 );
766 }
767
768 #[rstest]
769 #[case("autodeps", PackageOption::AutoDeps(true))]
770 #[case("debug", PackageOption::Debug(true))]
771 #[case("docs", PackageOption::Docs(true))]
772 #[case("emptydirs", PackageOption::EmptyDirs(true))]
773 #[case("!libtool", PackageOption::Libtool(false))]
774 #[case("lto", PackageOption::Lto(true))]
775 #[case("purge", PackageOption::Purge(true))]
776 #[case("staticlibs", PackageOption::StaticLibs(true))]
777 #[case("strip", PackageOption::Strip(true))]
778 #[case("zipman", PackageOption::Zipman(true))]
779 fn package_option(#[case] s: &str, #[case] expected: PackageOption) {
780 let result = PackageOption::from_str(s).expect("Parser should be successful");
781 assert_eq!(result, expected);
782 }
783
784 #[rstest]
785 #[case(
786 "!somethingelse",
787 "expected `autodeps`, `debug`, `docs`, `emptydirs`, `libtool`, `lto`, `purge`, `staticlibs`, `strip`, `zipman`"
788 )]
789 #[case(
790 "#somethingelse",
791 "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
792 )]
793 fn invalid_package_option(#[case] input: &str, #[case] err_snippet: &str) {
794 let Err(Error::ParseError(err_msg)) = PackageOption::from_str(input) else {
795 panic!("'{input}' erroneously parsed as VersionRequirement")
796 };
797 assert!(
798 err_msg.contains(err_snippet),
799 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
800 );
801 }
802
803 #[rstest]
804 #[case("buildflags", BuildEnvironmentOption::BuildFlags(true))]
805 #[case("ccache", BuildEnvironmentOption::Ccache(true))]
806 #[case("check", BuildEnvironmentOption::Check(true))]
807 #[case("color", BuildEnvironmentOption::Color(true))]
808 #[case("distcc", BuildEnvironmentOption::Distcc(true))]
809 #[case("!makeflags", BuildEnvironmentOption::MakeFlags(false))]
810 #[case("sign", BuildEnvironmentOption::Sign(true))]
811 #[case("!sign", BuildEnvironmentOption::Sign(false))]
812 fn build_environment_option(#[case] input: &str, #[case] expected: BuildEnvironmentOption) {
813 let result = BuildEnvironmentOption::from_str(input).expect("Parser should be successful");
814 assert_eq!(result, expected);
815 }
816
817 #[rstest]
818 #[case(
819 "!somethingelse",
820 "expected `buildflags`, `ccache`, `check`, `color`, `distcc`, `sign`, `makeflags`"
821 )]
822 #[case(
823 "#somethingelse",
824 "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
825 )]
826 fn invalid_build_environment_option(#[case] input: &str, #[case] err_snippet: &str) {
827 let Err(Error::ParseError(err_msg)) = BuildEnvironmentOption::from_str(input) else {
828 panic!("'{input}' erroneously parsed as VersionRequirement")
829 };
830 assert!(
831 err_msg.contains(err_snippet),
832 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
833 );
834 }
835
836 #[rstest]
837 #[case("#test", "invalid character in makepkg option")]
838 #[case("test!", "invalid character in makepkg option")]
839 fn invalid_option(#[case] input: &str, #[case] error_snippet: &str) {
840 let result = option_name_parser.parse(input);
841 assert!(result.is_err(), "Expected makepkg option parsing to fail");
842 let err = result.unwrap_err();
843 let pretty_error = err.to_string();
844 assert!(
845 pretty_error.contains(error_snippet),
846 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
847 );
848 }
849
850 #[rstest]
851 #[case(
852 "foo-bar-1:1.0.0-1-any",
853 InstalledPackage {
854 name: Name::new("foo-bar")?,
855 version: FullVersion::from_str("1:1.0.0-1")?,
856 architecture: Architecture::Any,
857 },
858 )]
859 #[case(
860 "foobar-1.0.0-1-x86_64",
861 InstalledPackage {
862 name: Name::new("foobar")?,
863 version: FullVersion::from_str("1.0.0-1")?,
864 architecture: Architecture::X86_64,
865 },
866 )]
867 fn installed_from_str(#[case] s: &str, #[case] result: InstalledPackage) -> TestResult {
868 assert_eq!(InstalledPackage::from_str(s), Ok(result));
869 Ok(())
870 }
871
872 #[rstest]
873 #[case("foo-1:1.0.0-bar-any", "invalid package release")]
874 #[case(
875 "foo-1:1.0.0_any",
876 "expected a package name, followed by an alpm-package-version (full or full with epoch) and an architecture."
877 )]
878 #[case("packagename-30-0.1oops-any", "expected end of package release value")]
879 #[case("package$with$dollars-30-0.1-any", "invalid character in package name")]
880 #[case("packagename-30-0.1-any*asdf", "invalid architecture")]
881 fn installed_new_parse_error(#[case] input: &str, #[case] error_snippet: &str) {
882 let result = InstalledPackage::from_str(input);
883 assert!(result.is_err(), "Expected InstalledPackage parsing to fail");
884 let err = result.unwrap_err();
885 let pretty_error = err.to_string();
886 assert!(
887 pretty_error.contains(error_snippet),
888 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
889 );
890 }
891}