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