alpm_types/
relation.rs

1use std::{
2    fmt::{Display, Formatter},
3    str::FromStr,
4};
5
6use serde::{Deserialize, Serialize};
7use winnow::{
8    ModalResult,
9    Parser,
10    ascii::{digit1, space1},
11    combinator::{
12        alt,
13        cut_err,
14        eof,
15        fail,
16        opt,
17        peek,
18        repeat,
19        repeat_till,
20        separated_pair,
21        seq,
22        terminated,
23    },
24    error::{StrContext, StrContextValue},
25    stream::Stream,
26    token::{any, rest, take_till, take_until, take_while},
27};
28
29use crate::{
30    ElfArchitectureFormat,
31    Error,
32    Name,
33    PackageVersion,
34    SharedObjectName,
35    VersionRequirement,
36};
37
38/// Provides either a [`PackageVersion`] or a [`SharedObjectName`].
39///
40/// This enum is used when creating [`SonameV1`].
41#[derive(Clone, Debug, Eq, PartialEq)]
42pub enum VersionOrSoname {
43    /// A version for a [`SonameV1`].
44    Version(PackageVersion),
45
46    /// A soname for a [`SonameV1`].
47    Soname(SharedObjectName),
48}
49
50impl FromStr for VersionOrSoname {
51    type Err = Error;
52
53    fn from_str(s: &str) -> Result<Self, Self::Err> {
54        Ok(Self::parser.parse(s)?)
55    }
56}
57
58impl VersionOrSoname {
59    /// Recognizes a [`PackageVersion`] or [`SharedObjectName`] in a string slice.
60    ///
61    /// First attempts to recognize a [`SharedObjectName`] and if that fails, falls back to
62    /// recognizing a [`PackageVersion`].
63    pub fn parser(input: &mut &str) -> ModalResult<Self> {
64        // In the following, we're doing our own `alt` implementation.
65        // The reason for this is that we build our type parsers so that they throw errors
66        // if they encounter unexpected input instead of backtracking.
67        let checkpoint = input.checkpoint();
68        let soname_result = SharedObjectName::parser.parse_next(input);
69        if soname_result.is_ok() {
70            let soname = soname_result?;
71            return Ok(VersionOrSoname::Soname(soname));
72        }
73
74        input.reset(&checkpoint);
75        let version_result = rest.and_then(PackageVersion::parser).parse_next(input);
76        if version_result.is_ok() {
77            let version = version_result?;
78            return Ok(VersionOrSoname::Version(version));
79        }
80
81        cut_err(fail)
82            .context(StrContext::Expected(StrContextValue::Description(
83                "version or shared object name",
84            )))
85            .parse_next(input)
86    }
87}
88
89impl Display for VersionOrSoname {
90    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
91        match self {
92            VersionOrSoname::Version(version) => write!(f, "{version}"),
93            VersionOrSoname::Soname(soname) => write!(f, "{soname}"),
94        }
95    }
96}
97
98/// Representation of [soname] data of a shared object based on the [alpm-sonamev1] specification.
99///
100/// Soname data may be used as [alpm-package-relation] of type _provision_ and _run-time
101/// dependency_.
102/// This type distinguishes between three forms: _basic_, _unversioned_ and _explicit_.
103///
104/// - [`SonameV1::Basic`] is used when only the `name` of a _shared object_ file is used. This form
105///   can be used in files that may contain static data about package sources (e.g. [PKGBUILD] or
106///   [SRCINFO] files).
107/// - [`SonameV1::Unversioned`] is used when the `name` of a _shared object_ file, its _soname_
108///   (which does _not_ expose a specific version) and its `architecture` (derived from the [ELF]
109///   class of the file) are used. This form can be used in files that may contain dynamic data
110///   derived from a specific package build environment (i.e. [PKGINFO]). It is discouraged to use
111///   this form in files that contain static data about package sources (e.g. [PKGBUILD] or
112///   [SRCINFO] files).
113/// - [`SonameV1::Explicit`] is used when the `name` of a _shared object_ file, the `version` from
114///   its _soname_ and its `architecture` (derived from the [ELF] class of the file) are used. This
115///   form can be used in files that may contain dynamic data derived from a specific package build
116///   environment (i.e. [PKGINFO]). It is discouraged to use this form in files that contain static
117///   data about package sources (e.g. [PKGBUILD] or [SRCINFO] files).
118///
119/// # Warning
120///
121/// This type is **deprecated** and `SonameV2` should be preferred instead!
122/// Due to the loose nature of the [alpm-sonamev1] specification, the _basic_ form overlaps with the
123/// specification of [`Name`] and the _explicit_ form overlaps with that of [`PackageRelation`].
124///
125/// # Examples
126///
127/// ```
128/// use alpm_types::{ElfArchitectureFormat, SonameV1};
129///
130/// # fn main() -> Result<(), alpm_types::Error> {
131/// let basic_soname = SonameV1::Basic("example.so".parse()?);
132/// let unversioned_soname = SonameV1::Unversioned {
133///     name: "example.so".parse()?,
134///     soname: "example.so".parse()?,
135///     architecture: ElfArchitectureFormat::Bit64,
136/// };
137/// let explicit_soname = SonameV1::Explicit {
138///     name: "example.so".parse()?,
139///     version: "1.0.0".parse()?,
140///     architecture: ElfArchitectureFormat::Bit64,
141/// };
142/// # Ok(())
143/// # }
144/// ```
145///
146/// [alpm-package-relation]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
147/// [alpm-sonamev1]: https://alpm.archlinux.page/specifications/alpm-sonamev1.7.html
148/// [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
149/// [soname]: https://en.wikipedia.org/wiki/Soname
150/// [PKGBUILD]: https://man.archlinux.org/man/PKGBUILD.5
151/// [SRCINFO]: https://alpm.archlinux.page/specifications/SRCINFO.5.html
152/// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
153#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
154pub enum SonameV1 {
155    /// Basic representation of a _shared object_ file.
156    ///
157    /// Tracks the `name` of a _shared object_ file.
158    /// This form is used when referring to _shared object_ files without their soname data.
159    ///
160    /// # Examples
161    ///
162    /// ```
163    /// use std::str::FromStr;
164    ///
165    /// use alpm_types::SonameV1;
166    ///
167    /// # fn main() -> Result<(), alpm_types::Error> {
168    /// let soname = SonameV1::from_str("example.so")?;
169    /// assert_eq!(soname, SonameV1::Basic("example.so".parse()?));
170    /// # Ok(())
171    /// # }
172    /// ```
173    Basic(SharedObjectName),
174
175    /// Unversioned representation of an ELF file's soname data.
176    ///
177    /// Tracks the `name` of a _shared object_ file, its _soname_ instead of a version and its
178    /// `architecture`. This form is used if the _soname data_ of a _shared object_ does not
179    /// expose a version.
180    ///
181    /// # Examples
182    ///
183    /// ```
184    /// use std::str::FromStr;
185    ///
186    /// use alpm_types::{ElfArchitectureFormat, SonameV1};
187    ///
188    /// # fn main() -> Result<(), alpm_types::Error> {
189    /// let soname = SonameV1::from_str("example.so=example.so-64")?;
190    /// assert_eq!(
191    ///     soname,
192    ///     SonameV1::Unversioned {
193    ///         name: "example.so".parse()?,
194    ///         soname: "example.so".parse()?,
195    ///         architecture: ElfArchitectureFormat::Bit64,
196    ///     }
197    /// );
198    /// # Ok(())
199    /// # }
200    /// ```
201    Unversioned {
202        /// The least specific name of the shared object file.
203        name: SharedObjectName,
204        /// The value of the shared object's _SONAME_ field in its _dynamic section_.
205        soname: SharedObjectName,
206        /// The ELF architecture format of the shared object file.
207        architecture: ElfArchitectureFormat,
208    },
209
210    /// Explicit representation of an ELF file's soname data.
211    ///
212    /// Tracks the `name` of a _shared object_ file, the `version` of its _soname_ and its
213    /// `architecture`. This form is used if the _soname data_ of a _shared object_ exposes a
214    /// specific version.
215    ///
216    /// # Examples
217    ///
218    /// ```
219    /// use std::str::FromStr;
220    ///
221    /// use alpm_types::{ElfArchitectureFormat, SonameV1};
222    ///
223    /// # fn main() -> Result<(), alpm_types::Error> {
224    /// let soname = SonameV1::from_str("example.so=1.0.0-64")?;
225    /// assert_eq!(
226    ///    soname,
227    ///    SonameV1::Explicit {
228    ///         name: "example.so".parse()?,
229    ///         version: "1.0.0".parse()?,
230    ///         architecture: ElfArchitectureFormat::Bit64,
231    ///     }
232    /// );
233    /// # Ok(())
234    /// # }
235    Explicit {
236        /// The least specific name of the shared object file.
237        name: SharedObjectName,
238        /// The version of the shared object file (as exposed in its _soname_ data).
239        version: PackageVersion,
240        /// The ELF architecture format of the shared object file.
241        architecture: ElfArchitectureFormat,
242    },
243}
244
245impl SonameV1 {
246    /// Creates a new [`SonameV1`].
247    ///
248    /// Depending on input, this function returns different variants of [`SonameV1`]:
249    ///
250    /// - [`SonameV1::Basic`], if both `version_or_soname` and `architecture` are [`None`]
251    /// - [`SonameV1::Unversioned`], if `version_or_soname` is [`VersionOrSoname::Soname`] and
252    ///   `architecture` is [`Some`]
253    /// - [`SonameV1::Explicit`], if `version_or_soname` is [`VersionOrSoname::Version`] and
254    ///   `architecture` is [`Some`]
255    ///
256    /// # Examples
257    ///
258    /// ```
259    /// use alpm_types::{ElfArchitectureFormat, SonameV1};
260    ///
261    /// # fn main() -> Result<(), alpm_types::Error> {
262    /// let basic_soname = SonameV1::new("example.so".parse()?, None, None)?;
263    /// assert_eq!(basic_soname, SonameV1::Basic("example.so".parse()?));
264    ///
265    /// let unversioned_soname = SonameV1::new(
266    ///     "example.so".parse()?,
267    ///     Some("example.so".parse()?),
268    ///     Some(ElfArchitectureFormat::Bit64),
269    /// )?;
270    /// assert_eq!(
271    ///     unversioned_soname,
272    ///     SonameV1::Unversioned {
273    ///         name: "example.so".parse()?,
274    ///         soname: "example.so".parse()?,
275    ///         architecture: "64".parse()?
276    ///     }
277    /// );
278    ///
279    /// let explicit_soname = SonameV1::new(
280    ///     "example.so".parse()?,
281    ///     Some("1.0.0".parse()?),
282    ///     Some(ElfArchitectureFormat::Bit64),
283    /// )?;
284    /// assert_eq!(
285    ///     explicit_soname,
286    ///     SonameV1::Explicit {
287    ///         name: "example.so".parse()?,
288    ///         version: "1.0.0".parse()?,
289    ///         architecture: "64".parse()?
290    ///     }
291    /// );
292    /// # Ok(())
293    /// # }
294    /// ```
295    pub fn new(
296        name: SharedObjectName,
297        version_or_soname: Option<VersionOrSoname>,
298        architecture: Option<ElfArchitectureFormat>,
299    ) -> Result<Self, Error> {
300        match (version_or_soname, architecture) {
301            (None, None) => Ok(Self::Basic(name)),
302            (Some(VersionOrSoname::Version(version)), Some(architecture)) => Ok(Self::Explicit {
303                name,
304                version,
305                architecture,
306            }),
307            (Some(VersionOrSoname::Soname(soname)), Some(architecture)) => Ok(Self::Unversioned {
308                name,
309                soname,
310                architecture,
311            }),
312            (None, Some(_)) => Err(Error::InvalidSonameV1(
313                "SonameV1 needs a version when specifying architecture",
314            )),
315            (Some(_), None) => Err(Error::InvalidSonameV1(
316                "SonameV1 needs an architecture when specifying version",
317            )),
318        }
319    }
320
321    /// Parses a [`SonameV1`] from a string slice.
322    pub fn parser(input: &mut &str) -> ModalResult<Self> {
323        // Parse the shared object name.
324        let name = Self::parse_shared_object_name(input)?;
325
326        // Parse the version delimiter `=`.
327        //
328        // If it doesn't exist, it is the basic form.
329        if Self::parse_version_delimiter(input).is_err() {
330            return Ok(SonameV1::Basic(name));
331        }
332
333        // Take all input until we hit the delimiter and architecture.
334        let (raw_version_or_soname, _): (String, _) =
335            cut_err(repeat_till(1.., any, peek(("-", digit1, eof))))
336                .context(StrContext::Expected(StrContextValue::Description(
337                    "a version or shared object name, followed by an ELF architecture format",
338                )))
339                .parse_next(input)?;
340
341        // Two cases are possible here:
342        //
343        // 1. Unversioned: `name=soname-architecture`
344        // 2. Explicit: `name=version-architecture`
345        let version_or_soname =
346            VersionOrSoname::parser.parse_next(&mut raw_version_or_soname.as_str())?;
347
348        // Parse the `-` delimiter
349        Self::parse_architecture_delimiter(input)?;
350
351        // Parse the architecture
352        let architecture = Self::parse_architecture(input)?;
353
354        match version_or_soname {
355            VersionOrSoname::Version(version) => Ok(SonameV1::Explicit {
356                name,
357                version,
358                architecture,
359            }),
360            VersionOrSoname::Soname(soname) => Ok(SonameV1::Unversioned {
361                name,
362                soname,
363                architecture,
364            }),
365        }
366    }
367
368    /// Parses the shared object name until the version delimiter `=`.
369    fn parse_shared_object_name(input: &mut &str) -> ModalResult<SharedObjectName> {
370        repeat_till(1.., any, peek(alt(("=", eof))))
371            .try_map(|(name, _): (String, &str)| SharedObjectName::from_str(&name))
372            .context(StrContext::Label("shared object name"))
373            .parse_next(input)
374    }
375
376    /// Parses the version delimiter `=`.
377    ///
378    /// This function discards the result for only checking if the version delimiter is present.
379    fn parse_version_delimiter(input: &mut &str) -> ModalResult<()> {
380        cut_err("=")
381            .context(StrContext::Label("version delimiter"))
382            .context(StrContext::Expected(StrContextValue::Description(
383                "version delimiter `=`",
384            )))
385            .parse_next(input)
386            .map(|_| ())
387    }
388
389    /// Parses the architecture delimiter `-`.
390    fn parse_architecture_delimiter(input: &mut &str) -> ModalResult<()> {
391        cut_err("-")
392            .context(StrContext::Label("architecture delimiter"))
393            .context(StrContext::Expected(StrContextValue::Description(
394                "architecture delimiter `-`",
395            )))
396            .parse_next(input)
397            .map(|_| ())
398    }
399
400    /// Parses the architecture.
401    fn parse_architecture(input: &mut &str) -> ModalResult<ElfArchitectureFormat> {
402        cut_err(take_while(1.., |c: char| c.is_ascii_digit()))
403            .try_map(ElfArchitectureFormat::from_str)
404            .context(StrContext::Label("architecture"))
405            .parse_next(input)
406    }
407}
408
409impl FromStr for SonameV1 {
410    type Err = Error;
411    /// Parses a [`SonameV1`] from a string slice.
412    ///
413    /// The string slice must be in the format `name[=version-architecture]`.
414    ///
415    /// # Errors
416    ///
417    /// Returns an error if a [`SonameV1`] can not be parsed from input.
418    ///
419    /// # Examples
420    ///
421    /// ```
422    /// use std::str::FromStr;
423    ///
424    /// use alpm_types::{ElfArchitectureFormat, SonameV1};
425    ///
426    /// # fn main() -> Result<(), alpm_types::Error> {
427    /// assert_eq!(
428    ///     SonameV1::from_str("example.so=1.0.0-64")?,
429    ///     SonameV1::Explicit {
430    ///         name: "example.so".parse()?,
431    ///         version: "1.0.0".parse()?,
432    ///         architecture: ElfArchitectureFormat::Bit64,
433    ///     },
434    /// );
435    /// assert_eq!(
436    ///     SonameV1::from_str("example.so=example.so-64")?,
437    ///     SonameV1::Unversioned {
438    ///         name: "example.so".parse()?,
439    ///         soname: "example.so".parse()?,
440    ///         architecture: ElfArchitectureFormat::Bit64,
441    ///     },
442    /// );
443    /// assert_eq!(
444    ///     SonameV1::from_str("example.so")?,
445    ///     SonameV1::Basic("example.so".parse()?),
446    /// );
447    /// # Ok(())
448    /// # }
449    /// ```
450    fn from_str(s: &str) -> Result<Self, Self::Err> {
451        Ok(Self::parser.parse(s)?)
452    }
453}
454
455impl Display for SonameV1 {
456    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
457        match self {
458            Self::Basic(name) => write!(f, "{name}"),
459            Self::Unversioned {
460                name,
461                soname,
462                architecture,
463            } => write!(f, "{name}={soname}-{architecture}"),
464            Self::Explicit {
465                name,
466                version,
467                architecture,
468            } => write!(f, "{name}={version}-{architecture}"),
469        }
470    }
471}
472
473/// A prefix associated with a library lookup directory.
474///
475/// Library lookup directories are used when detecting shared object files on a system.
476/// Each such lookup directory can be assigned to a _prefix_, which allows identifying them in other
477/// contexts. E.g. `lib` may serve as _prefix_ for the lookup directory `/usr/lib`.
478///
479/// This is a type alias for [`Name`].
480pub type SharedLibraryPrefix = Name;
481
482/// The value of a shared object's _soname_.
483///
484/// This data may be present in the _SONAME_ or _NEEDED_ fields of a shared object's _dynamic
485/// section_.
486///
487/// The _soname_ data may contain only a shared object name (e.g. `libexample.so`) or a shared
488/// object name, that also encodes version information (e.g. `libexample.so.1`).
489#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
490pub struct Soname {
491    /// The name part of a shared object's _soname_.
492    pub name: SharedObjectName,
493    /// The optional version part of a shared object's _soname_.
494    pub version: Option<PackageVersion>,
495}
496
497impl Soname {
498    /// Creates a new [`Soname`].
499    pub fn new(name: SharedObjectName, version: Option<PackageVersion>) -> Self {
500        Self { name, version }
501    }
502
503    /// Recognizes a [`Soname`] in a string slice.
504    ///
505    /// The passed data can be in the following formats:
506    ///
507    /// - `<name>.so`: A shared object name without a version. (e.g. `libexample.so`)
508    /// - `<name>.so.<version>`: A shared object name with a version. (e.g. `libexample.so.1`)
509    ///     - The version must be a valid [`PackageVersion`].
510    pub fn parser(input: &mut &str) -> ModalResult<Self> {
511        let name = cut_err(
512            (
513                // Parse the name of the shared object until eof or the `.so` is hit.
514                repeat_till::<_, _, String, _, _, _, _>(1.., any, peek(alt((".so", eof)))),
515                // Parse at least one or more `.so` suffix(es).
516                cut_err(repeat::<_, _, String, _, _>(1.., ".so"))
517                    .context(StrContext::Label("suffix"))
518                    .context(StrContext::Expected(StrContextValue::Description(
519                        "shared object name suffix '.so'",
520                    ))),
521            )
522                // Take both parts and map them onto a SharedObjectName
523                .take()
524                .and_then(Name::parser)
525                .map(SharedObjectName),
526        )
527        .context(StrContext::Label("shared object name"))
528        .parse_next(input)?;
529
530        // Parse the version delimiter.
531        let delimiter = cut_err(alt((".", eof)))
532            .context(StrContext::Label("version delimiter"))
533            .context(StrContext::Expected(StrContextValue::Description(
534                "version delimiter `.`",
535            )))
536            .parse_next(input)?;
537
538        // If a `.` is found, map the rest of the string to a version.
539        // Otherwise, we hit the `eof` and there's no version.
540        let version = match delimiter {
541            "" => None,
542            "." => Some(rest.and_then(PackageVersion::parser).parse_next(input)?),
543            _ => unreachable!(),
544        };
545
546        Ok(Self { name, version })
547    }
548}
549
550impl Display for Soname {
551    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
552        match &self.version {
553            Some(version) => write!(f, "{name}.{version}", name = self.name),
554            None => write!(f, "{name}", name = self.name),
555        }
556    }
557}
558
559impl FromStr for Soname {
560    type Err = Error;
561
562    /// Recognizes a [`Soname`] in a string slice.
563    ///
564    /// The string slice must be in the format of `<name>.so` or `<name>.so.<version>`.
565    ///
566    /// # Errors
567    ///
568    /// Returns an error if a [`Soname`] can not be parsed from input.
569    ///
570    /// # Examples
571    ///
572    /// ```
573    /// use std::str::FromStr;
574    ///
575    /// use alpm_types::Soname;
576    /// # fn main() -> Result<(), alpm_types::Error> {
577    /// assert_eq!(
578    ///     Soname::from_str("libexample.so.1")?,
579    ///     Soname::new("libexample.so".parse()?, Some("1".parse()?)),
580    /// );
581    /// assert_eq!(
582    ///     Soname::from_str("libexample.so")?,
583    ///     Soname::new("libexample.so".parse()?, None),
584    /// );
585    /// # Ok(())
586    /// # }
587    /// ```
588    fn from_str(s: &str) -> Result<Self, Self::Err> {
589        Ok(Self::parser.parse(s)?)
590    }
591}
592
593/// Representation of [soname] data of a shared object based on the [alpm-sonamev2] specification.
594///
595/// Soname data may be used as [alpm-package-relation] of type _provision_ or _run-time dependency_
596/// in [`PackageInfoV1`] and [`PackageInfoV2`]. The data consists of the arbitrarily
597/// defined `prefix`, which denotes the use name of a specific library directory, and the `soname`,
598/// which refers to the value of either the _SONAME_ or a _NEEDED_ field in the _dynamic section_ of
599/// an [ELF] file.
600///
601/// # Examples
602///
603/// This example assumpes that `lib` is used as the `prefix` for the library directory `/usr/lib`
604/// and the following files are contained in it:
605///
606/// ```bash
607/// /usr/lib/libexample.so -> libexample.so.1
608/// /usr/lib/libexample.so.1 -> libexample.so.1.0.0
609/// /usr/lib/libexample.so.1.0.0
610/// ```
611///
612/// The above file `/usr/lib/libexample.so.1.0.0` represents an [ELF] file, that exposes
613/// `libexample.so.1` as value of the _SONAME_ field in its _dynamic section_. This data can be
614/// represented as follows, using [`SonameV2`]:
615///
616/// ```rust
617/// use alpm_types::{Soname, SonameV2};
618///
619/// # fn main() -> Result<(), alpm_types::Error> {
620/// let soname_data = SonameV2 {
621///     prefix: "lib".parse()?,
622///     soname: Soname {
623///         name: "libexample.so".parse()?,
624///         version: Some("1".parse()?),
625///     },
626/// };
627/// assert_eq!(soname_data.to_string(), "lib:libexample.so.1");
628/// # Ok(())
629/// # }
630/// ```
631///
632/// [alpm-sonamev2]: https://alpm.archlinux.page/specifications/alpm-sonamev2.7.html
633/// [alpm-package-relation]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
634/// [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
635/// [soname]: https://en.wikipedia.org/wiki/Soname
636/// [`PackageInfoV1`]: https://docs.rs/alpm_pkginfo/latest/alpm_pkginfo/struct.PackageInfoV1.html
637/// [`PackageInfoV2`]: https://docs.rs/alpm_pkginfo/latest/alpm_pkginfo/struct.PackageInfoV2.html
638#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
639pub struct SonameV2 {
640    /// The directory prefix of the shared object file.
641    pub prefix: SharedLibraryPrefix,
642    /// The _soname_ of a shared object file.
643    pub soname: Soname,
644}
645
646impl SonameV2 {
647    /// Creates a new [`SonameV2`].
648    ///
649    /// # Examples
650    ///
651    /// ```
652    /// use alpm_types::SonameV2;
653    ///
654    /// # fn main() -> Result<(), alpm_types::Error> {
655    /// SonameV2::new("lib".parse()?, "libexample.so.1".parse()?);
656    /// # Ok(())
657    /// # }
658    /// ```
659    pub fn new(prefix: SharedLibraryPrefix, soname: Soname) -> Self {
660        Self { prefix, soname }
661    }
662
663    /// Recognizes a [`SonameV2`] in a string slice.
664    ///
665    /// The passed data must be in the format `<prefix>:<soname>`. (e.g. `lib:libexample.so.1`)
666    ///
667    /// See [`Soname::parser`] for details on the format of `<soname>`.
668    ///
669    /// # Errors
670    ///
671    /// Returns an error if no [`SonameV2`] can be created from `input`.
672    pub fn parser(input: &mut &str) -> ModalResult<Self> {
673        // Parse everything from the start to the first `:` and parse as `SharedLibraryPrefix`.
674        let prefix = cut_err(
675            repeat_till(1.., any, peek(alt((":", eof))))
676                .try_map(|(name, _): (String, &str)| SharedLibraryPrefix::from_str(&name)),
677        )
678        .context(StrContext::Label("prefix for a shared object lookup path"))
679        .parse_next(input)?;
680
681        cut_err(":")
682            .context(StrContext::Label("shared library prefix delimiter"))
683            .context(StrContext::Expected(StrContextValue::Description(
684                "shared library prefix `:`",
685            )))
686            .parse_next(input)?;
687
688        let soname = Soname::parser.parse_next(input)?;
689
690        Ok(Self { prefix, soname })
691    }
692}
693
694impl FromStr for SonameV2 {
695    type Err = Error;
696
697    /// Parses a [`SonameV2`] from a string slice.
698    ///
699    /// The string slice must be in the format `<prefix>:<soname>`.
700    ///
701    /// # Errors
702    ///
703    /// Returns an error if a [`SonameV2`] can not be parsed from input.
704    ///
705    /// # Examples
706    ///
707    /// ```
708    /// use std::str::FromStr;
709    ///
710    /// use alpm_types::{Soname, SonameV2};
711    ///
712    /// # fn main() -> Result<(), alpm_types::Error> {
713    /// assert_eq!(
714    ///     SonameV2::from_str("lib:libexample.so.1")?,
715    ///     SonameV2::new(
716    ///         "lib".parse()?,
717    ///         Soname::new("libexample.so".parse()?, Some("1".parse()?))
718    ///     ),
719    /// );
720    /// # Ok(())
721    /// # }
722    /// ```
723    fn from_str(s: &str) -> Result<Self, Self::Err> {
724        Ok(Self::parser.parse(s)?)
725    }
726}
727
728impl Display for SonameV2 {
729    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
730        write!(
731            f,
732            "{prefix}:{soname}",
733            prefix = self.prefix,
734            soname = self.soname
735        )
736    }
737}
738
739/// A package relation
740///
741/// Describes a relation to a component.
742/// Package relations may either consist of only a [`Name`] *or* of a [`Name`] and a
743/// [`VersionRequirement`].
744///
745/// ## Note
746///
747/// A [`PackageRelation`] covers all [alpm-package-relations] *except* optional
748/// dependencies, as those behave differently.
749///
750/// [alpm-package-relations]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
751#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
752pub struct PackageRelation {
753    /// The name of the package
754    pub name: Name,
755    /// The version requirement for the package
756    pub version_requirement: Option<VersionRequirement>,
757}
758
759impl PackageRelation {
760    /// Creates a new [`PackageRelation`]
761    ///
762    /// # Examples
763    ///
764    /// ```
765    /// use alpm_types::{PackageRelation, VersionComparison, VersionRequirement};
766    ///
767    /// # fn main() -> Result<(), alpm_types::Error> {
768    /// PackageRelation::new(
769    ///     "example".parse()?,
770    ///     Some(VersionRequirement {
771    ///         comparison: VersionComparison::Less,
772    ///         version: "1.0.0".parse()?,
773    ///     }),
774    /// );
775    ///
776    /// PackageRelation::new("example".parse()?, None);
777    /// # Ok(())
778    /// # }
779    /// ```
780    pub fn new(name: Name, version_requirement: Option<VersionRequirement>) -> Self {
781        Self {
782            name,
783            version_requirement,
784        }
785    }
786
787    /// Parses a [`PackageRelation`] from a string slice.
788    ///
789    /// Consumes all of its input.
790    ///
791    /// # Examples
792    ///
793    /// See [`Self::from_str`] for code examples.
794    ///
795    /// # Errors
796    ///
797    /// Returns an error if `input` is not a valid _package-relation_.
798    pub fn parser(input: &mut &str) -> ModalResult<Self> {
799        seq!(Self {
800            name: take_till(1.., ('<', '>', '=')).and_then(Name::parser).context(StrContext::Label("package name")),
801            version_requirement: opt(VersionRequirement::parser),
802            _: eof.context(StrContext::Expected(StrContextValue::Description("end of relation version requirement"))),
803        })
804        .parse_next(input)
805    }
806}
807
808impl Display for PackageRelation {
809    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
810        if let Some(version_requirement) = self.version_requirement.as_ref() {
811            write!(f, "{}{}", self.name, version_requirement)
812        } else {
813            write!(f, "{}", self.name)
814        }
815    }
816}
817
818impl FromStr for PackageRelation {
819    type Err = Error;
820    /// Parses a [`PackageRelation`] from a string slice.
821    ///
822    /// Delegates to [`PackageRelation::parser`].
823    ///
824    /// # Errors
825    ///
826    /// Returns an error if [`PackageRelation::parser`] fails.
827    ///
828    /// # Examples
829    ///
830    /// ```
831    /// use std::str::FromStr;
832    ///
833    /// use alpm_types::{PackageRelation, VersionComparison, VersionRequirement};
834    ///
835    /// # fn main() -> Result<(), alpm_types::Error> {
836    /// assert_eq!(
837    ///     PackageRelation::from_str("example<1.0.0")?,
838    ///     PackageRelation::new(
839    ///         "example".parse()?,
840    ///         Some(VersionRequirement {
841    ///             comparison: VersionComparison::Less,
842    ///             version: "1.0.0".parse()?
843    ///         })
844    ///     ),
845    /// );
846    ///
847    /// assert_eq!(
848    ///     PackageRelation::from_str("example<=1.0.0")?,
849    ///     PackageRelation::new(
850    ///         "example".parse()?,
851    ///         Some(VersionRequirement {
852    ///             comparison: VersionComparison::LessOrEqual,
853    ///             version: "1.0.0".parse()?
854    ///         })
855    ///     ),
856    /// );
857    ///
858    /// assert_eq!(
859    ///     PackageRelation::from_str("example=1.0.0")?,
860    ///     PackageRelation::new(
861    ///         "example".parse()?,
862    ///         Some(VersionRequirement {
863    ///             comparison: VersionComparison::Equal,
864    ///             version: "1.0.0".parse()?
865    ///         })
866    ///     ),
867    /// );
868    ///
869    /// assert_eq!(
870    ///     PackageRelation::from_str("example>1.0.0")?,
871    ///     PackageRelation::new(
872    ///         "example".parse()?,
873    ///         Some(VersionRequirement {
874    ///             comparison: VersionComparison::Greater,
875    ///             version: "1.0.0".parse()?
876    ///         })
877    ///     ),
878    /// );
879    ///
880    /// assert_eq!(
881    ///     PackageRelation::from_str("example>=1.0.0")?,
882    ///     PackageRelation::new(
883    ///         "example".parse()?,
884    ///         Some(VersionRequirement {
885    ///             comparison: VersionComparison::GreaterOrEqual,
886    ///             version: "1.0.0".parse()?
887    ///         })
888    ///     ),
889    /// );
890    ///
891    /// assert_eq!(
892    ///     PackageRelation::from_str("example")?,
893    ///     PackageRelation::new("example".parse()?, None),
894    /// );
895    ///
896    /// assert!(PackageRelation::from_str("example<").is_err());
897    /// # Ok(())
898    /// # }
899    /// ```
900    fn from_str(s: &str) -> Result<Self, Self::Err> {
901        Ok(Self::parser.parse(s)?)
902    }
903}
904
905/// An optional dependency for a package.
906///
907/// This type is used for representing dependencies that are not essential for base functionality
908/// of a package, but may be necessary to make use of certain features of a package.
909///
910/// An [`OptionalDependency`] consists of a package relation and an optional description separated
911/// by a colon (`:`).
912///
913/// - The package relation component must be a valid [`PackageRelation`].
914/// - If a description is provided it must be at least one character long.
915///
916/// Refer to [alpm-package-relation] of type [optional dependency] for details on the format.
917/// ## Examples
918///
919/// ```
920/// use std::str::FromStr;
921///
922/// use alpm_types::{Name, OptionalDependency};
923///
924/// # fn main() -> Result<(), alpm_types::Error> {
925/// // Create OptionalDependency from &str
926/// let opt_depend = OptionalDependency::from_str("example: this is an example dependency")?;
927///
928/// // Get the name
929/// assert_eq!("example", opt_depend.name().as_ref());
930///
931/// // Get the description
932/// assert_eq!(
933///     Some("this is an example dependency"),
934///     opt_depend.description().as_deref()
935/// );
936///
937/// // Format as String
938/// assert_eq!(
939///     "example: this is an example dependency",
940///     format!("{opt_depend}")
941/// );
942/// # Ok(())
943/// # }
944/// ```
945///
946/// [alpm-package-relation]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
947/// [optional dependency]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html#optional-dependency
948#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
949pub struct OptionalDependency {
950    package_relation: PackageRelation,
951    description: Option<String>,
952}
953
954impl OptionalDependency {
955    /// Create a new OptionalDependency in a Result
956    pub fn new(
957        package_relation: PackageRelation,
958        description: Option<String>,
959    ) -> OptionalDependency {
960        OptionalDependency {
961            package_relation,
962            description,
963        }
964    }
965
966    /// Return the name of the optional dependency
967    pub fn name(&self) -> &Name {
968        &self.package_relation.name
969    }
970
971    /// Return the version requirement of the optional dependency
972    pub fn version_requirement(&self) -> &Option<VersionRequirement> {
973        &self.package_relation.version_requirement
974    }
975
976    /// Return the description for the optional dependency, if it exists
977    pub fn description(&self) -> &Option<String> {
978        &self.description
979    }
980
981    /// Recognizes an [`OptionalDependency`] in a string slice.
982    ///
983    /// Consumes all of its input.
984    ///
985    /// # Errors
986    ///
987    /// Returns an error if `input` is not a valid _alpm-package-relation_ of type _optional
988    /// dependency_.
989    pub fn parser(input: &mut &str) -> ModalResult<Self> {
990        let description_parser = terminated(
991            // Descriptions may consist of any character except '\n' and '\r'.
992            // Descriptions are a also at the end of a `OptionalDependency`.
993            // We enforce forbidding `\n` and `\r` by only taking until either of them
994            // is hit and checking for `eof` afterwards.
995            // This will **always** succeed unless `\n` and `\r` are hit, in which case an
996            // error is thrown.
997            take_till(0.., ('\n', '\r')),
998            eof,
999        )
1000        .context(StrContext::Label("optional dependency description"))
1001        .context(StrContext::Expected(StrContextValue::Description(
1002            r"no carriage returns or newlines",
1003        )))
1004        .map(|d: &str| match d.trim_ascii() {
1005            "" => None,
1006            t => Some(t.to_string()),
1007        });
1008
1009        let (package_relation, description) = alt((
1010            // look for a ":" followed by at least one whitespace, then dispatch either side to the
1011            // relevant parser without allowing backtracking.
1012            separated_pair(
1013                take_until(1.., ":").and_then(cut_err(PackageRelation::parser)),
1014                (":", space1),
1015                rest.and_then(cut_err(description_parser)),
1016            ),
1017            // if we can't find ": ", then assume it's all PackageRelation
1018            // and assert we've reached the end of input
1019            (rest.and_then(PackageRelation::parser), eof.value(None)),
1020        ))
1021        .parse_next(input)?;
1022
1023        Ok(Self {
1024            package_relation,
1025            description,
1026        })
1027    }
1028}
1029
1030impl FromStr for OptionalDependency {
1031    type Err = Error;
1032
1033    /// Creates a new [`OptionalDependency`] from a string slice.
1034    ///
1035    /// Delegates to [`OptionalDependency::parser`].
1036    ///
1037    /// # Errors
1038    ///
1039    /// Returns an error if [`OptionalDependency::parser`] fails.
1040    fn from_str(s: &str) -> Result<Self, Self::Err> {
1041        Ok(Self::parser.parse(s)?)
1042    }
1043}
1044
1045impl Display for OptionalDependency {
1046    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
1047        match self.description {
1048            Some(ref description) => write!(fmt, "{}: {}", self.package_relation, description),
1049            None => write!(fmt, "{}", self.package_relation),
1050        }
1051    }
1052}
1053
1054/// Group of a package
1055///
1056/// Represents an arbitrary collection of packages that share a common
1057/// characteristic or functionality.
1058///
1059/// While group names can be any valid UTF-8 string, it is recommended to follow
1060/// the format of [`Name`] (`[a-z\d\-._@+]` but must not start with `[-.]`)
1061/// to ensure consistency and ease of use.
1062///
1063/// This is a type alias for [`String`].
1064///
1065/// ## Examples
1066/// ```
1067/// use alpm_types::Group;
1068///
1069/// // Create a Group
1070/// let group: Group = "package-group".to_string();
1071/// ```
1072pub type Group = String;
1073
1074/// Provides either a [`PackageRelation`], a [`SonameV1`] or a [`SonameV2`].
1075///
1076/// This enum is used for [alpm-package-relations] of type _run-time dependency_ and _provision_
1077/// e.g. in [PKGINFO], [SRCINFO] or [alpm-db-desc] files.
1078///
1079/// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
1080/// [SRCINFO]: https://alpm.archlinux.page/specifications/SRCINFO.5.html
1081/// [alpm-db-desc]: https://alpm.archlinux.page/specifications/alpm-db-desc.5.html
1082/// [alpm-package-relations]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
1083#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1084#[serde(untagged)]
1085pub enum RelationOrSoname {
1086    /// A package relation (as [`PackageRelation`]).
1087    Relation(PackageRelation),
1088    /// A shared object name following [alpm-sonamev1].
1089    ///
1090    /// [alpm-sonamev1]: https://alpm.archlinux.page/specifications/alpm-sonamev1.7.html
1091    SonameV1(SonameV1),
1092    /// A shared object name following [alpm-sonamev2].
1093    ///
1094    /// [alpm-sonamev2]: https://alpm.archlinux.page/specifications/alpm-sonamev2.7.html
1095    SonameV2(SonameV2),
1096}
1097
1098impl PartialEq<PackageRelation> for RelationOrSoname {
1099    fn eq(&self, other: &PackageRelation) -> bool {
1100        self.to_string() == other.to_string()
1101    }
1102}
1103
1104impl PartialEq<SonameV1> for RelationOrSoname {
1105    fn eq(&self, other: &SonameV1) -> bool {
1106        self.to_string() == other.to_string()
1107    }
1108}
1109
1110impl PartialEq<SonameV2> for RelationOrSoname {
1111    fn eq(&self, other: &SonameV2) -> bool {
1112        self.to_string() == other.to_string()
1113    }
1114}
1115
1116impl RelationOrSoname {
1117    /// Recognizes a [`SonameV2`], a [`SonameV1`] or a [`PackageRelation`] in a string slice.
1118    ///
1119    /// First attempts to recognize a [`SonameV2`], then a [`SonameV1`] and if that fails, falls
1120    /// back to recognizing a [`PackageRelation`].
1121    /// Depending on recognized type, a [`RelationOrSoname`] is created accordingly.
1122    pub fn parser(input: &mut &str) -> ModalResult<Self> {
1123        // Implement a custom `winnow::combinator::alt`, as all type parsers are built in
1124        // such a way that they return errors on unexpected input instead of backtracking.
1125        let checkpoint = input.checkpoint();
1126        let sonamev2_result = SonameV2::parser.parse_next(input);
1127        if sonamev2_result.is_ok() {
1128            let sonamev2 = sonamev2_result?;
1129            return Ok(RelationOrSoname::SonameV2(sonamev2));
1130        }
1131
1132        input.reset(&checkpoint);
1133        let sonamev1_result = SonameV1::parser.parse_next(input);
1134        if sonamev1_result.is_ok() {
1135            let sonamev1 = sonamev1_result?;
1136            return Ok(RelationOrSoname::SonameV1(sonamev1));
1137        }
1138
1139        input.reset(&checkpoint);
1140        let relation_result = rest.and_then(PackageRelation::parser).parse_next(input);
1141        if relation_result.is_ok() {
1142            let relation = relation_result?;
1143            return Ok(RelationOrSoname::Relation(relation));
1144        }
1145
1146        cut_err(fail)
1147            .context(StrContext::Expected(StrContextValue::Description(
1148                "alpm-sonamev2, alpm-sonamev1 or alpm-package-relation",
1149            )))
1150            .parse_next(input)
1151    }
1152}
1153
1154impl Display for RelationOrSoname {
1155    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1156        match self {
1157            RelationOrSoname::Relation(version) => write!(f, "{version}"),
1158            RelationOrSoname::SonameV1(soname) => write!(f, "{soname}"),
1159            RelationOrSoname::SonameV2(soname) => write!(f, "{soname}"),
1160        }
1161    }
1162}
1163
1164impl FromStr for RelationOrSoname {
1165    type Err = Error;
1166
1167    /// Creates a [`RelationOrSoname`] from a string slice.
1168    ///
1169    /// Relies on [`RelationOrSoname::parser`] to recognize types in `input` and create a
1170    /// [`RelationOrSoname`] accordingly.
1171    ///
1172    /// # Errors
1173    ///
1174    /// Returns an error if no [`RelationOrSoname`] can be created from `input`.
1175    ///
1176    /// # Examples
1177    ///
1178    /// ```
1179    /// use alpm_types::{PackageRelation, RelationOrSoname, SonameV1, SonameV2};
1180    ///
1181    /// # fn main() -> Result<(), alpm_types::Error> {
1182    /// let relation: RelationOrSoname = "example=1.0.0".parse()?;
1183    /// assert_eq!(
1184    ///     relation,
1185    ///     RelationOrSoname::Relation(PackageRelation::new(
1186    ///         "example".parse()?,
1187    ///         Some("=1.0.0".parse()?)
1188    ///     ))
1189    /// );
1190    ///
1191    /// let sonamev2: RelationOrSoname = "lib:example.so.1".parse()?;
1192    /// assert_eq!(
1193    ///     sonamev2,
1194    ///     RelationOrSoname::SonameV2(SonameV2::new("lib".parse()?, "example.so.1".parse()?))
1195    /// );
1196    ///
1197    /// let sonamev1: RelationOrSoname = "example.so".parse()?;
1198    /// assert_eq!(
1199    ///     sonamev1,
1200    ///     RelationOrSoname::SonameV1(SonameV1::new("example.so".parse()?, None, None)?)
1201    /// );
1202    /// # Ok(())
1203    /// # }
1204    /// ```
1205    fn from_str(s: &str) -> Result<Self, Self::Err> {
1206        Self::parser
1207            .parse(s)
1208            .map_err(|error| Error::ParseError(error.to_string()))
1209    }
1210}
1211
1212#[cfg(test)]
1213mod tests {
1214    use proptest::{prop_assert_eq, proptest, test_runner::Config as ProptestConfig};
1215    use rstest::rstest;
1216    use testresult::TestResult;
1217
1218    use super::*;
1219    use crate::VersionComparison;
1220
1221    const COMPARATOR_REGEX: &str = r"(<|<=|=|>=|>)";
1222    /// NOTE: [`Epoch`][alpm_types::Epoch] is implicitly constrained by [`std::usize::MAX`].
1223    /// However, it's unrealistic to ever reach that many forced downgrades for a package, hence
1224    /// we don't test that fully
1225    const EPOCH_REGEX: &str = r"[1-9]{1}[0-9]{0,10}";
1226    const NAME_REGEX: &str = r"[a-z0-9_@+]+[a-z0-9\-._@+]*";
1227    const PKGREL_REGEX: &str = r"[1-9][0-9]{0,8}(|[.][1-9][0-9]{0,8})";
1228    const PKGVER_REGEX: &str = r"([[:alnum:]][[:alnum:]_+.]*)";
1229    const DESCRIPTION_REGEX: &str = "[^\n\r]*";
1230
1231    proptest! {
1232        #![proptest_config(ProptestConfig::with_cases(1000))]
1233
1234
1235        #[test]
1236        fn valid_package_relation_from_str(s in format!("{NAME_REGEX}(|{COMPARATOR_REGEX}(|{EPOCH_REGEX}:){PKGVER_REGEX}(|-{PKGREL_REGEX}))").as_str()) {
1237            println!("s: {s}");
1238            let name = PackageRelation::from_str(&s).unwrap();
1239            prop_assert_eq!(s, format!("{}", name));
1240        }
1241    }
1242
1243    proptest! {
1244        #[test]
1245        fn opt_depend_from_str(
1246            name in NAME_REGEX,
1247            desc in DESCRIPTION_REGEX,
1248            use_desc in proptest::bool::ANY
1249        ) {
1250            let desc_trimmed = desc.trim_ascii();
1251            let desc_is_blank = desc_trimmed.is_empty();
1252
1253            let (raw_in, formatted_expected) = if use_desc {
1254                // Raw input and expected formatted output.
1255                // These are different because `desc` will be trimmed by the parser;
1256                // if it is *only* ascii whitespace then it will be skipped altogether.
1257                (
1258                    format!("{name}: {desc}"),
1259                    if !desc_is_blank {
1260                        format!("{name}: {desc_trimmed}")
1261                    } else {
1262                        name.clone()
1263                    }
1264                )
1265            } else {
1266                (name.clone(), name.clone())
1267            };
1268
1269            println!("input string: {raw_in}");
1270            let opt_depend = OptionalDependency::from_str(&raw_in).unwrap();
1271            let formatted_actual = format!("{opt_depend}");
1272            prop_assert_eq!(
1273                formatted_expected,
1274                formatted_actual,
1275                "Formatted output doesn't match input"
1276            );
1277        }
1278    }
1279
1280    #[rstest]
1281    #[case(
1282        "python>=3",
1283        Ok(PackageRelation {
1284            name: Name::new("python").unwrap(),
1285            version_requirement: Some(VersionRequirement {
1286                comparison: VersionComparison::GreaterOrEqual,
1287                version: "3".parse().unwrap(),
1288            }),
1289        }),
1290    )]
1291    #[case(
1292        "java-environment>=17",
1293        Ok(PackageRelation {
1294            name: Name::new("java-environment").unwrap(),
1295            version_requirement: Some(VersionRequirement {
1296                comparison: VersionComparison::GreaterOrEqual,
1297                version: "17".parse().unwrap(),
1298            }),
1299        }),
1300    )]
1301    fn valid_package_relation(
1302        #[case] input: &str,
1303        #[case] expected: Result<PackageRelation, Error>,
1304    ) {
1305        assert_eq!(PackageRelation::from_str(input), expected);
1306    }
1307
1308    #[rstest]
1309    #[case(
1310        "example: this is an example dependency",
1311        OptionalDependency {
1312            package_relation: PackageRelation {
1313                name: Name::new("example").unwrap(),
1314                version_requirement: None,
1315            },
1316            description: Some("this is an example dependency".to_string()),
1317        },
1318    )]
1319    #[case(
1320        "example-two:     a description with lots of whitespace padding     ",
1321        OptionalDependency {
1322            package_relation: PackageRelation {
1323                name: Name::new("example-two").unwrap(),
1324                version_requirement: None,
1325            },
1326            description: Some("a description with lots of whitespace padding".to_string())
1327        },
1328    )]
1329    #[case(
1330        "dep_name",
1331        OptionalDependency {
1332            package_relation: PackageRelation {
1333                name: Name::new("dep_name").unwrap(),
1334                version_requirement: None,
1335            },
1336            description: None,
1337        },
1338    )]
1339    #[case(
1340        "dep_name: ",
1341        OptionalDependency {
1342            package_relation: PackageRelation {
1343                name: Name::new("dep_name").unwrap(),
1344                version_requirement: None,
1345            },
1346            description: None,
1347        },
1348    )]
1349    #[case(
1350        "dep_name_with_special_chars-123: description with !@#$%^&*",
1351        OptionalDependency {
1352            package_relation: PackageRelation {
1353                name: Name::new("dep_name_with_special_chars-123").unwrap(),
1354                version_requirement: None,
1355            },
1356            description: Some("description with !@#$%^&*".to_string()),
1357        },
1358    )]
1359    // versioned optional dependencies
1360    #[case(
1361        "elfutils=0.192: for translations",
1362        OptionalDependency {
1363            package_relation: PackageRelation {
1364                name: Name::new("elfutils").unwrap(),
1365                version_requirement: Some(VersionRequirement {
1366                    comparison: VersionComparison::Equal,
1367                    version: "0.192".parse().unwrap(),
1368                }),
1369            },
1370            description: Some("for translations".to_string()),
1371        },
1372    )]
1373    #[case(
1374        "python>=3: For Python bindings",
1375        OptionalDependency {
1376            package_relation: PackageRelation {
1377                name: Name::new("python").unwrap(),
1378                version_requirement: Some(VersionRequirement {
1379                    comparison: VersionComparison::GreaterOrEqual,
1380                    version: "3".parse().unwrap(),
1381                }),
1382            },
1383            description: Some("For Python bindings".to_string()),
1384        },
1385    )]
1386    #[case(
1387        "java-environment>=17: required by extension-wiki-publisher and extension-nlpsolver",
1388        OptionalDependency {
1389            package_relation: PackageRelation {
1390                name: Name::new("java-environment").unwrap(),
1391                version_requirement: Some(VersionRequirement {
1392                    comparison: VersionComparison::GreaterOrEqual,
1393                    version: "17".parse().unwrap(),
1394                }),
1395            },
1396            description: Some("required by extension-wiki-publisher and extension-nlpsolver".to_string()),
1397        },
1398    )]
1399    fn opt_depend_from_string(#[case] input: &str, #[case] expected: OptionalDependency) {
1400        let opt_depend_result = OptionalDependency::from_str(input);
1401        let Ok(optional_dependency) = opt_depend_result else {
1402            panic!(
1403                "Encountered unexpected error when parsing optional dependency: {opt_depend_result:?}"
1404            )
1405        };
1406
1407        assert_eq!(
1408            expected, optional_dependency,
1409            "Optional dependency has not been correctly parsed."
1410        );
1411    }
1412
1413    #[rstest]
1414    #[case(
1415        "example: this is an example dependency",
1416        "example: this is an example dependency"
1417    )]
1418    #[case(
1419        "example-two:     a description with lots of whitespace padding     ",
1420        "example-two: a description with lots of whitespace padding"
1421    )]
1422    #[case(
1423        "tabs:    a description with a tab directly after the colon",
1424        "tabs: a description with a tab directly after the colon"
1425    )]
1426    #[case("dep_name", "dep_name")]
1427    #[case("dep_name: ", "dep_name")]
1428    #[case(
1429        "dep_name_with_special_chars-123: description with !@#$%^&*",
1430        "dep_name_with_special_chars-123: description with !@#$%^&*"
1431    )]
1432    // versioned optional dependencies
1433    #[case("elfutils=0.192: for translations", "elfutils=0.192: for translations")]
1434    #[case("python>=3: For Python bindings", "python>=3: For Python bindings")]
1435    #[case(
1436        "java-environment>=17: required by extension-wiki-publisher and extension-nlpsolver",
1437        "java-environment>=17: required by extension-wiki-publisher and extension-nlpsolver"
1438    )]
1439    fn opt_depend_to_string(#[case] input: &str, #[case] expected: &str) {
1440        let opt_depend_result = OptionalDependency::from_str(input);
1441        let Ok(optional_dependency) = opt_depend_result else {
1442            panic!(
1443                "Encountered unexpected error when parsing optional dependency: {opt_depend_result:?}"
1444            )
1445        };
1446        assert_eq!(
1447            expected,
1448            optional_dependency.to_string(),
1449            "OptionalDependency to_string is erroneous."
1450        );
1451    }
1452
1453    #[rstest]
1454    #[case(
1455        "#invalid-name: this is an example dependency",
1456        "invalid first character of package name"
1457    )]
1458    #[case(": no_name_colon", "invalid first character of package name")]
1459    #[case(
1460        "name:description with no leading whitespace",
1461        "invalid character in package name"
1462    )]
1463    #[case(
1464        "dep-name>=10: \n\ndescription with\rnewlines",
1465        "expected no carriage returns or newlines"
1466    )]
1467    fn opt_depend_invalid_string_parse_error(#[case] input: &str, #[case] err_snippet: &str) {
1468        let Err(Error::ParseError(err_msg)) = OptionalDependency::from_str(input) else {
1469            panic!("'{input}' did not fail to parse as expected")
1470        };
1471        assert!(
1472            err_msg.contains(err_snippet),
1473            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1474        );
1475    }
1476
1477    #[rstest]
1478    #[case("example.so", SonameV1::Basic("example.so".parse().unwrap()))]
1479    #[case("example.so=1.0.0-64", SonameV1::Explicit {
1480        name: "example.so".parse().unwrap(),
1481        version: "1.0.0".parse().unwrap(),
1482        architecture: ElfArchitectureFormat::Bit64,
1483    })]
1484    fn sonamev1_from_string(
1485        #[case] input: &str,
1486        #[case] expected_result: SonameV1,
1487    ) -> testresult::TestResult<()> {
1488        let soname = SonameV1::from_str(input)?;
1489        assert_eq!(expected_result, soname);
1490        assert_eq!(input, soname.to_string());
1491        Ok(())
1492    }
1493
1494    #[rstest]
1495    #[case(
1496        "libwlroots-0.18.so=libwlroots-0.18.so-64",
1497        SonameV1::Unversioned {
1498            name: "libwlroots-0.18.so".parse().unwrap(),
1499            soname: "libwlroots-0.18.so".parse().unwrap(),
1500            architecture: ElfArchitectureFormat::Bit64,
1501        },
1502    )]
1503    #[case(
1504        "libexample.so=otherlibexample.so-64",
1505        SonameV1::Unversioned {
1506            name: "libexample.so".parse().unwrap(),
1507            soname: "otherlibexample.so".parse().unwrap(),
1508            architecture: ElfArchitectureFormat::Bit64,
1509        },
1510    )]
1511    fn sonamev1_from_string_without_version(
1512        #[case] input: &str,
1513        #[case] expected_result: SonameV1,
1514    ) -> testresult::TestResult<()> {
1515        let soname = SonameV1::from_str(input)?;
1516        assert_eq!(expected_result, soname);
1517        assert_eq!(input, soname.to_string());
1518        Ok(())
1519    }
1520
1521    #[rstest]
1522    #[case("noso", "invalid shared object name")]
1523    #[case("invalidversion.so=1*2-64", "expected version or shared object name")]
1524    #[case(
1525        "nodelimiter.so=1.64",
1526        "expected a version or shared object name, followed by an ELF architecture format"
1527    )]
1528    #[case(
1529        "noarchitecture.so=1-",
1530        "expected a version or shared object name, followed by an ELF architecture format"
1531    )]
1532    #[case("invalidarchitecture.so=1-82", "invalid architecture")]
1533    #[case("invalidsoname.so~1.64", "unexpected trailing content")]
1534    fn invalid_sonamev1_parser(#[case] input: &str, #[case] error_snippet: &str) {
1535        let result = SonameV1::from_str(input);
1536        assert!(result.is_err(), "Expected SonameV1 parsing to fail");
1537        let err = result.unwrap_err();
1538        let pretty_error = err.to_string();
1539        assert!(
1540            pretty_error.contains(error_snippet),
1541            "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
1542        );
1543    }
1544
1545    #[rstest]
1546    #[case(
1547        "otherlibexample.so",
1548        VersionOrSoname::Soname(
1549            SharedObjectName::new("otherlibexample.so").unwrap())
1550    )]
1551    #[case(
1552        "1.0.0",
1553        VersionOrSoname::Version(
1554            PackageVersion::from_str("1.0.0").unwrap())
1555    )]
1556    fn version_or_soname_from_string(
1557        #[case] input: &str,
1558        #[case] expected_result: VersionOrSoname,
1559    ) -> testresult::TestResult<()> {
1560        let version = VersionOrSoname::from_str(input)?;
1561        assert_eq!(expected_result, version);
1562        assert_eq!(input, version.to_string());
1563        Ok(())
1564    }
1565
1566    #[rstest]
1567    #[case(
1568        "lib:libexample.so",
1569        SonameV2 {
1570            prefix: "lib".parse().unwrap(),
1571            soname: Soname {
1572                name: "libexample.so".parse().unwrap(),
1573                version: None,
1574            },
1575        },
1576    )]
1577    #[case(
1578        "usr:libexample.so.1",
1579        SonameV2 {
1580            prefix: "usr".parse().unwrap(),
1581            soname: Soname {
1582                name: "libexample.so".parse().unwrap(),
1583                version: "1".parse().ok(),
1584            },
1585        },
1586    )]
1587    #[case(
1588        "lib:libexample.so.1.2.3",
1589        SonameV2 {
1590            prefix: "lib".parse().unwrap(),
1591            soname: Soname {
1592                name: "libexample.so".parse().unwrap(),
1593                version: "1.2.3".parse().ok(),
1594            },
1595        },
1596    )]
1597    #[case(
1598        "lib:libexample.so.so.420",
1599        SonameV2 {
1600            prefix: "lib".parse().unwrap(),
1601            soname: Soname {
1602                name: "libexample.so.so".parse().unwrap(),
1603                version: "420".parse().ok(),
1604            },
1605        },
1606    )]
1607    #[case(
1608        "lib:libexample.so.test",
1609        SonameV2 {
1610            prefix: "lib".parse().unwrap(),
1611            soname: Soname {
1612                name: "libexample.so".parse().unwrap(),
1613                version: "test".parse().ok(),
1614            },
1615        },
1616    )]
1617    fn sonamev2_from_string(
1618        #[case] input: &str,
1619        #[case] expected_result: SonameV2,
1620    ) -> testresult::TestResult<()> {
1621        let soname = SonameV2::from_str(input)?;
1622        assert_eq!(expected_result, soname);
1623        assert_eq!(input, soname.to_string());
1624        Ok(())
1625    }
1626
1627    #[rstest]
1628    #[case("libexample.so.1", "invalid shared library prefix delimiter")]
1629    #[case("lib:libexample.so-abc", "invalid version delimiter")]
1630    #[case("lib:libexample.so.10-10", "invalid pkgver character")]
1631    #[case("lib:libexample.so.1.0.0-64", "invalid pkgver character")]
1632    fn invalid_sonamev2_parser(#[case] input: &str, #[case] error_snippet: &str) {
1633        let result = SonameV2::from_str(input);
1634        assert!(result.is_err(), "Expected SonameV2 parsing to fail");
1635        let err = result.unwrap_err();
1636        let pretty_error = err.to_string();
1637        assert!(
1638            pretty_error.contains(error_snippet),
1639            "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
1640        );
1641    }
1642
1643    #[rstest]
1644    #[case(
1645        "example",
1646        RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), None))
1647    )]
1648    #[case(
1649        "example=1.0.0",
1650        RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), "=1.0.0".parse().ok())) 
1651    )]
1652    #[case(
1653        "example>=1.0.0",
1654        RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), ">=1.0.0".parse().ok()))
1655    )]
1656    #[case(
1657        "lib:example.so.1",
1658        RelationOrSoname::SonameV2(
1659            SonameV2::new(
1660                "lib".parse().unwrap(),
1661                Soname::from_str("example.so.1").unwrap(),
1662            )
1663        )
1664    )]
1665    #[case(
1666        "lib:example.so",
1667        RelationOrSoname::SonameV2(
1668            SonameV2::new(
1669                "lib".parse().unwrap(),
1670                Soname::from_str("example.so").unwrap(),
1671            )
1672        )
1673    )]
1674    #[case(
1675        "example.so",
1676        RelationOrSoname::SonameV1(
1677            SonameV1::new(
1678                "example.so".parse().unwrap(),
1679                None,
1680                None,
1681            ).unwrap()
1682        )
1683    )]
1684    #[case(
1685        "example.so=1.0.0-64",
1686        RelationOrSoname::SonameV1(
1687            SonameV1::new(
1688                "example.so".parse().unwrap(),
1689                Some(VersionOrSoname::Version("1.0.0".parse().unwrap())),
1690                Some(ElfArchitectureFormat::Bit64),
1691            ).unwrap()
1692        )
1693    )]
1694    #[case(
1695        "libexample.so=otherlibexample.so-64",
1696        RelationOrSoname::SonameV1(
1697            SonameV1::new(
1698                "libexample.so".parse().unwrap(),
1699                Some(VersionOrSoname::Soname("otherlibexample.so".parse().unwrap())),
1700                Some(ElfArchitectureFormat::Bit64),
1701            ).unwrap()
1702        )
1703    )]
1704    fn test_relation_or_soname_parser(
1705        #[case] mut input: &str,
1706        #[case] expected: RelationOrSoname,
1707    ) -> TestResult {
1708        let input_str = input.to_string();
1709        let result = RelationOrSoname::parser(&mut input)?;
1710        assert_eq!(result, expected);
1711        assert_eq!(result.to_string(), input_str);
1712        Ok(())
1713    }
1714}