Skip to main content

alpm_types/
env.rs

1use std::{
2    fmt::{Display, Formatter},
3    str::FromStr,
4};
5
6use alpm_parsers::{
7    iter_char_context,
8    iter_str_context,
9    traits::{AlpmParser, ParserUntil},
10};
11#[cfg(feature = "serde")]
12use serde::{Deserialize, Serialize};
13use strum::VariantNames;
14use winnow::{
15    ModalResult,
16    Parser,
17    combinator::{alt, cut_err, fail, opt, peek, repeat, repeat_till},
18    error::{
19        AddContext,
20        ContextError,
21        ErrMode,
22        ParserError,
23        StrContext,
24        StrContextValue::{self, *},
25    },
26    stream::Stream,
27    token::{any, one_of},
28};
29
30use crate::{
31    Architecture,
32    FullVersion,
33    Name,
34    PackageFileName,
35    PackageRelation,
36    VersionComparison,
37    VersionRequirement,
38    error::Error,
39};
40
41/// Recognizes the `!` boolean operator in option names.
42///
43/// This parser **does not** fully consume its input.
44/// It also expects the package name to be there, if the `!` does not exist.
45///
46/// # Format
47///
48/// The parser expects a `!` or either one of ASCII alphanumeric character, hyphen, dot, or
49/// underscore.
50///
51/// # Errors
52///
53/// If the input string does not match the expected format, an error will be returned.
54fn option_bool_parser(input: &mut &str) -> ModalResult<bool> {
55    let alphanum = |c: char| c.is_ascii_alphanumeric();
56    let special_first_chars = ['-', '.', '_', '!'];
57    let valid_chars = one_of((alphanum, special_first_chars));
58
59    // Make sure that we have either a `!` at the start or the first char of a name.
60    cut_err(peek(valid_chars))
61        .context(StrContext::Expected(CharLiteral('!')))
62        .context(StrContext::Expected(Description(
63            "ASCII alphanumeric character",
64        )))
65        .context_with(iter_char_context!(special_first_chars))
66        .parse_next(input)?;
67
68    Ok(opt('!').parse_next(input)?.is_none())
69}
70
71/// The set of special characters that are allowed in [makepkg.conf options].
72///
73/// # Note
74///
75/// These special characters only apply to values in the `BUILDENV` and `OPTIONS`
76/// arrays found in [makepkg.conf options].
77///
78/// [makepkg.conf options]: https://man.archlinux.org/man/makepkg.conf.5.en#OPTIONS
79pub(crate) static SPECIAL_OPTION_CHARS: [char; 3] = ['-', '.', '_'];
80
81/// Recognizes option names.
82///
83/// This parser fully consumes its input.
84///
85/// # Format
86///
87/// The parser expects a sequence of ASCII alphanumeric characters, hyphens, dots, or underscores.
88///
89/// # Errors
90///
91/// If the input string does not match the expected format, an error will be returned.
92fn option_name_parser<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
93    let alphanum = |c: char| c.is_ascii_alphanumeric();
94
95    let valid_chars = one_of((alphanum, SPECIAL_OPTION_CHARS));
96    let name = repeat::<_, _, (), _, _>(0.., valid_chars)
97        .take()
98        .parse_next(input)?;
99
100    Ok(name)
101}
102
103/// Wraps the [`PackageOption`] and [`BuildEnvironmentOption`] enums.
104///
105/// This is necessary for metadata files such as [SRCINFO] or [PKGBUILD] package scripts that don't
106/// differentiate between the different types and scopes of options.
107///
108/// [SRCINFO]: https://alpm.archlinux.page/specifications/SRCINFO.5.html
109/// [PKGBUILD]: https://man.archlinux.org/man/PKGBUILD.5
110#[derive(Clone, Debug, Eq, PartialEq)]
111#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
112#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "snake_case"))]
113pub enum MakepkgOption {
114    /// A [`BuildEnvironmentOption`]
115    BuildEnvironment(BuildEnvironmentOption),
116    /// A [`PackageOption`]
117    Package(PackageOption),
118}
119
120impl AlpmParser for MakepkgOption {
121    /// Recognizes any [`PackageOption`] and [`BuildEnvironmentOption`] in a
122    /// string slice.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if `input` does not begin with a valid [`MakepkgOption`].
127    fn parser(input: &mut &str) -> ModalResult<Self> {
128        alt((
129            BuildEnvironmentOption::parser.map(MakepkgOption::BuildEnvironment),
130            PackageOption::parser.map(MakepkgOption::Package),
131            fail.context(StrContext::Label("packaging or build environment option"))
132                .context_with(iter_str_context!([
133                    BuildEnvironmentOption::VARIANTS.to_vec(),
134                    PackageOption::VARIANTS.to_vec()
135                ])),
136        ))
137        .parse_next(input)
138    }
139
140    fn delimiter_error_context<'a, O, P>(
141        parser: P,
142    ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
143    where
144        P: Parser<&'a str, O, ErrMode<ContextError>>,
145    {
146        parser
147            .context(StrContext::Label("makepkg option"))
148            .context(StrContext::Expected(StrContextValue::Description(
149                "string consisting of alphanumeric characters or",
150            )))
151            .context_with(iter_char_context!(SPECIAL_OPTION_CHARS))
152    }
153}
154
155impl FromStr for MakepkgOption {
156    type Err = Error;
157    /// Creates a [`MakepkgOption`] from string slice.
158    fn from_str(s: &str) -> Result<Self, Self::Err> {
159        Ok(Self::parser.parse(s)?)
160    }
161}
162
163impl Display for MakepkgOption {
164    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
165        match self {
166            MakepkgOption::BuildEnvironment(option) => write!(fmt, "{option}"),
167            MakepkgOption::Package(option) => write!(fmt, "{option}"),
168        }
169    }
170}
171
172/// An option string used in a build environment
173///
174/// The option string is identified by its name and whether it is on (not prefixed with "!") or off
175/// (prefixed with "!").
176///
177/// See [the makepkg.conf manpage](https://man.archlinux.org/man/makepkg.conf.5.en) for more information.
178///
179/// ## Examples
180/// ```
181/// # fn main() -> Result<(), alpm_types::Error> {
182/// use alpm_types::BuildEnvironmentOption;
183///
184/// let option = BuildEnvironmentOption::new("distcc")?;
185/// assert_eq!(option.on(), true);
186/// assert_eq!(option.name(), "distcc");
187///
188/// let not_option = BuildEnvironmentOption::new("!ccache")?;
189/// assert_eq!(not_option.on(), false);
190/// assert_eq!(not_option.name(), "ccache");
191/// # Ok(())
192/// # }
193/// ```
194#[derive(Clone, Debug, Eq, PartialEq, VariantNames)]
195#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
196#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
197pub enum BuildEnvironmentOption {
198    /// Use or unset the values of build flags (e.g. `CPPFLAGS`, `CFLAGS`, `CXXFLAGS`, `LDFLAGS`)
199    /// specified in user-specific configs (e.g. [makepkg.conf]).
200    ///
201    /// [makepkg.conf]: https://man.archlinux.org/man/makepkg.conf.5
202    #[strum(serialize = "buildflags")]
203    BuildFlags(bool),
204    /// Use ccache to cache compilation
205    #[strum(serialize = "ccache")]
206    Ccache(bool),
207    /// Run the check() function if present in the PKGBUILD
208    #[strum(serialize = "check")]
209    Check(bool),
210    /// Colorize output messages
211    #[strum(serialize = "color")]
212    Color(bool),
213    /// Use the Distributed C/C++/ObjC compiler
214    #[strum(serialize = "distcc")]
215    Distcc(bool),
216    /// Generate PGP signature file
217    #[strum(serialize = "sign")]
218    Sign(bool),
219    /// Use or unset the value of the `MAKEFLAGS` environment variable specified in
220    /// user-specific configs (e.g. [makepkg.conf]).
221    ///
222    /// [makepkg.conf]: https://man.archlinux.org/man/makepkg.conf.5
223    #[strum(serialize = "makeflags")]
224    MakeFlags(bool),
225}
226
227impl BuildEnvironmentOption {
228    /// Create a new [`BuildEnvironmentOption`] in a Result
229    ///
230    /// # Errors
231    ///
232    /// An error is returned if the string slice does not match a valid build environment option.
233    pub fn new(option: &str) -> Result<Self, Error> {
234        Self::from_str(option)
235    }
236
237    /// Get the name of the BuildEnvironmentOption
238    pub fn name(&self) -> &str {
239        match self {
240            Self::BuildFlags(_) => "buildflags",
241            Self::Ccache(_) => "ccache",
242            Self::Check(_) => "check",
243            Self::Color(_) => "color",
244            Self::Distcc(_) => "distcc",
245            Self::MakeFlags(_) => "makeflags",
246            Self::Sign(_) => "sign",
247        }
248    }
249
250    /// Get whether the BuildEnvironmentOption is on
251    pub fn on(&self) -> bool {
252        match self {
253            Self::BuildFlags(on)
254            | Self::Ccache(on)
255            | Self::Check(on)
256            | Self::Color(on)
257            | Self::Distcc(on)
258            | Self::MakeFlags(on)
259            | Self::Sign(on) => *on,
260        }
261    }
262}
263
264impl AlpmParser for BuildEnvironmentOption {
265    /// Recognizes a [`BuildEnvironmentOption`] in a string slice.
266    ///
267    /// # Errors
268    ///
269    /// Returns an error if `input` does not begin with a valid [`BuildEnvironmentOption`].
270    fn parser(input: &mut &str) -> ModalResult<Self> {
271        let on = option_bool_parser.parse_next(input)?;
272        let mut name = option_name_parser.parse_next(input)?;
273
274        alt((
275            "buildflags".value(Self::BuildFlags(on)),
276            "ccache".value(Self::Ccache(on)),
277            "check".value(Self::Check(on)),
278            "color".value(Self::Color(on)),
279            "distcc".value(Self::Distcc(on)),
280            "makeflags".value(Self::MakeFlags(on)),
281            "sign".value(Self::Sign(on)),
282            fail.context(StrContext::Label("makepkg build environment option"))
283                .context_with(iter_str_context!([BuildEnvironmentOption::VARIANTS])),
284        ))
285        .parse_next(&mut name)
286    }
287
288    fn delimiter_error_context<'a, O, P>(
289        parser: P,
290    ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
291    where
292        P: Parser<&'a str, O, ErrMode<ContextError>>,
293    {
294        parser
295            .context(StrContext::Label("build environment option"))
296            .context(StrContext::Expected(StrContextValue::Description(
297                "string consisting of alphanumeric characters or",
298            )))
299            .context_with(iter_char_context!(SPECIAL_OPTION_CHARS))
300    }
301}
302
303impl FromStr for BuildEnvironmentOption {
304    type Err = Error;
305    /// Creates a [`BuildEnvironmentOption`] from a string slice.
306    ///
307    /// Delegates to [`BuildEnvironmentOption::parser`].
308    ///
309    /// # Errors
310    ///
311    /// Returns an error if [`BuildEnvironmentOption::parser`] fails.
312    fn from_str(s: &str) -> Result<Self, Self::Err> {
313        Ok(Self::parser_until_eof.parse(s)?)
314    }
315}
316
317impl Display for BuildEnvironmentOption {
318    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
319        write!(fmt, "{}{}", if self.on() { "" } else { "!" }, self.name())
320    }
321}
322
323/// An option string used in packaging
324///
325/// The option string is identified by its name and whether it is on (not prefixed with "!") or off
326/// (prefixed with "!").
327///
328/// See [the makepkg.conf manpage](https://man.archlinux.org/man/makepkg.conf.5.en) for more information.
329///
330/// ## Examples
331/// ```
332/// # fn main() -> Result<(), alpm_types::Error> {
333/// use alpm_types::PackageOption;
334///
335/// let option = PackageOption::new("debug")?;
336/// assert_eq!(option.on(), true);
337/// assert_eq!(option.name(), "debug");
338///
339/// let not_option = PackageOption::new("!lto")?;
340/// assert_eq!(not_option.on(), false);
341/// assert_eq!(not_option.name(), "lto");
342/// # Ok(())
343/// # }
344/// ```
345#[derive(Clone, Debug, Eq, PartialEq, VariantNames)]
346#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
347#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
348pub enum PackageOption {
349    /// Automatically add dependencies and provisions (see [alpm-sonamev2]).
350    ///
351    /// [alpm-sonamev2]: https://alpm.archlinux.page/specifications/alpm-sonamev2.7.html
352    #[strum(serialize = "autodeps")]
353    AutoDeps(bool),
354
355    /// Add debugging flags as specified in DEBUG_* variables
356    #[strum(serialize = "debug")]
357    Debug(bool),
358
359    /// Save doc directories specified by DOC_DIRS
360    #[strum(serialize = "docs")]
361    Docs(bool),
362
363    /// Leave empty directories in packages
364    #[strum(serialize = "emptydirs")]
365    EmptyDirs(bool),
366
367    /// Leave libtool (.la) files in packages
368    #[strum(serialize = "libtool")]
369    Libtool(bool),
370
371    /// Add compile flags for building with link time optimization
372    #[strum(serialize = "lto")]
373    Lto(bool),
374
375    /// Strip debug symbols from Portable Executable (PE) format files
376    #[strum(serialize = "pestrip")]
377    PEStrip(bool),
378
379    /// Remove files specified by PURGE_TARGETS
380    #[strum(serialize = "purge")]
381    Purge(bool),
382
383    /// Leave static library (.a) files in packages
384    #[strum(serialize = "staticlibs")]
385    StaticLibs(bool),
386
387    /// Strip symbols from binaries/libraries
388    #[strum(serialize = "strip")]
389    Strip(bool),
390
391    /// Compress manual (man and info) pages in MAN_DIRS with gzip
392    #[strum(serialize = "zipman")]
393    Zipman(bool),
394}
395
396impl PackageOption {
397    /// Creates a new [`PackageOption`] from a string slice.
398    ///
399    /// # Errors
400    ///
401    /// An error is returned if the string slice does not match a valid package option.
402    pub fn new(option: &str) -> Result<Self, Error> {
403        Self::from_str(option)
404    }
405
406    /// Returns the name of the [`PackageOption`] as string slice.
407    pub fn name(&self) -> &str {
408        match self {
409            Self::AutoDeps(_) => "autodeps",
410            Self::Debug(_) => "debug",
411            Self::Docs(_) => "docs",
412            Self::EmptyDirs(_) => "emptydirs",
413            Self::Libtool(_) => "libtool",
414            Self::Lto(_) => "lto",
415            Self::PEStrip(_) => "pestrip",
416            Self::Purge(_) => "purge",
417            Self::StaticLibs(_) => "staticlibs",
418            Self::Strip(_) => "strip",
419            Self::Zipman(_) => "zipman",
420        }
421    }
422
423    /// Returns whether the [`PackageOption`] is on or off.
424    pub fn on(&self) -> bool {
425        match self {
426            Self::AutoDeps(on)
427            | Self::Debug(on)
428            | Self::Docs(on)
429            | Self::EmptyDirs(on)
430            | Self::Libtool(on)
431            | Self::Lto(on)
432            | Self::Purge(on)
433            | Self::PEStrip(on)
434            | Self::StaticLibs(on)
435            | Self::Strip(on)
436            | Self::Zipman(on) => *on,
437        }
438    }
439}
440
441impl AlpmParser for PackageOption {
442    /// Recognizes a [`PackageOption`] in a string slice.
443    ///
444    /// # Errors
445    ///
446    /// Returns an error if `input` does not begin with a valid [`PackageOption`].
447    fn parser(input: &mut &str) -> ModalResult<Self> {
448        let on = option_bool_parser.parse_next(input)?;
449        let mut name = option_name_parser.parse_next(input)?;
450
451        alt((
452            alt((
453                "autodeps".value(Self::AutoDeps(on)),
454                "debug".value(Self::Debug(on)),
455                "docs".value(Self::Docs(on)),
456                "emptydirs".value(Self::EmptyDirs(on)),
457                "libtool".value(Self::Libtool(on)),
458                "lto".value(Self::Lto(on)),
459                "pestrip".value(Self::PEStrip(on)),
460                "purge".value(Self::Purge(on)),
461                "staticlibs".value(Self::StaticLibs(on)),
462            )),
463            alt((
464                "strip".value(Self::Strip(on)),
465                "zipman".value(Self::Zipman(on)),
466            )),
467            fail.context(StrContext::Label("makepkg packaging option"))
468                .context_with(iter_str_context!([PackageOption::VARIANTS])),
469        ))
470        .parse_next(&mut name)
471    }
472
473    fn delimiter_error_context<'a, O, P>(
474        parser: P,
475    ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
476    where
477        P: Parser<&'a str, O, ErrMode<ContextError>>,
478    {
479        parser
480            .context(StrContext::Label("package option"))
481            .context(StrContext::Expected(StrContextValue::Description(
482                "string consisting of alphanumeric characters or",
483            )))
484            .context_with(iter_char_context!(SPECIAL_OPTION_CHARS))
485    }
486}
487
488impl FromStr for PackageOption {
489    type Err = Error;
490    /// Creates a [`PackageOption`] from a string slice.
491    ///
492    /// Delegates to [`PackageOption::parser`].
493    ///
494    /// # Errors
495    ///
496    /// Returns an error if [`PackageOption::parser`] fails.
497    fn from_str(s: &str) -> Result<Self, Self::Err> {
498        Ok(Self::parser_until_eof.parse(s)?)
499    }
500}
501
502impl Display for PackageOption {
503    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
504        write!(fmt, "{}{}", if self.on() { "" } else { "!" }, self.name())
505    }
506}
507
508/// Information on an installed package in an environment
509///
510/// Tracks the [`Name`], [`FullVersion`] and an [`Architecture`] of a package in an environment.
511///
512/// # Examples
513///
514/// ```
515/// use std::str::FromStr;
516///
517/// use alpm_types::{Architecture, FullVersion, InstalledPackage, Name};
518/// # fn main() -> Result<(), alpm_types::Error> {
519/// assert_eq!(
520///     InstalledPackage::from_str("foo-bar-1:1.0.0-1-any")?,
521///     InstalledPackage::new(
522///         Name::new("foo-bar")?,
523///         FullVersion::from_str("1:1.0.0-1")?,
524///         Architecture::Any
525///     )
526/// );
527/// assert_eq!(
528///     InstalledPackage::from_str("foo-bar-1.0.0-1-any")?,
529///     InstalledPackage::new(
530///         Name::new("foo-bar")?,
531///         FullVersion::from_str("1.0.0-1")?,
532///         Architecture::Any
533///     )
534/// );
535/// # Ok(())
536/// # }
537/// ```
538#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
539#[cfg_attr(feature = "serde", derive(Serialize))]
540pub struct InstalledPackage {
541    name: Name,
542    version: FullVersion,
543    architecture: Architecture,
544}
545
546impl InstalledPackage {
547    /// Creates a new [`InstalledPackage`].
548    ///
549    /// # Examples
550    ///
551    /// ```
552    /// use std::str::FromStr;
553    ///
554    /// use alpm_types::InstalledPackage;
555    ///
556    /// # fn main() -> Result<(), alpm_types::Error> {
557    /// assert_eq!(
558    ///     "example-1:1.0.0-1-x86_64",
559    ///     InstalledPackage::new("example".parse()?, "1:1.0.0-1".parse()?, "x86_64".parse()?)
560    ///         .to_string()
561    /// );
562    /// # Ok(())
563    /// # }
564    /// ```
565    pub fn new(name: Name, version: FullVersion, architecture: Architecture) -> Self {
566        Self {
567            name,
568            version,
569            architecture,
570        }
571    }
572
573    /// Returns a reference to the [`Name`].
574    ///
575    /// # Examples
576    ///
577    /// ```
578    /// use std::str::FromStr;
579    ///
580    /// use alpm_types::{InstalledPackage, Name};
581    ///
582    /// # fn main() -> Result<(), alpm_types::Error> {
583    /// let file_name =
584    ///     InstalledPackage::new("example".parse()?, "1:1.0.0-1".parse()?, "x86_64".parse()?);
585    ///
586    /// assert_eq!(file_name.name(), &Name::new("example")?);
587    /// # Ok(())
588    /// # }
589    /// ```
590    pub fn name(&self) -> &Name {
591        &self.name
592    }
593
594    /// Returns a reference to the [`FullVersion`].
595    ///
596    /// # Examples
597    ///
598    /// ```
599    /// use std::str::FromStr;
600    ///
601    /// use alpm_types::{FullVersion, InstalledPackage};
602    ///
603    /// # fn main() -> Result<(), alpm_types::Error> {
604    /// let file_name =
605    ///     InstalledPackage::new("example".parse()?, "1:1.0.0-1".parse()?, "x86_64".parse()?);
606    ///
607    /// assert_eq!(file_name.version(), &FullVersion::from_str("1:1.0.0-1")?);
608    /// # Ok(())
609    /// # }
610    /// ```
611    pub fn version(&self) -> &FullVersion {
612        &self.version
613    }
614
615    /// Returns the [`Architecture`].
616    ///
617    /// # Examples
618    ///
619    /// ```
620    /// use std::str::FromStr;
621    ///
622    /// use alpm_types::{InstalledPackage, SystemArchitecture};
623    ///
624    /// # fn main() -> Result<(), alpm_types::Error> {
625    /// let file_name =
626    ///     InstalledPackage::new("example".parse()?, "1:1.0.0-1".parse()?, "x86_64".parse()?);
627    ///
628    /// assert_eq!(file_name.architecture(), &SystemArchitecture::X86_64.into());
629    /// # Ok(())
630    /// # }
631    /// ```
632    pub fn architecture(&self) -> &Architecture {
633        &self.architecture
634    }
635
636    /// Returns the [`PackageRelation`] encoded in this [`InstalledPackage`].
637    ///
638    /// # Examples
639    ///
640    /// ```
641    /// use std::str::FromStr;
642    ///
643    /// use alpm_types::{InstalledPackage, PackageRelation};
644    ///
645    /// # fn main() -> Result<(), alpm_types::Error> {
646    /// let installed_package =
647    ///     InstalledPackage::new("example".parse()?, "1:1.0.0-1".parse()?, "x86_64".parse()?);
648    ///
649    /// assert_eq!(
650    ///     installed_package.to_package_relation(),
651    ///     PackageRelation::from_str("example=1:1.0.0-1")?
652    /// );
653    /// # Ok(())
654    /// # }
655    /// ```
656    pub fn to_package_relation(&self) -> PackageRelation {
657        PackageRelation {
658            name: self.name.clone(),
659            version_requirement: Some(VersionRequirement {
660                comparison: VersionComparison::Equal,
661                version: self.version.clone().into(),
662            }),
663        }
664    }
665}
666
667impl ParserUntil for InstalledPackage {
668    /// Recognizes an [`InstalledPackage`] in a string slice before a `delimiter`.
669    ///
670    /// # Errors
671    ///
672    /// Returns an error if
673    ///
674    /// - the [`Name`] component can not be recognized,
675    /// - the [`FullVersion`] component can not be recognized,
676    /// - or the [`Architecture`] component can not be recognized.
677    ///
678    /// # Examples
679    ///
680    /// ```
681    /// use alpm_parsers::traits::ParserUntil;
682    /// use alpm_types::InstalledPackage;
683    /// use winnow::Parser;
684    ///
685    /// # fn main() -> Result<(), alpm_types::Error> {
686    /// let name = "example-package-1:1.0.0-1-x86_64";
687    /// assert_eq!(
688    ///     name,
689    ///     InstalledPackage::parser_until_eof.parse(name)?.to_string()
690    /// );
691    /// # Ok(())
692    /// # }
693    /// ```
694    fn parser_until<'a, P>(delimiter: P) -> impl Parser<&'a str, Self, ErrMode<ContextError>>
695    where
696        P: Parser<&'a str, &'a str, ErrMode<ContextError>>,
697    {
698        // Define the actual parser closure.
699        // The delimiter is moved into the closure and borrowed via `by_ref()` on each call.
700        let mut delimiter_parser = delimiter;
701        move |input: &mut &'a str| -> ModalResult<Self> {
702            // Detect the amount of dashes in input and subsequently in the Name component.
703            //
704            // This is a necessary step because dashes are used as delimiters between the
705            // components of the file name and the Name component (an alpm-package-name) can contain
706            // dashes, too.
707            // We know that the minimum amount of dashes in a valid alpm-package file name is
708            // three (one dash between the Name, Version, PackageRelease, and Architecture
709            // component each).
710            // We rely on this fact to determine the amount of dashes in the Name component and
711            // thereby the cut-off point between the Name and the Version component.
712            let checkpoint = input.checkpoint();
713            let dashes: usize =
714                repeat_till::<_, _, (), _, _, _, _>(0.., any, peek(delimiter_parser.by_ref()))
715                    .take()
716                    .map(|s| {
717                        s.chars().fold(0, |acc, char| {
718                            if char == '-' {
719                                return acc + 1;
720                            }
721                            acc
722                        })
723                    })
724                    .parse_next(input)?;
725            input.reset(&checkpoint);
726
727            if dashes < 2 {
728                let context_error = ContextError::from_input(input)
729                .add_context(
730                    input,
731                    &input.checkpoint(),
732                    StrContext::Label("alpm-package file name"),
733                )
734                .add_context(
735                    input,
736                    &input.checkpoint(),
737                    StrContext::Expected(StrContextValue::Description(
738                        concat!(
739                        "a package name, followed by an alpm-package-version (full or full with epoch) and an alpm-architecture.",
740                        "\nAll components must be delimited with a dash ('-')."
741                        )
742                    ))
743                );
744
745                return Err(ErrMode::Backtrack(context_error));
746            }
747
748            // The (zero or more) dashes in the Name component.
749            let dashes_till_version = dashes.saturating_sub(2);
750
751            // Advance the parser to the dash just behind the Name component, based on the amount of
752            // dashes in the Name, e.g.:
753            // "example-package-1:1.0.0-1-x86_64" -> "-1:1.0.0-1-x86_64"
754            let name = Name::parse_name_followed_by_version(dashes_till_version)
755                .context(StrContext::Label("alpm-package-name"))
756                .parse_next(input)?;
757
758            // Consume leading dash in front of Version, e.g.:
759            // "-1:1.0.0-1-x86_64" -> "1:1.0.0-1-x86_64"
760            "-".parse_next(input)?;
761
762            // Advance the parser to beyond the Version component (which contains one dash), e.g.:
763            // "1:1.0.0-1-x86_64" -> "-x86_64"
764            let version: FullVersion = FullVersion::parser
765            .context(StrContext::Label("alpm-package-version"))
766            .context(StrContext::Expected(StrContextValue::Description(
767                "an alpm-package-version (full or full with epoch) followed by a `-` and an alpm-architecture",
768            )))
769            .parse_next(input)?;
770
771            // Consume leading dash, e.g.:
772            // "-x86_64" -> "x86_64"
773            "-".context(StrContext::Label("alpm-package file name"))
774                .context(StrContext::Expected(StrContextValue::Description(
775                    "expected a `-` followed by an alpm-architecture",
776                )))
777                .parse_next(input)?;
778
779            // Parse the architecture component
780            let architecture =
781                Architecture::parser_until(delimiter_parser.by_ref()).parse_next(input)?;
782
783            Ok(Self {
784                name,
785                version,
786                architecture,
787            })
788        }
789    }
790}
791
792impl From<PackageFileName> for InstalledPackage {
793    /// Creates a [`InstalledPackage`] from a [`PackageFileName`].
794    fn from(value: PackageFileName) -> Self {
795        Self {
796            name: value.name,
797            version: value.version,
798            architecture: value.architecture,
799        }
800    }
801}
802
803impl FromStr for InstalledPackage {
804    type Err = Error;
805
806    /// Creates an [`InstalledPackage`] from a string slice.
807    ///
808    /// Delegates to [`InstalledPackage::parser_until`].
809    ///
810    /// # Errors
811    ///
812    /// Returns an error if [`InstalledPackage::parser_until`] fails.
813    ///
814    /// # Examples
815    ///
816    /// ```
817    /// use std::str::FromStr;
818    ///
819    /// use alpm_types::InstalledPackage;
820    ///
821    /// # fn main() -> Result<(), alpm_types::Error> {
822    /// let filename = "example-package-1:1.0.0-1-x86_64";
823    /// assert_eq!(filename, InstalledPackage::from_str(filename)?.to_string());
824    /// # Ok(())
825    /// # }
826    /// ```
827    fn from_str(s: &str) -> Result<InstalledPackage, Self::Err> {
828        Ok(Self::parser_until_eof.parse(s)?)
829    }
830}
831
832impl Display for InstalledPackage {
833    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
834        write!(fmt, "{}-{}-{}", self.name, self.version, self.architecture)
835    }
836}
837
838#[cfg(test)]
839mod tests {
840    use insta::assert_snapshot;
841    use rstest::rstest;
842    use testresult::TestResult;
843
844    use super::*;
845    use crate::{SystemArchitecture, configure_insta};
846
847    #[rstest]
848    #[case(
849        "!makeflags",
850        MakepkgOption::BuildEnvironment(BuildEnvironmentOption::MakeFlags(false))
851    )]
852    #[case("autodeps", MakepkgOption::Package(PackageOption::AutoDeps(true)))]
853    #[case(
854        "ccache",
855        MakepkgOption::BuildEnvironment(BuildEnvironmentOption::Ccache(true))
856    )]
857    fn makepkg_option(#[case] input: &str, #[case] expected: MakepkgOption) {
858        let result = MakepkgOption::from_str(input).expect("Parser should be successful");
859        assert_eq!(result, expected);
860    }
861
862    #[rstest]
863    #[case("!somethingelse")]
864    #[case("#somethingelse")]
865    fn invalid_makepkg_option(#[case] input: &str) {
866        let Err(Error::ParseError(err_msg)) = MakepkgOption::from_str(input) else {
867            panic!("'{input}' erroneously parsed as MakepkgOption")
868        };
869
870        let (test_name, _guard) = configure_insta();
871        assert_snapshot!(test_name, err_msg.to_string());
872    }
873
874    #[rstest]
875    #[case("autodeps", PackageOption::AutoDeps(true))]
876    #[case("debug", PackageOption::Debug(true))]
877    #[case("docs", PackageOption::Docs(true))]
878    #[case("emptydirs", PackageOption::EmptyDirs(true))]
879    #[case("!libtool", PackageOption::Libtool(false))]
880    #[case("lto", PackageOption::Lto(true))]
881    #[case("pestrip", PackageOption::PEStrip(true))]
882    #[case("purge", PackageOption::Purge(true))]
883    #[case("staticlibs", PackageOption::StaticLibs(true))]
884    #[case("strip", PackageOption::Strip(true))]
885    #[case("zipman", PackageOption::Zipman(true))]
886    fn package_option(#[case] s: &str, #[case] expected: PackageOption) {
887        let result = PackageOption::from_str(s).expect("Parser should be successful");
888        assert_eq!(result, expected);
889    }
890
891    #[rstest]
892    #[case("!somethingelse")]
893    #[case("#somethingelse")]
894    fn invalid_package_option(#[case] input: &str) {
895        let Err(Error::ParseError(err_msg)) = PackageOption::from_str(input) else {
896            panic!("'{input}' erroneously parsed as PackageOption")
897        };
898
899        let (test_name, _guard) = configure_insta();
900        assert_snapshot!(test_name, err_msg.to_string());
901    }
902
903    #[rstest]
904    #[case("buildflags", BuildEnvironmentOption::BuildFlags(true))]
905    #[case("ccache", BuildEnvironmentOption::Ccache(true))]
906    #[case("check", BuildEnvironmentOption::Check(true))]
907    #[case("color", BuildEnvironmentOption::Color(true))]
908    #[case("distcc", BuildEnvironmentOption::Distcc(true))]
909    #[case("!makeflags", BuildEnvironmentOption::MakeFlags(false))]
910    #[case("sign", BuildEnvironmentOption::Sign(true))]
911    #[case("!sign", BuildEnvironmentOption::Sign(false))]
912    fn build_environment_option(#[case] input: &str, #[case] expected: BuildEnvironmentOption) {
913        let result = BuildEnvironmentOption::from_str(input).expect("Parser should be successful");
914        assert_eq!(result, expected);
915    }
916
917    #[rstest]
918    #[case("!somethingelse")]
919    #[case("#somethingelse")]
920    fn invalid_build_environment_option(#[case] input: &str) {
921        let Err(Error::ParseError(err_msg)) = BuildEnvironmentOption::from_str(input) else {
922            panic!("'{input}' erroneously parsed as BuildEnvironmentOption")
923        };
924
925        let (test_name, _guard) = configure_insta();
926        assert_snapshot!(test_name, err_msg.to_string());
927    }
928
929    #[rstest]
930    #[case(
931        "foo-bar-1:1.0.0-1-any",
932        InstalledPackage {
933            name: Name::new("foo-bar")?,
934            version: FullVersion::from_str("1:1.0.0-1")?,
935            architecture: Architecture::Any,
936        },
937    )]
938    #[case(
939        "foobar-1.0.0-1-x86_64",
940        InstalledPackage {
941            name: Name::new("foobar")?,
942            version: FullVersion::from_str("1.0.0-1")?,
943            architecture: SystemArchitecture::X86_64.into(),
944        },
945    )]
946    fn installed_from_str(#[case] s: &str, #[case] result: InstalledPackage) -> TestResult {
947        assert_eq!(InstalledPackage::from_str(s), Ok(result));
948        Ok(())
949    }
950
951    #[rstest]
952    #[case("foo-1:1.0.0-bar-any")]
953    #[case("foo-1:1.0.0_any")]
954    #[case("packagename-30-0.1oops-any")]
955    #[case("package$with$dollars-30-0.1-any")]
956    #[case("packagename-30-0.1-any*asdf")]
957    fn installed_new_parse_error(#[case] input: &str) {
958        let Err(Error::ParseError(err_msg)) = InstalledPackage::from_str(input) else {
959            panic!("'{input}' erroneously parsed as InstalledPackage")
960        };
961
962        let (test_name, _guard) = configure_insta();
963        assert_snapshot!(test_name, err_msg.to_string());
964    }
965}