Skip to main content

alpm_types/
pkg.rs

1use std::{convert::Infallible, fmt::Display, str::FromStr};
2
3use alpm_parsers::{
4    iter_str_context,
5    traits::{AlpmParser, ParserUntil},
6};
7use serde::{Deserialize, Serialize};
8use serde_with::{DeserializeFromStr, SerializeDisplay};
9use strum::{Display, EnumString, VariantNames};
10use winnow::{
11    ModalResult,
12    Parser,
13    ascii::{alpha1, space0},
14    combinator::{alt, not, peek, repeat_till},
15    error::{ContextError, ErrMode, StrContext, StrContextValue},
16    token::any,
17};
18
19use crate::{Error, Name};
20
21/// The type of a package
22///
23/// ## Examples
24/// ```
25/// use std::str::FromStr;
26///
27/// use alpm_types::PackageType;
28///
29/// // create PackageType from str
30/// assert_eq!(PackageType::from_str("pkg"), Ok(PackageType::Package));
31///
32/// // format as String
33/// assert_eq!("debug", format!("{}", PackageType::Debug));
34/// assert_eq!("pkg", format!("{}", PackageType::Package));
35/// assert_eq!("src", format!("{}", PackageType::Source));
36/// assert_eq!("split", format!("{}", PackageType::Split));
37/// ```
38#[derive(Clone, Copy, Debug, Display, EnumString, Eq, PartialEq, Serialize, VariantNames)]
39pub enum PackageType {
40    /// a debug package
41    #[strum(to_string = "debug")]
42    Debug,
43    /// a single (non-split) package
44    #[strum(to_string = "pkg")]
45    Package,
46    /// a source-only package
47    #[strum(to_string = "src")]
48    Source,
49    /// one split package out of a set of several
50    #[strum(to_string = "split")]
51    Split,
52}
53
54impl AlpmParser for PackageType {
55    /// Recognizes a [`PackageType`] in a string slice.
56    ///
57    /// # Errors
58    ///
59    /// Returns an error if `input` does not begin with a valid variant
60    /// of [`PackageType`].
61    fn parser(input: &mut &str) -> Result<Self, ErrMode<ContextError>> {
62        alpha1
63            .try_map(PackageType::from_str)
64            .context(StrContext::Label("package type"))
65            .context_with(iter_str_context!([PackageType::VARIANTS]))
66            .parse_next(input)
67    }
68
69    fn delimiter_error_context<'a, O, P>(
70        parser: P,
71    ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
72    where
73        P: Parser<&'a str, O, ErrMode<ContextError>>,
74    {
75        parser
76            .context(StrContext::Label("package type"))
77            .context(StrContext::Expected(StrContextValue::Description(
78                "a string consisting of alphabetic characters",
79            )))
80    }
81}
82
83/// Description of a package
84///
85/// This type enforces the following invariants on the contained string:
86/// - No leading/trailing spaces
87/// - Tabs and newlines are substituted with spaces.
88/// - Multiple, consecutive spaces are substituted with a single space.
89///
90/// This is a type alias for [`String`].
91///
92/// ## Examples
93///
94/// ```
95/// use alpm_types::PackageDescription;
96///
97/// # fn main() {
98/// // Create PackageDescription from a string slice
99/// let description = PackageDescription::from("my special package ");
100///
101/// assert_eq!(&description.to_string(), "my special package");
102/// # }
103/// ```
104#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
105pub struct PackageDescription(String);
106
107impl PackageDescription {
108    /// Create a new `PackageDescription` from a given `String`.
109    pub fn new(description: &str) -> Self {
110        Self::from(description)
111    }
112}
113
114impl Default for PackageDescription {
115    /// Returns the default [`PackageDescription`].
116    ///
117    /// Following the default for [`String`], this returns a [`PackageDescription`] wrapping an
118    /// empty string.
119    fn default() -> Self {
120        Self::new("")
121    }
122}
123
124impl FromStr for PackageDescription {
125    type Err = Infallible;
126
127    fn from_str(s: &str) -> Result<Self, Self::Err> {
128        Ok(Self::from(s))
129    }
130}
131
132impl AsRef<str> for PackageDescription {
133    /// Returns a reference to the inner [`String`].
134    fn as_ref(&self) -> &str {
135        &self.0
136    }
137}
138
139impl From<&str> for PackageDescription {
140    /// Creates a new [`PackageDescription`] from a string slice.
141    ///
142    /// Trims leading and trailing whitespace.
143    /// Replaces any new lines and tabs with a space.
144    /// Replaces any consecutive spaces with a single space.
145    fn from(value: &str) -> Self {
146        // Trim front and back and replace unwanted whitespace chars.
147        let mut description = value.trim().replace(['\n', '\r', '\t'], " ");
148
149        // Remove all spaces that follow a space.
150        let mut previous = ' ';
151        description.retain(|ch| {
152            if ch == ' ' && previous == ' ' {
153                return false;
154            };
155            previous = ch;
156            true
157        });
158
159        Self(description)
160    }
161}
162
163impl Display for PackageDescription {
164    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165        write!(f, "{}", self.0)
166    }
167}
168
169/// Name of the base package information that one or more packages are built from.
170///
171/// This is a type alias for [`Name`].
172///
173/// ## Examples
174/// ```
175/// use std::str::FromStr;
176///
177/// use alpm_types::{Error, Name};
178///
179/// # fn main() -> Result<(), alpm_types::Error> {
180/// // create PackageBaseName from &str
181/// let pkgbase = Name::from_str("test-123@.foo_+")?;
182///
183/// // format as String
184/// let pkgbase = Name::from_str("foo")?;
185/// assert_eq!("foo", pkgbase.to_string());
186/// # Ok(())
187/// # }
188/// ```
189pub type PackageBaseName = Name;
190
191/// Extra data entry associated with a package
192///
193/// This type wraps a key-value pair of data as String, which is separated by an equal sign (`=`).
194#[derive(Clone, Debug, DeserializeFromStr, PartialEq, SerializeDisplay)]
195pub struct ExtraDataEntry {
196    key: String,
197    value: String,
198}
199
200impl ExtraDataEntry {
201    /// Create a new extra_data
202    pub fn new(key: String, value: String) -> Self {
203        Self { key, value }
204    }
205
206    /// Return the key of the extra_data
207    pub fn key(&self) -> &str {
208        &self.key
209    }
210
211    /// Return the value of the extra_data
212    pub fn value(&self) -> &str {
213        &self.value
214    }
215}
216
217impl ParserUntil for ExtraDataEntry {
218    /// Recognizes an [`ExtraDataEntry`] in a string slice before a `delimiter`.
219    ///
220    /// # Errors
221    ///
222    /// Returns an error if `input` does not contain a valid [`ExtraDataEntry`] before the
223    /// `delimiter`.
224    fn parser_until<'a, P>(delimiter: P) -> impl Parser<&'a str, Self, ErrMode<ContextError>>
225    where
226        P: Parser<&'a str, &'a str, ErrMode<ContextError>>,
227    {
228        // Define the actual parser closure.
229        // The delimiter is moved into the closure and borrowed via `by_ref()` on each call.
230        let mut delimiter_parser = delimiter;
231        move |input: &mut &'a str| -> ModalResult<Self> {
232            // Handle the case were there's no key
233            not("=")
234                .context(StrContext::Label("extra data"))
235                .context(StrContext::Expected(StrContextValue::Description(
236                    "a utf-8 key before the `=` delimiter",
237                )))
238                .parse_next(input)?;
239
240            let key: &str = repeat_till::<_, _, (), _, _, _, _>(
241                1..,
242                any,
243                peek(alt((
244                    (space0, "=", space0).take(),
245                    delimiter_parser.by_ref(),
246                ))),
247            )
248            .take()
249            .context(StrContext::Label("extra data key"))
250            .context(StrContext::Expected(StrContextValue::Description(
251                "a UTF-8 string, followed by an equals (`=`) character.",
252            )))
253            .parse_next(input)?;
254
255            (space0, "=", space0)
256                .context(StrContext::Label("extra data delimiter"))
257                .context(StrContext::Expected(StrContextValue::Description(
258                    "a `=` between the key and value",
259                )))
260                .parse_next(input)?;
261
262            let value: &str =
263                repeat_till::<_, _, (), _, _, _, _>(1.., any, peek(delimiter_parser.by_ref()))
264                    .take()
265                    .context(StrContext::Label("extra data value"))
266                    .context(StrContext::Expected(StrContextValue::Description(
267                        "a UTF-8 string",
268                    )))
269                    .parse_next(input)?;
270
271            peek(delimiter_parser.by_ref())
272                .context(StrContext::Label("extra data value"))
273                .context(StrContext::Expected(StrContextValue::Description(
274                    "end of input",
275                )))
276                .parse_next(input)?;
277
278            Ok(Self::new(key.trim().to_string(), value.trim().to_string()))
279        }
280    }
281}
282
283impl FromStr for ExtraDataEntry {
284    type Err = Error;
285
286    /// Parses an `extra_data` from string.
287    ///
288    /// The string is expected to be in the format `key=value`.
289    ///
290    /// ## Errors
291    ///
292    /// This function returns an error if the string is missing the key or value component.
293    ///
294    /// ## Examples
295    ///
296    /// ```
297    /// use std::str::FromStr;
298    ///
299    /// use alpm_types::{ExtraDataEntry, PackageType};
300    ///
301    /// # fn main() -> Result<(), alpm_types::Error> {
302    /// // create ExtraDataEntry from str
303    /// let extra_data: ExtraDataEntry = ExtraDataEntry::from_str("pkgtype=debug")?;
304    /// assert_eq!(extra_data.key(), "pkgtype");
305    /// assert_eq!(extra_data.value(), "debug");
306    /// # Ok(())
307    /// # }
308    /// ```
309    fn from_str(s: &str) -> Result<Self, Self::Err> {
310        Ok(Self::parser_until_eof.parse(s)?)
311    }
312}
313
314impl Display for ExtraDataEntry {
315    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
316        write!(f, "{}={}", self.key, self.value)
317    }
318}
319
320/// Extra data associated with a package.
321///
322/// This type wraps a vector of [`ExtraDataEntry`] items enforcing that it includes a valid
323/// `pkgtype` entry.
324///
325/// Can be created from a [`Vec<ExtraDataEntry>`] or [`ExtraDataEntry`] using [`TryFrom::try_from`].
326#[derive(Clone, Debug, PartialEq, Serialize)]
327pub struct ExtraData(Vec<ExtraDataEntry>);
328
329impl ExtraData {
330    /// Returns the package type.
331    pub fn pkg_type(&self) -> PackageType {
332        self.0
333            .iter()
334            .find(|v| v.key() == "pkgtype")
335            .map(|v| PackageType::from_str(v.value()).expect("Invalid package type"))
336            .unwrap_or_else(|| unreachable!("Valid xdata should always contain a pkgtype entry."))
337    }
338
339    /// Returns the number of extra data entries.
340    pub fn len(&self) -> usize {
341        self.0.len()
342    }
343
344    /// Returns true if there are no extra data entries.
345    ///
346    /// Due to the invariant enforced in [`TryFrom`], this will always return `false` and is only
347    /// included for consistency with [`Vec::is_empty`] in the standard library.
348    pub fn is_empty(&self) -> bool {
349        self.0.is_empty()
350    }
351}
352
353impl TryFrom<Vec<ExtraDataEntry>> for ExtraData {
354    type Error = Error;
355
356    /// Creates an [`ExtraData`] from a vector of [`ExtraDataEntry`].
357    ///
358    /// ## Errors
359    ///
360    /// Returns an error in the following cases:
361    ///
362    /// - if the `value` does not contain a `pkgtype` key.
363    /// - if the `pkgtype` entry does not contain a valid package type.
364    fn try_from(value: Vec<ExtraDataEntry>) -> Result<Self, Self::Error> {
365        if let Some(pkg_type) = value.iter().find(|v| v.key() == "pkgtype") {
366            let _ = PackageType::from_str(pkg_type.value())?;
367            Ok(Self(value))
368        } else {
369            Err(Error::MissingComponent {
370                component: "extra_data with a valid \"pkgtype\" entry",
371            })
372        }
373    }
374}
375
376impl TryFrom<ExtraDataEntry> for ExtraData {
377    type Error = Error;
378
379    /// Creates an [`ExtraData`] from a single [`ExtraDataEntry`].
380    ///
381    /// Delegates to [`TryFrom::try_from`] for [`Vec<ExtraDataEntry>`].
382    ///
383    /// ## Errors
384    ///
385    /// If the [`TryFrom::try_from`] for [`Vec<ExtraDataEntry>`] returns an error.
386    fn try_from(value: ExtraDataEntry) -> Result<Self, Self::Error> {
387        Self::try_from(vec![value])
388    }
389}
390
391impl From<ExtraData> for Vec<ExtraDataEntry> {
392    /// Converts the [`ExtraData`] into a [`Vec<ExtraDataEntry>`].
393    fn from(value: ExtraData) -> Self {
394        value.0
395    }
396}
397
398impl IntoIterator for ExtraData {
399    type Item = ExtraDataEntry;
400    type IntoIter = std::vec::IntoIter<ExtraDataEntry>;
401
402    /// Consumes the [`ExtraData`] and returns an iterator over [`ExtraDataEntry`] items.
403    fn into_iter(self) -> Self::IntoIter {
404        self.0.into_iter()
405    }
406}
407
408impl AsRef<[ExtraDataEntry]> for ExtraData {
409    /// Returns a reference to the inner [`Vec<ExtraDataEntry>`].
410    fn as_ref(&self) -> &[ExtraDataEntry] {
411        &self.0
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use std::str::FromStr;
418
419    use insta::assert_snapshot;
420    use rstest::rstest;
421    use testresult::TestResult;
422
423    use super::*;
424    use crate::configure_insta;
425
426    #[rstest]
427    #[case("debug", Ok(PackageType::Debug))]
428    #[case("pkg", Ok(PackageType::Package))]
429    #[case("src", Ok(PackageType::Source))]
430    #[case("split", Ok(PackageType::Split))]
431    #[case("foo", Err(strum::ParseError::VariantNotFound))]
432    fn pkgtype_from_string(
433        #[case] from_str: &str,
434        #[case] result: Result<PackageType, strum::ParseError>,
435    ) {
436        assert_eq!(PackageType::from_str(from_str), result);
437    }
438
439    #[rstest]
440    #[case(PackageType::Debug, "debug")]
441    #[case(PackageType::Package, "pkg")]
442    #[case(PackageType::Source, "src")]
443    #[case(PackageType::Split, "split")]
444    fn pkgtype_format_string(#[case] pkgtype: PackageType, #[case] pkgtype_str: &str) {
445        assert_eq!(pkgtype_str, format!("{pkgtype}"));
446    }
447
448    #[rstest]
449    #[case("key=value", "key", "value")]
450    #[case("pkgtype=debug", "pkgtype", "debug")]
451    #[case("test-123@.foo_+=1000", "test-123@.foo_+", "1000")]
452    fn extra_data_entry_from_str(
453        #[case] data: &str,
454        #[case] key: &str,
455        #[case] value: &str,
456    ) -> TestResult {
457        let extra_data = ExtraDataEntry::from_str(data)?;
458        assert_eq!(extra_data.key(), key);
459        assert_eq!(extra_data.value(), value);
460        assert_eq!(extra_data.to_string(), data);
461        Ok(())
462    }
463
464    #[rstest]
465    #[case("key")]
466    #[case("key=")]
467    #[case("=value")]
468    fn extra_data_entry_from_str_error(#[case] input: &str) {
469        let Err(Error::ParseError(err_msg)) = ExtraDataEntry::from_str(input) else {
470            panic!("'{input}' erroneously parsed as a ExtraDataEntry")
471        };
472
473        let (test_name, _guard) = configure_insta();
474        assert_snapshot!(test_name, err_msg.to_string());
475    }
476
477    #[rstest]
478    #[case::empty_list(vec![])]
479    #[case::invalid_pkgtype(vec![ExtraDataEntry::from_str("pkgtype=foo")?])]
480    fn extra_data_invalid(#[case] xdata: Vec<ExtraDataEntry>) -> TestResult {
481        assert!(ExtraData::try_from(xdata).is_err());
482        Ok(())
483    }
484
485    #[rstest]
486    #[case::only_pkgtype(vec![ExtraDataEntry::from_str("pkgtype=pkg")?])]
487    #[case::with_additional_xdata_entry(vec![ExtraDataEntry::from_str("pkgtype=pkg")?, ExtraDataEntry::from_str("foo=bar")?])]
488    fn extra_data_valid(#[case] xdata: Vec<ExtraDataEntry>) -> TestResult {
489        let xdata = ExtraData::try_from(xdata)?;
490        assert_eq!(xdata.pkg_type(), PackageType::Package);
491        Ok(())
492    }
493
494    #[rstest]
495    #[case("  trailing  ", "trailing")]
496    #[case("in    between    words", "in between words")]
497    #[case("\nsome\t whitespace\n chars\n", "some whitespace chars")]
498    #[case("  \neverything\t   combined\n yeah \n   ", "everything combined yeah")]
499    fn package_description(#[case] input: &str, #[case] result: &str) {
500        assert_eq!(PackageDescription::new(input).to_string(), result);
501    }
502}