Skip to main content

alpm_types/
pkg.rs

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