Skip to main content

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            "autodeps".value(Self::AutoDeps(on)),
417            "debug".value(Self::Debug(on)),
418            "docs".value(Self::Docs(on)),
419            "emptydirs".value(Self::EmptyDirs(on)),
420            "libtool".value(Self::Libtool(on)),
421            "lto".value(Self::Lto(on)),
422            "pestrip".value(Self::PEStrip(on)),
423            "purge".value(Self::Purge(on)),
424            "staticlibs".value(Self::StaticLibs(on)),
425            "strip".value(Self::Strip(on)),
426            "zipman".value(Self::Zipman(on)),
427            fail.context(StrContext::Label("makepkg packaging option"))
428                .context_with(iter_str_context!([PackageOption::VARIANTS])),
429        ))
430        .parse_next(&mut name)
431    }
432}
433
434impl FromStr for PackageOption {
435    type Err = Error;
436    /// Creates a [`PackageOption`] from a string slice.
437    ///
438    /// Delegates to [`PackageOption::parser`].
439    ///
440    /// # Errors
441    ///
442    /// Returns an error if [`PackageOption::parser`] fails.
443    fn from_str(s: &str) -> Result<Self, Self::Err> {
444        Ok(Self::parser.parse(s)?)
445    }
446}
447
448impl Display for PackageOption {
449    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
450        write!(fmt, "{}{}", if self.on() { "" } else { "!" }, self.name())
451    }
452}
453
454/// Information on an installed package in an environment
455///
456/// Tracks the [`Name`], [`FullVersion`] and an [`Architecture`] of a package in an environment.
457///
458/// # Examples
459///
460/// ```
461/// use std::str::FromStr;
462///
463/// use alpm_types::{Architecture, FullVersion, InstalledPackage, Name};
464/// # fn main() -> Result<(), alpm_types::Error> {
465/// assert_eq!(
466///     InstalledPackage::from_str("foo-bar-1:1.0.0-1-any")?,
467///     InstalledPackage::new(
468///         Name::new("foo-bar")?,
469///         FullVersion::from_str("1:1.0.0-1")?,
470///         Architecture::Any
471///     )
472/// );
473/// assert_eq!(
474///     InstalledPackage::from_str("foo-bar-1.0.0-1-any")?,
475///     InstalledPackage::new(
476///         Name::new("foo-bar")?,
477///         FullVersion::from_str("1.0.0-1")?,
478///         Architecture::Any
479///     )
480/// );
481/// # Ok(())
482/// # }
483/// ```
484#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
485pub struct InstalledPackage {
486    name: Name,
487    version: FullVersion,
488    architecture: Architecture,
489}
490
491impl InstalledPackage {
492    /// Creates a new [`InstalledPackage`].
493    ///
494    /// # Examples
495    ///
496    /// ```
497    /// use std::str::FromStr;
498    ///
499    /// use alpm_types::InstalledPackage;
500    ///
501    /// # fn main() -> Result<(), alpm_types::Error> {
502    /// assert_eq!(
503    ///     "example-1:1.0.0-1-x86_64",
504    ///     InstalledPackage::new("example".parse()?, "1:1.0.0-1".parse()?, "x86_64".parse()?)
505    ///         .to_string()
506    /// );
507    /// # Ok(())
508    /// # }
509    /// ```
510    pub fn new(name: Name, version: FullVersion, architecture: Architecture) -> Self {
511        Self {
512            name,
513            version,
514            architecture,
515        }
516    }
517
518    /// Returns a reference to the [`Name`].
519    ///
520    /// # Examples
521    ///
522    /// ```
523    /// use std::str::FromStr;
524    ///
525    /// use alpm_types::{InstalledPackage, Name};
526    ///
527    /// # fn main() -> Result<(), alpm_types::Error> {
528    /// let file_name =
529    ///     InstalledPackage::new("example".parse()?, "1:1.0.0-1".parse()?, "x86_64".parse()?);
530    ///
531    /// assert_eq!(file_name.name(), &Name::new("example")?);
532    /// # Ok(())
533    /// # }
534    /// ```
535    pub fn name(&self) -> &Name {
536        &self.name
537    }
538
539    /// Returns a reference to the [`FullVersion`].
540    ///
541    /// # Examples
542    ///
543    /// ```
544    /// use std::str::FromStr;
545    ///
546    /// use alpm_types::{FullVersion, InstalledPackage};
547    ///
548    /// # fn main() -> Result<(), alpm_types::Error> {
549    /// let file_name =
550    ///     InstalledPackage::new("example".parse()?, "1:1.0.0-1".parse()?, "x86_64".parse()?);
551    ///
552    /// assert_eq!(file_name.version(), &FullVersion::from_str("1:1.0.0-1")?);
553    /// # Ok(())
554    /// # }
555    /// ```
556    pub fn version(&self) -> &FullVersion {
557        &self.version
558    }
559
560    /// Returns the [`Architecture`].
561    ///
562    /// # Examples
563    ///
564    /// ```
565    /// use std::str::FromStr;
566    ///
567    /// use alpm_types::{InstalledPackage, SystemArchitecture};
568    ///
569    /// # fn main() -> Result<(), alpm_types::Error> {
570    /// let file_name =
571    ///     InstalledPackage::new("example".parse()?, "1:1.0.0-1".parse()?, "x86_64".parse()?);
572    ///
573    /// assert_eq!(file_name.architecture(), &SystemArchitecture::X86_64.into());
574    /// # Ok(())
575    /// # }
576    /// ```
577    pub fn architecture(&self) -> &Architecture {
578        &self.architecture
579    }
580
581    /// Returns the [`PackageRelation`] encoded in this [`InstalledPackage`].
582    ///
583    /// # Examples
584    ///
585    /// ```
586    /// use std::str::FromStr;
587    ///
588    /// use alpm_types::{InstalledPackage, PackageRelation};
589    ///
590    /// # fn main() -> Result<(), alpm_types::Error> {
591    /// let installed_package =
592    ///     InstalledPackage::new("example".parse()?, "1:1.0.0-1".parse()?, "x86_64".parse()?);
593    ///
594    /// assert_eq!(
595    ///     installed_package.to_package_relation(),
596    ///     PackageRelation::from_str("example=1:1.0.0-1")?
597    /// );
598    /// # Ok(())
599    /// # }
600    /// ```
601    pub fn to_package_relation(&self) -> PackageRelation {
602        PackageRelation {
603            name: self.name.clone(),
604            version_requirement: Some(VersionRequirement {
605                comparison: VersionComparison::Equal,
606                version: self.version.clone().into(),
607            }),
608        }
609    }
610
611    /// Recognizes an [`InstalledPackage`] in a string slice.
612    ///
613    /// Relies on [`winnow`] to parse `input` and recognize the [`Name`], [`FullVersion`], and
614    /// [`Architecture`] components.
615    ///
616    /// # Errors
617    ///
618    /// Returns an error if
619    ///
620    /// - the [`Name`] component can not be recognized,
621    /// - the [`FullVersion`] component can not be recognized,
622    /// - or the [`Architecture`] component can not be recognized.
623    ///
624    /// # Examples
625    ///
626    /// ```
627    /// use alpm_types::InstalledPackage;
628    /// use winnow::Parser;
629    ///
630    /// # fn main() -> Result<(), alpm_types::Error> {
631    /// let name = "example-package-1:1.0.0-1-x86_64";
632    /// assert_eq!(name, InstalledPackage::parser.parse(name)?.to_string());
633    /// # Ok(())
634    /// # }
635    /// ```
636    pub fn parser(input: &mut &str) -> ModalResult<Self> {
637        // Detect the amount of dashes in input and subsequently in the Name component.
638        //
639        // This is a necessary step because dashes are used as delimiters between the
640        // components of the file name and the Name component (an alpm-package-name) can contain
641        // dashes, too.
642        // We know that the minimum amount of dashes in a valid alpm-package file name is
643        // three (one dash between the Name, Version, PackageRelease, and Architecture
644        // component each).
645        // We rely on this fact to determine the amount of dashes in the Name component and
646        // thereby the cut-off point between the Name and the Version component.
647        let dashes: usize = input.chars().filter(|char| char == &'-').count();
648
649        if dashes < 2 {
650            let context_error = ContextError::from_input(input)
651                .add_context(
652                    input,
653                    &input.checkpoint(),
654                    StrContext::Label("alpm-package file name"),
655                )
656                .add_context(
657                    input,
658                    &input.checkpoint(),
659                    StrContext::Expected(StrContextValue::Description(
660                        concat!(
661                        "a package name, followed by an alpm-package-version (full or full with epoch) and an architecture.",
662                        "\nAll components must be delimited with a dash ('-')."
663                        )
664                    ))
665                );
666
667            return Err(ErrMode::Cut(context_error));
668        }
669
670        // The (zero or more) dashes in the Name component.
671        let dashes_in_name = dashes.saturating_sub(3);
672
673        // Advance the parser to the dash just behind the Name component, based on the amount of
674        // dashes in the Name, e.g.:
675        // "example-package-1:1.0.0-1-x86_64.pkg.tar.zst" -> "-1:1.0.0-1-x86_64.pkg.tar.zst"
676        let name = cut_err(
677            repeat::<_, _, (), _, _>(
678                dashes_in_name + 1,
679                // Advances to the next `-`.
680                // If multiple `-` are present, the `-` that has been previously advanced to will
681                // be consumed in the next itaration via the `opt("-")`. This enables us to go
682                // **up to** the last `-`, while still consuming all `-` in between.
683                (opt("-"), take_until(0.., "-"), peek("-")),
684            )
685            .take()
686            // example-package
687            .and_then(Name::parser),
688        )
689        .context(StrContext::Label("alpm-package-name"))
690        .parse_next(input)?;
691
692        // Consume leading dash in front of Version, e.g.:
693        // "-1:1.0.0-1-x86_64.pkg.tar.zst" -> "1:1.0.0-1-x86_64.pkg.tar.zst"
694        "-".parse_next(input)?;
695
696        // Advance the parser to beyond the Version component (which contains one dash), e.g.:
697        // "1:1.0.0-1-x86_64.pkg.tar.zst" -> "-x86_64.pkg.tar.zst"
698        let version: FullVersion = cut_err((take_until(0.., "-"), "-", take_until(0.., "-")))
699            .context(StrContext::Label("alpm-package-version"))
700            .context(StrContext::Expected(StrContextValue::Description(
701                "an alpm-package-version (full or full with epoch) followed by a `-` and an architecture",
702            )))
703            .take()
704            .and_then(cut_err(FullVersion::parser))
705            .parse_next(input)?;
706
707        // Consume leading dash, e.g.:
708        // "-x86_64.pkg.tar.zst" -> "x86_64.pkg.tar.zst"
709        "-".parse_next(input)?;
710
711        // Advance the parser to beyond the Architecture component, e.g.:
712        // "x86_64.pkg.tar.zst" -> ".pkg.tar.zst"
713        let architecture = rest.and_then(Architecture::parser).parse_next(input)?;
714
715        Ok(Self {
716            name,
717            version,
718            architecture,
719        })
720    }
721}
722
723impl From<PackageFileName> for InstalledPackage {
724    /// Creates a [`InstalledPackage`] from a [`PackageFileName`].
725    fn from(value: PackageFileName) -> Self {
726        Self {
727            name: value.name,
728            version: value.version,
729            architecture: value.architecture,
730        }
731    }
732}
733
734impl FromStr for InstalledPackage {
735    type Err = Error;
736
737    /// Creates an [`InstalledPackage`] from a string slice.
738    ///
739    /// Delegates to [`InstalledPackage::parser`].
740    ///
741    /// # Errors
742    ///
743    /// Returns an error if [`InstalledPackage::parser`] fails.
744    ///
745    /// # Examples
746    ///
747    /// ```
748    /// use std::str::FromStr;
749    ///
750    /// use alpm_types::InstalledPackage;
751    ///
752    /// # fn main() -> Result<(), alpm_types::Error> {
753    /// let filename = "example-package-1:1.0.0-1-x86_64";
754    /// assert_eq!(filename, InstalledPackage::from_str(filename)?.to_string());
755    /// # Ok(())
756    /// # }
757    /// ```
758    fn from_str(s: &str) -> Result<InstalledPackage, Self::Err> {
759        Ok(Self::parser.parse(s)?)
760    }
761}
762
763impl Display for InstalledPackage {
764    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
765        write!(fmt, "{}-{}-{}", self.name, self.version, self.architecture)
766    }
767}
768
769#[cfg(test)]
770mod tests {
771    use rstest::rstest;
772    use testresult::TestResult;
773
774    use super::*;
775    use crate::SystemArchitecture;
776
777    #[rstest]
778    #[case(
779        "!makeflags",
780        MakepkgOption::BuildEnvironment(BuildEnvironmentOption::MakeFlags(false))
781    )]
782    #[case("autodeps", MakepkgOption::Package(PackageOption::AutoDeps(true)))]
783    #[case(
784        "ccache",
785        MakepkgOption::BuildEnvironment(BuildEnvironmentOption::Ccache(true))
786    )]
787    fn makepkg_option(#[case] input: &str, #[case] expected: MakepkgOption) {
788        let result = MakepkgOption::from_str(input).expect("Parser should be successful");
789        assert_eq!(result, expected);
790    }
791
792    #[rstest]
793    #[case(
794        "!somethingelse",
795        concat!(
796            "expected `buildflags`, `ccache`, `check`, `color`, `distcc`, `sign`, `makeflags`, ",
797            "`autodeps`, `debug`, `docs`, `emptydirs`, `libtool`, `lto`, `pestrip`, `purge`, ",
798            "`staticlibs`, `strip`, `zipman`",
799        )
800    )]
801    #[case(
802        "#somethingelse",
803        "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
804    )]
805    fn invalid_makepkg_option(#[case] input: &str, #[case] err_snippet: &str) {
806        let Err(Error::ParseError(err_msg)) = MakepkgOption::from_str(input) else {
807            panic!("'{input}' erroneously parsed as VersionRequirement")
808        };
809        assert!(
810            err_msg.contains(err_snippet),
811            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
812        );
813    }
814
815    #[rstest]
816    #[case("autodeps", PackageOption::AutoDeps(true))]
817    #[case("debug", PackageOption::Debug(true))]
818    #[case("docs", PackageOption::Docs(true))]
819    #[case("emptydirs", PackageOption::EmptyDirs(true))]
820    #[case("!libtool", PackageOption::Libtool(false))]
821    #[case("lto", PackageOption::Lto(true))]
822    #[case("pestrip", PackageOption::PEStrip(true))]
823    #[case("purge", PackageOption::Purge(true))]
824    #[case("staticlibs", PackageOption::StaticLibs(true))]
825    #[case("strip", PackageOption::Strip(true))]
826    #[case("zipman", PackageOption::Zipman(true))]
827    fn package_option(#[case] s: &str, #[case] expected: PackageOption) {
828        let result = PackageOption::from_str(s).expect("Parser should be successful");
829        assert_eq!(result, expected);
830    }
831
832    #[rstest]
833    #[case(
834        "!somethingelse",
835        "expected `autodeps`, `debug`, `docs`, `emptydirs`, `libtool`, `lto`, `pestrip`, `purge`, `staticlibs`, `strip`, `zipman`"
836    )]
837    #[case(
838        "#somethingelse",
839        "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
840    )]
841    fn invalid_package_option(#[case] input: &str, #[case] err_snippet: &str) {
842        let Err(Error::ParseError(err_msg)) = PackageOption::from_str(input) else {
843            panic!("'{input}' erroneously parsed as VersionRequirement")
844        };
845        assert!(
846            err_msg.contains(err_snippet),
847            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
848        );
849    }
850
851    #[rstest]
852    #[case("buildflags", BuildEnvironmentOption::BuildFlags(true))]
853    #[case("ccache", BuildEnvironmentOption::Ccache(true))]
854    #[case("check", BuildEnvironmentOption::Check(true))]
855    #[case("color", BuildEnvironmentOption::Color(true))]
856    #[case("distcc", BuildEnvironmentOption::Distcc(true))]
857    #[case("!makeflags", BuildEnvironmentOption::MakeFlags(false))]
858    #[case("sign", BuildEnvironmentOption::Sign(true))]
859    #[case("!sign", BuildEnvironmentOption::Sign(false))]
860    fn build_environment_option(#[case] input: &str, #[case] expected: BuildEnvironmentOption) {
861        let result = BuildEnvironmentOption::from_str(input).expect("Parser should be successful");
862        assert_eq!(result, expected);
863    }
864
865    #[rstest]
866    #[case(
867        "!somethingelse",
868        "expected `buildflags`, `ccache`, `check`, `color`, `distcc`, `sign`, `makeflags`"
869    )]
870    #[case(
871        "#somethingelse",
872        "expected `!`, ASCII alphanumeric character, `-`, `.`, `_`"
873    )]
874    fn invalid_build_environment_option(#[case] input: &str, #[case] err_snippet: &str) {
875        let Err(Error::ParseError(err_msg)) = BuildEnvironmentOption::from_str(input) else {
876            panic!("'{input}' erroneously parsed as VersionRequirement")
877        };
878        assert!(
879            err_msg.contains(err_snippet),
880            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
881        );
882    }
883
884    #[rstest]
885    #[case("#test", "invalid character in makepkg option")]
886    #[case("test!", "invalid character in makepkg option")]
887    fn invalid_option(#[case] input: &str, #[case] error_snippet: &str) {
888        let result = option_name_parser.parse(input);
889        assert!(result.is_err(), "Expected makepkg option parsing to fail");
890        let err = result.unwrap_err();
891        let pretty_error = err.to_string();
892        assert!(
893            pretty_error.contains(error_snippet),
894            "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
895        );
896    }
897
898    #[rstest]
899    #[case(
900        "foo-bar-1:1.0.0-1-any",
901        InstalledPackage {
902            name: Name::new("foo-bar")?,
903            version: FullVersion::from_str("1:1.0.0-1")?,
904            architecture: Architecture::Any,
905        },
906    )]
907    #[case(
908        "foobar-1.0.0-1-x86_64",
909        InstalledPackage {
910            name: Name::new("foobar")?,
911            version: FullVersion::from_str("1.0.0-1")?,
912            architecture: SystemArchitecture::X86_64.into(),
913        },
914    )]
915    fn installed_from_str(#[case] s: &str, #[case] result: InstalledPackage) -> TestResult {
916        assert_eq!(InstalledPackage::from_str(s), Ok(result));
917        Ok(())
918    }
919
920    #[rstest]
921    #[case("foo-1:1.0.0-bar-any", "invalid package release")]
922    #[case(
923        "foo-1:1.0.0_any",
924        "expected a package name, followed by an alpm-package-version (full or full with epoch) and an architecture."
925    )]
926    #[case("packagename-30-0.1oops-any", "expected end of package release value")]
927    #[case("package$with$dollars-30-0.1-any", "invalid character in package name")]
928    #[case(
929        "packagename-30-0.1-any*asdf",
930        "invalid character in system architecture"
931    )]
932    fn installed_new_parse_error(#[case] input: &str, #[case] error_snippet: &str) {
933        let result = InstalledPackage::from_str(input);
934        assert!(result.is_err(), "Expected InstalledPackage parsing to fail");
935        let err = result.unwrap_err();
936        let pretty_error = err.to_string();
937        assert!(
938            pretty_error.contains(error_snippet),
939            "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
940        );
941    }
942}