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