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,
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(Debug, Clone, PartialEq, Eq)]
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, Eq, PartialEq)]
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, Eq, PartialEq)]
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 ": ", then dispatch either side to the relevant parser
1011            // without allowing backtracking
1012            separated_pair(
1013                take_until(1.., ": ").and_then(cut_err(PackageRelation::parser)),
1014                ": ",
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.name(), description),
1049            None => write!(fmt, "{}", self.name()),
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`] or a [`SonameV1::Basic`].
1075///
1076/// This enum is used for [alpm-package-relations] of type _run-time dependency_ and _provision_ in
1077/// `.SRCINFO` files.
1078///
1079/// [alpm-package-relations]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
1080#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1081#[serde(untagged)]
1082pub enum RelationOrSoname {
1083    /// A shared object name (as [`SonameV1::Basic`]).
1084    BasicSonameV1(SonameV1),
1085    /// A package relation (as [`PackageRelation`]).
1086    Relation(PackageRelation),
1087}
1088
1089impl PartialEq<PackageRelation> for RelationOrSoname {
1090    fn eq(&self, other: &PackageRelation) -> bool {
1091        self.to_string() == other.to_string()
1092    }
1093}
1094
1095impl PartialEq<SonameV1> for RelationOrSoname {
1096    fn eq(&self, other: &SonameV1) -> bool {
1097        self.to_string() == other.to_string()
1098    }
1099}
1100
1101impl RelationOrSoname {
1102    /// Recognizes a [`SonameV1::Basic`] or a [`PackageRelation`] in a string slice.
1103    ///
1104    /// First attempts to recognize a [`SonameV1::Basic`] and if that fails, falls back to
1105    /// recognizing a [`PackageRelation`].
1106    /// Depending on recognized type, a [`RelationOrSoname`] is created accordingly.
1107    pub fn parser(input: &mut &str) -> ModalResult<Self> {
1108        // Implement a custom `winnow::combinator::alt`, as all type parsers are built in
1109        // such a way that they return errors on unexpected input instead of backtracking.
1110        let checkpoint = input.checkpoint();
1111        let shared_object_name_result = SharedObjectName::parser.parse_next(input);
1112        if shared_object_name_result.is_ok() {
1113            let shared_object_name = shared_object_name_result?;
1114            return Ok(RelationOrSoname::BasicSonameV1(SonameV1::Basic(
1115                shared_object_name,
1116            )));
1117        }
1118
1119        input.reset(&checkpoint);
1120        let relation_result = rest.and_then(PackageRelation::parser).parse_next(input);
1121        if relation_result.is_ok() {
1122            let relation = relation_result?;
1123            return Ok(RelationOrSoname::Relation(relation));
1124        }
1125
1126        cut_err(fail)
1127            .context(StrContext::Expected(StrContextValue::Description(
1128                "alpm-sonamev1 or alpm-package-relation",
1129            )))
1130            .parse_next(input)
1131    }
1132}
1133
1134impl Display for RelationOrSoname {
1135    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1136        match self {
1137            RelationOrSoname::Relation(version) => write!(f, "{version}"),
1138            RelationOrSoname::BasicSonameV1(soname) => write!(f, "{soname}"),
1139        }
1140    }
1141}
1142
1143impl FromStr for RelationOrSoname {
1144    type Err = Error;
1145
1146    /// Creates a [`RelationOrSoname`] from a string slice.
1147    ///
1148    /// Relies on [`RelationOrSoname::parser`] to recognize types in `input` and create a
1149    /// [`RelationOrSoname`] accordingly.
1150    ///
1151    /// # Errors
1152    ///
1153    /// Returns an error if no [`RelationOrSoname`] can be created from `input`.
1154    ///
1155    /// # Examples
1156    ///
1157    /// ```
1158    /// use alpm_types::{PackageRelation, RelationOrSoname, SonameV1};
1159    ///
1160    /// # fn main() -> Result<(), alpm_types::Error> {
1161    /// let relation: RelationOrSoname = "example=1.0.0".parse()?;
1162    /// assert_eq!(
1163    ///     relation,
1164    ///     RelationOrSoname::Relation(PackageRelation::new(
1165    ///         "example".parse()?,
1166    ///         Some("=1.0.0".parse()?)
1167    ///     ))
1168    /// );
1169    ///
1170    /// let soname: RelationOrSoname = "example.so".parse()?;
1171    /// assert_eq!(
1172    ///     soname,
1173    ///     RelationOrSoname::BasicSonameV1(SonameV1::new("example.so".parse()?, None, None)?)
1174    /// );
1175    /// # Ok(())
1176    /// # }
1177    /// ```
1178    fn from_str(s: &str) -> Result<Self, Self::Err> {
1179        Self::parser
1180            .parse(s)
1181            .map_err(|error| Error::ParseError(error.to_string()))
1182    }
1183}
1184
1185#[cfg(test)]
1186mod tests {
1187    use proptest::{prop_assert_eq, proptest, test_runner::Config as ProptestConfig};
1188    use rstest::rstest;
1189    use testresult::TestResult;
1190
1191    use super::*;
1192    use crate::VersionComparison;
1193
1194    const COMPARATOR_REGEX: &str = r"(<|<=|=|>=|>)";
1195    /// NOTE: [`Epoch`][alpm_types::Epoch] is implicitly constrained by [`std::usize::MAX`].
1196    /// However, it's unrealistic to ever reach that many forced downgrades for a package, hence
1197    /// we don't test that fully
1198    const EPOCH_REGEX: &str = r"[1-9]{1}[0-9]{0,10}";
1199    const NAME_REGEX: &str = r"[a-z0-9_@+]+[a-z0-9\-._@+]*";
1200    const PKGREL_REGEX: &str = r"[1-9][0-9]{0,8}(|[.][1-9][0-9]{0,8})";
1201    const PKGVER_REGEX: &str = r"([[:alnum:]][[:alnum:]_+.]*)";
1202    const DESCRIPTION_REGEX: &str = "[^\n\r]*";
1203
1204    proptest! {
1205        #![proptest_config(ProptestConfig::with_cases(1000))]
1206
1207
1208        #[test]
1209        fn valid_package_relation_from_str(s in format!("{NAME_REGEX}(|{COMPARATOR_REGEX}(|{EPOCH_REGEX}:){PKGVER_REGEX}(|-{PKGREL_REGEX}))").as_str()) {
1210            println!("s: {s}");
1211            let name = PackageRelation::from_str(&s).unwrap();
1212            prop_assert_eq!(s, format!("{}", name));
1213        }
1214    }
1215
1216    proptest! {
1217        #[test]
1218        fn opt_depend_from_str(
1219            name in NAME_REGEX,
1220            desc in DESCRIPTION_REGEX,
1221            use_desc in proptest::bool::ANY
1222        ) {
1223            let desc_trimmed = desc.trim_ascii();
1224            let desc_is_blank = desc_trimmed.is_empty();
1225
1226            let (raw_in, formatted_expected) = if use_desc {
1227                // Raw input and expected formatted output.
1228                // These are different because `desc` will be trimmed by the parser;
1229                // if it is *only* ascii whitespace then it will be skipped altogether.
1230                (
1231                    format!("{name}: {desc}"),
1232                    if !desc_is_blank {
1233                        format!("{name}: {desc_trimmed}")
1234                    } else {
1235                        name.clone()
1236                    }
1237                )
1238            } else {
1239                (name.clone(), name.clone())
1240            };
1241
1242            println!("input string: {raw_in}");
1243            let opt_depend = OptionalDependency::from_str(&raw_in).unwrap();
1244            let formatted_actual = format!("{}", opt_depend);
1245            prop_assert_eq!(
1246                formatted_expected,
1247                formatted_actual,
1248                "Formatted output doesn't match input"
1249            );
1250        }
1251    }
1252
1253    #[rstest]
1254    #[case(
1255        "python>=3",
1256        Ok(PackageRelation {
1257            name: Name::new("python").unwrap(),
1258            version_requirement: Some(VersionRequirement {
1259                comparison: VersionComparison::GreaterOrEqual,
1260                version: "3".parse().unwrap(),
1261            }),
1262        }),
1263    )]
1264    #[case(
1265        "java-environment>=17",
1266        Ok(PackageRelation {
1267            name: Name::new("java-environment").unwrap(),
1268            version_requirement: Some(VersionRequirement {
1269                comparison: VersionComparison::GreaterOrEqual,
1270                version: "17".parse().unwrap(),
1271            }),
1272        }),
1273    )]
1274    fn valid_package_relation(
1275        #[case] input: &str,
1276        #[case] expected: Result<PackageRelation, Error>,
1277    ) {
1278        assert_eq!(PackageRelation::from_str(input), expected);
1279    }
1280
1281    #[rstest]
1282    #[case(
1283        "example: this is an example dependency",
1284        Ok(OptionalDependency {
1285            package_relation: PackageRelation {
1286                name: Name::new("example").unwrap(),
1287                version_requirement: None,
1288            },
1289            description: Some("this is an example dependency".to_string()),
1290        }),
1291    )]
1292    #[case(
1293        "example-two:     a description with lots of whitespace padding     ",
1294        Ok(OptionalDependency {
1295            package_relation: PackageRelation {
1296                name: Name::new("example-two").unwrap(),
1297                version_requirement: None,
1298            },
1299            description: Some("a description with lots of whitespace padding".to_string())
1300        }),
1301    )]
1302    #[case(
1303        "dep_name",
1304        Ok(OptionalDependency {
1305            package_relation: PackageRelation {
1306                name: Name::new("dep_name").unwrap(),
1307                version_requirement: None,
1308            },
1309            description: None,
1310        }),
1311    )]
1312    #[case(
1313        "dep_name: ",
1314        Ok(OptionalDependency {
1315            package_relation: PackageRelation {
1316                name: Name::new("dep_name").unwrap(),
1317                version_requirement: None,
1318            },
1319            description: None,
1320        }),
1321    )]
1322    #[case(
1323        "dep_name_with_special_chars-123: description with !@#$%^&*",
1324        Ok(OptionalDependency {
1325            package_relation: PackageRelation {
1326                name: Name::new("dep_name_with_special_chars-123").unwrap(),
1327                version_requirement: None,
1328            },
1329            description: Some("description with !@#$%^&*".to_string()),
1330        }),
1331    )]
1332    // versioned optional dependencies
1333    #[case(
1334        "elfutils=0.192: for translations",
1335        Ok(OptionalDependency {
1336            package_relation: PackageRelation {
1337                name: Name::new("elfutils").unwrap(),
1338                version_requirement: Some(VersionRequirement {
1339                    comparison: VersionComparison::Equal,
1340                    version: "0.192".parse().unwrap(),
1341                }),
1342            },
1343            description: Some("for translations".to_string()),
1344        }),
1345    )]
1346    #[case(
1347        "python>=3: For Python bindings",
1348        Ok(OptionalDependency {
1349            package_relation: PackageRelation {
1350                name: Name::new("python").unwrap(),
1351                version_requirement: Some(VersionRequirement {
1352                    comparison: VersionComparison::GreaterOrEqual,
1353                    version: "3".parse().unwrap(),
1354                }),
1355            },
1356            description: Some("For Python bindings".to_string()),
1357        }),
1358    )]
1359    #[case(
1360        "java-environment>=17: required by extension-wiki-publisher and extension-nlpsolver",
1361        Ok(OptionalDependency {
1362            package_relation: PackageRelation {
1363                name: Name::new("java-environment").unwrap(),
1364                version_requirement: Some(VersionRequirement {
1365                    comparison: VersionComparison::GreaterOrEqual,
1366                    version: "17".parse().unwrap(),
1367                }),
1368            },
1369            description: Some("required by extension-wiki-publisher and extension-nlpsolver".to_string()),
1370        }),
1371    )]
1372    fn opt_depend_from_string(
1373        #[case] input: &str,
1374        #[case] expected_result: Result<OptionalDependency, Error>,
1375    ) {
1376        let opt_depend_result = OptionalDependency::from_str(input);
1377        assert_eq!(expected_result, opt_depend_result);
1378    }
1379
1380    #[rstest]
1381    #[case(
1382        "#invalid-name: this is an example dependency",
1383        "invalid first character of package name"
1384    )]
1385    #[case(": no_name_colon", "invalid first character of package name")]
1386    #[case(
1387        "name:description with no leading whitespace",
1388        "invalid character in package name"
1389    )]
1390    #[case(
1391        "dep-name>=10: \n\ndescription with\rnewlines",
1392        "expected no carriage returns or newlines"
1393    )]
1394    fn opt_depend_invalid_string_parse_error(#[case] input: &str, #[case] err_snippet: &str) {
1395        let Err(Error::ParseError(err_msg)) = OptionalDependency::from_str(input) else {
1396            panic!("'{input}' did not fail to parse as expected")
1397        };
1398        assert!(
1399            err_msg.contains(err_snippet),
1400            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1401        );
1402    }
1403
1404    #[rstest]
1405    #[case("example.so", SonameV1::Basic("example.so".parse().unwrap()))]
1406    #[case("example.so=1.0.0-64", SonameV1::Explicit {
1407        name: "example.so".parse().unwrap(),
1408        version: "1.0.0".parse().unwrap(),
1409        architecture: ElfArchitectureFormat::Bit64,
1410    })]
1411    fn sonamev1_from_string(
1412        #[case] input: &str,
1413        #[case] expected_result: SonameV1,
1414    ) -> testresult::TestResult<()> {
1415        let soname = SonameV1::from_str(input)?;
1416        assert_eq!(expected_result, soname);
1417        assert_eq!(input, soname.to_string());
1418        Ok(())
1419    }
1420
1421    #[rstest]
1422    #[case(
1423        "libwlroots-0.18.so=libwlroots-0.18.so-64",
1424        SonameV1::Unversioned {
1425            name: "libwlroots-0.18.so".parse().unwrap(),
1426            soname: "libwlroots-0.18.so".parse().unwrap(),
1427            architecture: ElfArchitectureFormat::Bit64,
1428        },
1429    )]
1430    #[case(
1431        "libexample.so=otherlibexample.so-64",
1432        SonameV1::Unversioned {
1433            name: "libexample.so".parse().unwrap(),
1434            soname: "otherlibexample.so".parse().unwrap(),
1435            architecture: ElfArchitectureFormat::Bit64,
1436        },
1437    )]
1438    fn sonamev1_from_string_without_version(
1439        #[case] input: &str,
1440        #[case] expected_result: SonameV1,
1441    ) -> testresult::TestResult<()> {
1442        let soname = SonameV1::from_str(input)?;
1443        assert_eq!(expected_result, soname);
1444        assert_eq!(input, soname.to_string());
1445        Ok(())
1446    }
1447
1448    #[rstest]
1449    #[case("noso", "invalid shared object name")]
1450    #[case("invalidversion.so=1*2-64", "expected version or shared object name")]
1451    #[case(
1452        "nodelimiter.so=1.64",
1453        "expected a version or shared object name, followed by an ELF architecture format"
1454    )]
1455    #[case(
1456        "noarchitecture.so=1-",
1457        "expected a version or shared object name, followed by an ELF architecture format"
1458    )]
1459    #[case("invalidarchitecture.so=1-82", "invalid architecture")]
1460    #[case("invalidsoname.so~1.64", "unexpected trailing content")]
1461    fn invalid_sonamev1_parser(#[case] input: &str, #[case] error_snippet: &str) {
1462        let result = SonameV1::from_str(input);
1463        assert!(result.is_err(), "Expected SonameV1 parsing to fail");
1464        let err = result.unwrap_err();
1465        let pretty_error = err.to_string();
1466        assert!(
1467            pretty_error.contains(error_snippet),
1468            "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
1469        );
1470    }
1471
1472    #[rstest]
1473    #[case(
1474        "otherlibexample.so",
1475        VersionOrSoname::Soname(
1476            SharedObjectName::new("otherlibexample.so").unwrap())
1477    )]
1478    #[case(
1479        "1.0.0",
1480        VersionOrSoname::Version(
1481            PackageVersion::from_str("1.0.0").unwrap())
1482    )]
1483    fn version_or_soname_from_string(
1484        #[case] input: &str,
1485        #[case] expected_result: VersionOrSoname,
1486    ) -> testresult::TestResult<()> {
1487        let version = VersionOrSoname::from_str(input)?;
1488        assert_eq!(expected_result, version);
1489        assert_eq!(input, version.to_string());
1490        Ok(())
1491    }
1492
1493    #[rstest]
1494    #[case(
1495        "lib:libexample.so",
1496        SonameV2 {
1497            prefix: "lib".parse().unwrap(),
1498            soname: Soname {
1499                name: "libexample.so".parse().unwrap(),
1500                version: None,
1501            },
1502        },
1503    )]
1504    #[case(
1505        "usr:libexample.so.1",
1506        SonameV2 {
1507            prefix: "usr".parse().unwrap(),
1508            soname: Soname {
1509                name: "libexample.so".parse().unwrap(),
1510                version: "1".parse().ok(),
1511            },
1512        },
1513    )]
1514    #[case(
1515        "lib:libexample.so.1.2.3",
1516        SonameV2 {
1517            prefix: "lib".parse().unwrap(),
1518            soname: Soname {
1519                name: "libexample.so".parse().unwrap(),
1520                version: "1.2.3".parse().ok(),
1521            },
1522        },
1523    )]
1524    #[case(
1525        "lib:libexample.so.so.420",
1526        SonameV2 {
1527            prefix: "lib".parse().unwrap(),
1528            soname: Soname {
1529                name: "libexample.so.so".parse().unwrap(),
1530                version: "420".parse().ok(),
1531            },
1532        },
1533    )]
1534    #[case(
1535        "lib:libexample.so.test",
1536        SonameV2 {
1537            prefix: "lib".parse().unwrap(),
1538            soname: Soname {
1539                name: "libexample.so".parse().unwrap(),
1540                version: "test".parse().ok(),
1541            },
1542        },
1543    )]
1544    fn sonamev2_from_string(
1545        #[case] input: &str,
1546        #[case] expected_result: SonameV2,
1547    ) -> testresult::TestResult<()> {
1548        let soname = SonameV2::from_str(input)?;
1549        assert_eq!(expected_result, soname);
1550        assert_eq!(input, soname.to_string());
1551        Ok(())
1552    }
1553
1554    #[rstest]
1555    #[case("libexample.so.1", "invalid shared library prefix delimiter")]
1556    #[case("lib:libexample.so-abc", "invalid version delimiter")]
1557    #[case("lib:libexample.so.10-10", "invalid pkgver character")]
1558    #[case("lib:libexample.so.1.0.0-64", "invalid pkgver character")]
1559    fn invalid_sonamev2_parser(#[case] input: &str, #[case] error_snippet: &str) {
1560        let result = SonameV2::from_str(input);
1561        assert!(result.is_err(), "Expected SonameV2 parsing to fail");
1562        let err = result.unwrap_err();
1563        let pretty_error = err.to_string();
1564        assert!(
1565            pretty_error.contains(error_snippet),
1566            "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
1567        );
1568    }
1569
1570    #[rstest]
1571    #[case(
1572        "example",
1573        RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), None))
1574    )]
1575    #[case(
1576        "example=1.0.0",
1577        RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), "=1.0.0".parse().ok())) 
1578    )]
1579    #[case(
1580        "example>=1.0.0",
1581        RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), ">=1.0.0".parse().ok()))
1582    )]
1583    #[case(
1584        "example.so",
1585        RelationOrSoname::BasicSonameV1(
1586            SonameV1::new(
1587                "example.so".parse().unwrap(),
1588                None,
1589                None,
1590            ).unwrap()
1591        )
1592    )]
1593    fn test_relation_or_soname_parser(
1594        #[case] mut input: &str,
1595        #[case] expected: RelationOrSoname,
1596    ) -> TestResult {
1597        let input_str = input.to_string();
1598        let result = RelationOrSoname::parser(&mut input)?;
1599        assert_eq!(result, expected);
1600        assert_eq!(result.to_string(), input_str);
1601        Ok(())
1602    }
1603}