Skip to main content

alpm_types/
env.rs

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