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