alpm_types/
env.rs

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