alpm_types/relation/
composite.rs

1//! Composite relation types used in metadata files.
2
3use std::{
4    fmt::{Display, Formatter},
5    str::FromStr,
6};
7
8use serde::{Deserialize, Serialize};
9use winnow::{
10    ModalResult,
11    Parser,
12    combinator::{cut_err, fail},
13    error::{StrContext, StrContextValue},
14    stream::Stream,
15    token::rest,
16};
17
18use crate::{Error, PackageRelation, SonameV1, SonameV2};
19
20/// Provides either a [`PackageRelation`], a [`SonameV1`] or a [`SonameV2`].
21///
22/// This enum is used for [alpm-package-relations] of type _run-time dependency_ and _provision_
23/// e.g. in [PKGINFO], [SRCINFO] or [alpm-db-desc] files.
24///
25/// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
26/// [SRCINFO]: https://alpm.archlinux.page/specifications/SRCINFO.5.html
27/// [alpm-db-desc]: https://alpm.archlinux.page/specifications/alpm-db-desc.5.html
28/// [alpm-package-relations]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
29#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
30#[serde(untagged)]
31pub enum RelationOrSoname {
32    /// A package relation (as [`PackageRelation`]).
33    Relation(PackageRelation),
34    /// A shared object name following [alpm-sonamev1].
35    ///
36    /// [alpm-sonamev1]: https://alpm.archlinux.page/specifications/alpm-sonamev1.7.html
37    SonameV1(SonameV1),
38    /// A shared object name following [alpm-sonamev2].
39    ///
40    /// [alpm-sonamev2]: https://alpm.archlinux.page/specifications/alpm-sonamev2.7.html
41    SonameV2(SonameV2),
42}
43
44impl PartialEq<PackageRelation> for RelationOrSoname {
45    fn eq(&self, other: &PackageRelation) -> bool {
46        self.to_string() == other.to_string()
47    }
48}
49
50impl PartialEq<SonameV1> for RelationOrSoname {
51    fn eq(&self, other: &SonameV1) -> bool {
52        self.to_string() == other.to_string()
53    }
54}
55
56impl PartialEq<SonameV2> for RelationOrSoname {
57    fn eq(&self, other: &SonameV2) -> bool {
58        self.to_string() == other.to_string()
59    }
60}
61
62impl RelationOrSoname {
63    /// Recognizes a [`SonameV2`], a [`SonameV1`] or a [`PackageRelation`] in a string slice.
64    ///
65    /// First attempts to recognize a [`SonameV2`], then a [`SonameV1`] and if that fails, falls
66    /// back to recognizing a [`PackageRelation`].
67    /// Depending on recognized type, a [`RelationOrSoname`] is created accordingly.
68    pub fn parser(input: &mut &str) -> ModalResult<Self> {
69        // Implement a custom `winnow::combinator::alt`, as all type parsers are built in
70        // such a way that they return errors on unexpected input instead of backtracking.
71        let checkpoint = input.checkpoint();
72        let sonamev2_result = SonameV2::parser.parse_next(input);
73        if sonamev2_result.is_ok() {
74            let sonamev2 = sonamev2_result?;
75            return Ok(RelationOrSoname::SonameV2(sonamev2));
76        }
77
78        input.reset(&checkpoint);
79        let sonamev1_result = SonameV1::parser.parse_next(input);
80        if sonamev1_result.is_ok() {
81            let sonamev1 = sonamev1_result?;
82            return Ok(RelationOrSoname::SonameV1(sonamev1));
83        }
84
85        input.reset(&checkpoint);
86        let relation_result = rest.and_then(PackageRelation::parser).parse_next(input);
87        if relation_result.is_ok() {
88            let relation = relation_result?;
89            return Ok(RelationOrSoname::Relation(relation));
90        }
91
92        cut_err(fail)
93            .context(StrContext::Expected(StrContextValue::Description(
94                "alpm-sonamev2, alpm-sonamev1 or alpm-package-relation",
95            )))
96            .parse_next(input)
97    }
98}
99
100impl Display for RelationOrSoname {
101    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
102        match self {
103            RelationOrSoname::Relation(version) => write!(f, "{version}"),
104            RelationOrSoname::SonameV1(soname) => write!(f, "{soname}"),
105            RelationOrSoname::SonameV2(soname) => write!(f, "{soname}"),
106        }
107    }
108}
109
110impl FromStr for RelationOrSoname {
111    type Err = Error;
112
113    /// Creates a [`RelationOrSoname`] from a string slice.
114    ///
115    /// Relies on [`RelationOrSoname::parser`] to recognize types in `input` and create a
116    /// [`RelationOrSoname`] accordingly.
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if no [`RelationOrSoname`] can be created from `input`.
121    ///
122    /// # Examples
123    ///
124    /// ```
125    /// use alpm_types::{PackageRelation, RelationOrSoname, SonameV1, SonameV2};
126    ///
127    /// # fn main() -> Result<(), alpm_types::Error> {
128    /// let relation: RelationOrSoname = "example=1.0.0".parse()?;
129    /// assert_eq!(
130    ///     relation,
131    ///     RelationOrSoname::Relation(PackageRelation::new(
132    ///         "example".parse()?,
133    ///         Some("=1.0.0".parse()?)
134    ///     ))
135    /// );
136    ///
137    /// let sonamev2: RelationOrSoname = "lib:example.so.1".parse()?;
138    /// assert_eq!(
139    ///     sonamev2,
140    ///     RelationOrSoname::SonameV2(SonameV2::new("lib".parse()?, "example.so.1".parse()?))
141    /// );
142    ///
143    /// let sonamev1: RelationOrSoname = "example.so".parse()?;
144    /// assert_eq!(
145    ///     sonamev1,
146    ///     RelationOrSoname::SonameV1(SonameV1::new("example.so".parse()?, None, None)?)
147    /// );
148    /// # Ok(())
149    /// # }
150    /// ```
151    fn from_str(s: &str) -> Result<Self, Self::Err> {
152        Self::parser
153            .parse(s)
154            .map_err(|error| Error::ParseError(error.to_string()))
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use rstest::rstest;
161    use testresult::TestResult;
162
163    use super::*;
164    use crate::{ElfArchitectureFormat, Soname, VersionOrSoname};
165
166    #[rstest]
167    #[case("libexample.so.1", "invalid shared library prefix delimiter")]
168    #[case("lib:libexample.so-abc", "invalid version delimiter")]
169    #[case("lib:libexample.so.10-10", "invalid pkgver character")]
170    #[case("lib:libexample.so.1.0.0-64", "invalid pkgver character")]
171    fn invalid_sonamev2_parser(#[case] input: &str, #[case] error_snippet: &str) {
172        let result = SonameV2::from_str(input);
173        assert!(result.is_err(), "Expected SonameV2 parsing to fail");
174        let err = result.unwrap_err();
175        let pretty_error = err.to_string();
176        assert!(
177            pretty_error.contains(error_snippet),
178            "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
179        );
180    }
181
182    #[rstest]
183    #[case(
184        "example",
185        RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), None))
186    )]
187    #[case(
188        "example=1.0.0",
189        RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), "=1.0.0".parse().ok())) 
190    )]
191    #[case(
192        "example>=1.0.0",
193        RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), ">=1.0.0".parse().ok()))
194    )]
195    #[case(
196        "lib:example.so.1",
197        RelationOrSoname::SonameV2(
198            SonameV2::new(
199                "lib".parse().unwrap(),
200                Soname::from_str("example.so.1").unwrap(),
201            )
202        )
203    )]
204    #[case(
205        "lib:example.so",
206        RelationOrSoname::SonameV2(
207            SonameV2::new(
208                "lib".parse().unwrap(),
209                Soname::from_str("example.so").unwrap(),
210            )
211        )
212    )]
213    #[case(
214        "example.so",
215        RelationOrSoname::SonameV1(
216            SonameV1::new(
217                "example.so".parse().unwrap(),
218                None,
219                None,
220            ).unwrap()
221        )
222    )]
223    #[case(
224        "example.so=1.0.0-64",
225        RelationOrSoname::SonameV1(
226            SonameV1::new(
227                "example.so".parse().unwrap(),
228                Some(VersionOrSoname::Version("1.0.0".parse().unwrap())),
229                Some(ElfArchitectureFormat::Bit64),
230            ).unwrap()
231        )
232    )]
233    #[case(
234        "libexample.so=otherlibexample.so-64",
235        RelationOrSoname::SonameV1(
236            SonameV1::new(
237                "libexample.so".parse().unwrap(),
238                Some(VersionOrSoname::Soname("otherlibexample.so".parse().unwrap())),
239                Some(ElfArchitectureFormat::Bit64),
240            ).unwrap()
241        )
242    )]
243    fn test_relation_or_soname_parser(
244        #[case] mut input: &str,
245        #[case] expected: RelationOrSoname,
246    ) -> TestResult {
247        let input_str = input.to_string();
248        let result = RelationOrSoname::parser(&mut input)?;
249        assert_eq!(result, expected);
250        assert_eq!(result.to_string(), input_str);
251        Ok(())
252    }
253}