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