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