Skip to main content

alpm_types/relation/
soname.rs

1//! Representation of [soname] information in [ELF] files.
2//!
3//! [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
4//! [soname]: https://en.wikipedia.org/wiki/Soname
5
6use std::{
7    fmt::{Display, Formatter},
8    str::FromStr,
9};
10
11use alpm_parsers::traits::{AlpmParser, ParserUntil};
12use serde::{Deserialize, Serialize};
13use winnow::{
14    ModalResult,
15    Parser,
16    combinator::{alt, eof, opt, peek, repeat_till},
17    error::{ContextError, ErrMode, StrContext, StrContextValue},
18    token::any,
19};
20
21#[cfg(doc)]
22use crate::PackageRelation;
23use crate::{ElfArchitectureFormat, Error, Name, PackageVersion, SharedObjectName};
24
25/// Provides either a [`PackageVersion`] or a [`SharedObjectName`].
26///
27/// This enum is used when creating [`SonameV1`].
28#[derive(Clone, Debug, Eq, PartialEq)]
29pub enum VersionOrSoname {
30    /// A version for a [`SonameV1`].
31    Version(PackageVersion),
32
33    /// A soname for a [`SonameV1`].
34    Soname(SharedObjectName),
35}
36
37impl FromStr for VersionOrSoname {
38    type Err = Error;
39
40    /// Creates a [`VersionOrSoname`] from a string slice.
41    ///
42    /// Delegates to [`VersionOrSoname::parser`].
43    ///
44    /// # Errors
45    ///
46    /// Returns an error if [`VersionOrSoname::parser`] fails.
47    fn from_str(s: &str) -> Result<Self, Self::Err> {
48        Ok(Self::parser.parse(s)?)
49    }
50}
51
52impl AlpmParser for VersionOrSoname {
53    /// Recognizes a [`PackageVersion`] or [`SharedObjectName`] in a string slice.
54    ///
55    /// First attempts to recognize a [`SharedObjectName`] and if that fails, falls back to
56    /// recognizing a [`PackageVersion`].
57    ///
58    /// # Errors
59    ///
60    /// Returns an error if `input` does not begin with a valid [`SharedObjectName`] or
61    /// [`PackageVersion`].
62    fn parser(input: &mut &str) -> ModalResult<Self> {
63        alt((
64            SharedObjectName::parser.map(VersionOrSoname::Soname),
65            PackageVersion::parser.map(VersionOrSoname::Version),
66        ))
67        .context(StrContext::Label("version or shared object name"))
68        .context(StrContext::Expected(StrContextValue::Description(
69            "a valid alpm-sonamev1 or alpm-pkgver",
70        )))
71        .parse_next(input)
72    }
73
74    fn delimiter_error_context<'a, O, P>(
75        parser: P,
76    ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
77    where
78        P: Parser<&'a str, O, ErrMode<ContextError>>,
79    {
80        parser
81            .context(StrContext::Label("version or shared object name"))
82            .context(StrContext::Expected(StrContextValue::Description(
83                "end of input.",
84            )))
85    }
86}
87
88impl Display for VersionOrSoname {
89    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
90        match self {
91            VersionOrSoname::Version(version) => write!(f, "{version}"),
92            VersionOrSoname::Soname(soname) => write!(f, "{soname}"),
93        }
94    }
95}
96
97/// Representation of [soname] data of a shared object based on the [alpm-sonamev1] specification.
98///
99/// Soname data may be used as [alpm-package-relation] of type _provision_ and _run-time
100/// dependency_.
101/// This type distinguishes between three forms: _basic_, _unversioned_ and _explicit_.
102///
103/// - [`SonameV1::Basic`] is used when only the `name` of a _shared object_ file is used. This form
104///   can be used in files that may contain static data about package sources (e.g. [PKGBUILD] or
105///   [SRCINFO] files).
106/// - [`SonameV1::Unversioned`] is used when the `name` of a _shared object_ file, its _soname_
107///   (which does _not_ expose a specific version) and its `architecture` (derived from the [ELF]
108///   class of the file) are used. This form can be used in files that may contain dynamic data
109///   derived from a specific package build environment (i.e. [PKGINFO]). It is discouraged to use
110///   this form in files that contain static data about package sources (e.g. [PKGBUILD] or
111///   [SRCINFO] files).
112/// - [`SonameV1::Explicit`] is used when the `name` of a _shared object_ file, the `version` from
113///   its _soname_ and its `architecture` (derived from the [ELF] class of the file) are used. This
114///   form can be used in files that may contain dynamic data derived from a specific package build
115///   environment (i.e. [PKGINFO]). It is discouraged to use this form in files that contain static
116///   data about package sources (e.g. [PKGBUILD] or [SRCINFO] files).
117///
118/// # Warning
119///
120/// This type is **deprecated** and `SonameV2` should be preferred instead!
121/// Due to the loose nature of the [alpm-sonamev1] specification, the _basic_ form overlaps with the
122/// specification of [`Name`] and the _explicit_ form overlaps with that of [`PackageRelation`].
123///
124/// # Examples
125///
126/// ```
127/// use alpm_types::{ElfArchitectureFormat, SonameV1};
128///
129/// # fn main() -> Result<(), alpm_types::Error> {
130/// let basic_soname = SonameV1::Basic("example.so".parse()?);
131/// let unversioned_soname = SonameV1::Unversioned {
132///     name: "example.so".parse()?,
133///     soname: "example.so".parse()?,
134///     architecture: ElfArchitectureFormat::Bit64,
135/// };
136/// let explicit_soname = SonameV1::Explicit {
137///     name: "example.so".parse()?,
138///     version: "1.0.0".parse()?,
139///     architecture: ElfArchitectureFormat::Bit64,
140/// };
141/// # Ok(())
142/// # }
143/// ```
144///
145/// [alpm-package-relation]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
146/// [alpm-sonamev1]: https://alpm.archlinux.page/specifications/alpm-sonamev1.7.html
147/// [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
148/// [soname]: https://en.wikipedia.org/wiki/Soname
149/// [PKGBUILD]: https://man.archlinux.org/man/PKGBUILD.5
150/// [SRCINFO]: https://alpm.archlinux.page/specifications/SRCINFO.5.html
151/// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
152#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
153pub enum SonameV1 {
154    /// Basic representation of a _shared object_ file.
155    ///
156    /// Tracks the `name` of a _shared object_ file.
157    /// This form is used when referring to _shared object_ files without their soname data.
158    ///
159    /// # Examples
160    ///
161    /// ```
162    /// use std::str::FromStr;
163    ///
164    /// use alpm_types::SonameV1;
165    ///
166    /// # fn main() -> Result<(), alpm_types::Error> {
167    /// let soname = SonameV1::from_str("example.so")?;
168    /// assert_eq!(soname, SonameV1::Basic("example.so".parse()?));
169    /// # Ok(())
170    /// # }
171    /// ```
172    Basic(SharedObjectName),
173
174    /// Unversioned representation of an ELF file's soname data.
175    ///
176    /// Tracks the `name` of a _shared object_ file, its _soname_ instead of a version and its
177    /// `architecture`. This form is used if the _soname data_ of a _shared object_ does not
178    /// expose a version.
179    ///
180    /// # Examples
181    ///
182    /// ```
183    /// use std::str::FromStr;
184    ///
185    /// use alpm_types::{ElfArchitectureFormat, SonameV1};
186    ///
187    /// # fn main() -> Result<(), alpm_types::Error> {
188    /// let soname = SonameV1::from_str("example.so=example.so-64")?;
189    /// assert_eq!(
190    ///     soname,
191    ///     SonameV1::Unversioned {
192    ///         name: "example.so".parse()?,
193    ///         soname: "example.so".parse()?,
194    ///         architecture: ElfArchitectureFormat::Bit64,
195    ///     }
196    /// );
197    /// # Ok(())
198    /// # }
199    /// ```
200    Unversioned {
201        /// The least specific name of the shared object file.
202        name: SharedObjectName,
203        /// The value of the shared object's _SONAME_ field in its _dynamic section_.
204        soname: SharedObjectName,
205        /// The ELF architecture format of the shared object file.
206        architecture: ElfArchitectureFormat,
207    },
208
209    /// Explicit representation of an ELF file's soname data.
210    ///
211    /// Tracks the `name` of a _shared object_ file, the `version` of its _soname_ and its
212    /// `architecture`. This form is used if the _soname data_ of a _shared object_ exposes a
213    /// specific version.
214    ///
215    /// # Examples
216    ///
217    /// ```
218    /// use std::str::FromStr;
219    ///
220    /// use alpm_types::{ElfArchitectureFormat, SonameV1};
221    ///
222    /// # fn main() -> Result<(), alpm_types::Error> {
223    /// let soname = SonameV1::from_str("example.so=1.0.0-64")?;
224    /// assert_eq!(
225    ///    soname,
226    ///    SonameV1::Explicit {
227    ///         name: "example.so".parse()?,
228    ///         version: "1.0.0".parse()?,
229    ///         architecture: ElfArchitectureFormat::Bit64,
230    ///     }
231    /// );
232    /// # Ok(())
233    /// # }
234    Explicit {
235        /// The least specific name of the shared object file.
236        name: SharedObjectName,
237        /// The version of the shared object file (as exposed in its _soname_ data).
238        version: PackageVersion,
239        /// The ELF architecture format of the shared object file.
240        architecture: ElfArchitectureFormat,
241    },
242}
243
244impl SonameV1 {
245    /// Creates a new [`SonameV1`].
246    ///
247    /// Depending on input, this function returns different variants of [`SonameV1`]:
248    ///
249    /// - [`SonameV1::Basic`], if both `version_or_soname` and `architecture` are [`None`]
250    /// - [`SonameV1::Unversioned`], if `version_or_soname` is [`VersionOrSoname::Soname`] and
251    ///   `architecture` is [`Some`]
252    /// - [`SonameV1::Explicit`], if `version_or_soname` is [`VersionOrSoname::Version`] and
253    ///   `architecture` is [`Some`]
254    ///
255    /// # Examples
256    ///
257    /// ```
258    /// use alpm_types::{ElfArchitectureFormat, SonameV1};
259    ///
260    /// # fn main() -> Result<(), alpm_types::Error> {
261    /// let basic_soname = SonameV1::new("example.so".parse()?, None, None)?;
262    /// assert_eq!(basic_soname, SonameV1::Basic("example.so".parse()?));
263    ///
264    /// let unversioned_soname = SonameV1::new(
265    ///     "example.so".parse()?,
266    ///     Some("example.so".parse()?),
267    ///     Some(ElfArchitectureFormat::Bit64),
268    /// )?;
269    /// assert_eq!(
270    ///     unversioned_soname,
271    ///     SonameV1::Unversioned {
272    ///         name: "example.so".parse()?,
273    ///         soname: "example.so".parse()?,
274    ///         architecture: "64".parse()?
275    ///     }
276    /// );
277    ///
278    /// let explicit_soname = SonameV1::new(
279    ///     "example.so".parse()?,
280    ///     Some("1.0.0".parse()?),
281    ///     Some(ElfArchitectureFormat::Bit64),
282    /// )?;
283    /// assert_eq!(
284    ///     explicit_soname,
285    ///     SonameV1::Explicit {
286    ///         name: "example.so".parse()?,
287    ///         version: "1.0.0".parse()?,
288    ///         architecture: "64".parse()?
289    ///     }
290    /// );
291    /// # Ok(())
292    /// # }
293    /// ```
294    pub fn new(
295        name: SharedObjectName,
296        version_or_soname: Option<VersionOrSoname>,
297        architecture: Option<ElfArchitectureFormat>,
298    ) -> Result<Self, Error> {
299        match (version_or_soname, architecture) {
300            (None, None) => Ok(Self::Basic(name)),
301            (Some(VersionOrSoname::Version(version)), Some(architecture)) => Ok(Self::Explicit {
302                name,
303                version,
304                architecture,
305            }),
306            (Some(VersionOrSoname::Soname(soname)), Some(architecture)) => Ok(Self::Unversioned {
307                name,
308                soname,
309                architecture,
310            }),
311            (None, Some(_)) => Err(Error::InvalidSonameV1(
312                "SonameV1 needs a version when specifying architecture",
313            )),
314            (Some(_), None) => Err(Error::InvalidSonameV1(
315                "SonameV1 needs an architecture when specifying version",
316            )),
317        }
318    }
319
320    /// Returns a reference to the [`SharedObjectName`] of the [`SonameV1`].
321    ///
322    /// # Examples
323    ///
324    /// ```
325    /// use alpm_types::{ElfArchitectureFormat, SharedObjectName, SonameV1};
326    ///
327    /// # fn main() -> Result<(), alpm_types::Error> {
328    /// let shared_object_name: SharedObjectName = "example.so".parse()?;
329    ///
330    /// let basic = SonameV1::new("example.so".parse()?, None, None)?;
331    /// assert_eq!(&shared_object_name, basic.shared_object_name());
332    ///
333    /// let unversioned = SonameV1::new(
334    ///     "example.so".parse()?,
335    ///     Some("example.so".parse()?),
336    ///     Some(ElfArchitectureFormat::Bit64),
337    /// )?;
338    /// assert_eq!(&shared_object_name, unversioned.shared_object_name());
339    ///
340    /// let explicit = SonameV1::new(
341    ///     "example.so".parse()?,
342    ///     Some("1.0.0".parse()?),
343    ///     Some(ElfArchitectureFormat::Bit64),
344    /// )?;
345    /// assert_eq!(&shared_object_name, explicit.shared_object_name());
346    /// # Ok(())
347    /// # }
348    /// ```
349    pub fn shared_object_name(&self) -> &SharedObjectName {
350        match self {
351            SonameV1::Basic(name) => name,
352            SonameV1::Unversioned { name, .. } => name,
353            SonameV1::Explicit { name, .. } => name,
354        }
355    }
356}
357
358impl AlpmParser for SonameV1 {
359    /// Recognizes a [`SonameV1`] in a string slice.
360    ///
361    /// # Errors
362    ///
363    /// Returns an error if `input` does not begin with an [alpm-sonamev1].
364    ///
365    /// [alpm-sonamev1]: https://alpm.archlinux.page/specifications/alpm-sonamev1.7.html
366    fn parser(input: &mut &str) -> ModalResult<Self> {
367        // Parse the shared object name.
368        let name = repeat_till(1.., any, peek(alt(("=", eof))))
369            .try_map(|(name, _): (String, &str)| SharedObjectName::from_str(&name))
370            .context(StrContext::Label("shared object name"))
371            .parse_next(input)?;
372
373        // Parse the version delimiter `=`.
374        //
375        // If it doesn't exist, it is the basic form.
376        if opt("=").parse_next(input)?.is_none() {
377            return Ok(SonameV1::Basic(name));
378        }
379
380        // Two cases are possible here:
381        //
382        // 1. Unversioned: `name=soname-architecture`
383        // 2. Explicit: `name=version-architecture`
384        let version_or_soname = VersionOrSoname::parser
385            .context(StrContext::Expected(StrContextValue::Description(
386                "a version or shared object name, followed by an ELF architecture format",
387            )))
388            .parse_next(input)?;
389
390        // Parse the `-` delimiter
391        "-".context(StrContext::Label("architecture delimiter"))
392            .context(StrContext::Expected(StrContextValue::Description(
393                "architecture delimiter `-`",
394            )))
395            .parse_next(input)?;
396
397        // Parse the architecture
398        let architecture = ElfArchitectureFormat::parser.parse_next(input)?;
399
400        match version_or_soname {
401            VersionOrSoname::Version(version) => Ok(SonameV1::Explicit {
402                name,
403                version,
404                architecture,
405            }),
406            VersionOrSoname::Soname(soname) => Ok(SonameV1::Unversioned {
407                name,
408                soname,
409                architecture,
410            }),
411        }
412    }
413
414    fn delimiter_error_context<'a, O, P>(
415        parser: P,
416    ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
417    where
418        P: Parser<&'a str, O, ErrMode<ContextError>>,
419    {
420        parser
421            .context(StrContext::Label("sonamev1"))
422            .context(StrContext::Expected(StrContextValue::Description(
423                "the string to end after the sonamev1 definition.",
424            )))
425    }
426}
427
428impl FromStr for SonameV1 {
429    type Err = Error;
430    /// Creates a [`SonameV1`] from a string slice.
431    ///
432    /// The string slice must be in the format `name[=version-architecture]`.
433    ///
434    /// Delegates to [`SonameV1::parser`].
435    ///
436    /// # Errors
437    ///
438    /// Returns an error if [`SonameV1::parser`] fails.
439    ///
440    /// # Examples
441    ///
442    /// ```
443    /// use std::str::FromStr;
444    ///
445    /// use alpm_types::{ElfArchitectureFormat, SonameV1};
446    ///
447    /// # fn main() -> Result<(), alpm_types::Error> {
448    /// assert_eq!(
449    ///     SonameV1::from_str("example.so=1.0.0-64")?,
450    ///     SonameV1::Explicit {
451    ///         name: "example.so".parse()?,
452    ///         version: "1.0.0".parse()?,
453    ///         architecture: ElfArchitectureFormat::Bit64,
454    ///     },
455    /// );
456    /// assert_eq!(
457    ///     SonameV1::from_str("example.so=example.so-64")?,
458    ///     SonameV1::Unversioned {
459    ///         name: "example.so".parse()?,
460    ///         soname: "example.so".parse()?,
461    ///         architecture: ElfArchitectureFormat::Bit64,
462    ///     },
463    /// );
464    /// assert_eq!(
465    ///     SonameV1::from_str("example.so")?,
466    ///     SonameV1::Basic("example.so".parse()?),
467    /// );
468    /// # Ok(())
469    /// # }
470    /// ```
471    fn from_str(s: &str) -> Result<Self, Self::Err> {
472        Ok(Self::parser_until_eof.parse(s)?)
473    }
474}
475
476impl Display for SonameV1 {
477    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
478        match self {
479            Self::Basic(name) => write!(f, "{name}"),
480            Self::Unversioned {
481                name,
482                soname,
483                architecture,
484            } => write!(f, "{name}={soname}-{architecture}"),
485            Self::Explicit {
486                name,
487                version,
488                architecture,
489            } => write!(f, "{name}={version}-{architecture}"),
490        }
491    }
492}
493
494/// A prefix associated with a library lookup directory.
495///
496/// Library lookup directories are used when detecting shared object files on a system.
497/// Each such lookup directory can be assigned to a _prefix_, which allows identifying them in other
498/// contexts. E.g. `lib` may serve as _prefix_ for the lookup directory `/usr/lib`.
499///
500/// May only consist of alphanumeric characters
501pub type SharedLibraryPrefix = Name;
502
503/// The value of a shared object's _soname_.
504///
505/// This data may be present in the _SONAME_ or _NEEDED_ fields of a shared object's _dynamic
506/// section_.
507///
508/// The _soname_ data may contain only a shared object name (e.g. `libexample.so`) or a shared
509/// object name, that also encodes version information (e.g. `libexample.so.1`).
510#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
511pub struct Soname {
512    /// The name part of a shared object's _soname_.
513    pub name: SharedObjectName,
514    /// The optional version part of a shared object's _soname_.
515    pub version: Option<PackageVersion>,
516}
517
518impl Soname {
519    /// Creates a new [`Soname`].
520    pub fn new(name: SharedObjectName, version: Option<PackageVersion>) -> Self {
521        Self { name, version }
522    }
523
524    /// Recognizes a [`Soname`] in a string slice.
525    ///
526    /// The passed data can be in the following formats:
527    ///
528    /// - `<name>.so`: A shared object name without a version. (e.g. `libexample.so`)
529    /// - `<name>.so.<version>`: A shared object name with a version. (e.g. `libexample.so.1`)
530    ///     - The version must be a valid [`PackageVersion`].
531    ///
532    /// # Errors
533    ///
534    /// Returns an error if `input` does not begin with a valid [`Soname`].
535    pub fn parser(input: &mut &str) -> ModalResult<Self> {
536        // NOTE: This parser is pretty much all over the place, as there's no way to parse this
537        // type in a paradigmatic way. There are no clear delimiters, and parsing can effectively
538        // only be achieved by splitting on `.` characters from the back of the string, or by
539        // looking for the `.so` substring.
540        // However, those may also part of the `Name` character set (which is why we check for
541        // multiple `.so` instances).
542        let name = SharedObjectName::parser
543            .context(StrContext::Label("shared object name"))
544            .parse_next(input)?;
545
546        // Parse the version delimiter.
547        let delimiter = opt(".").parse_next(input)?;
548
549        // If a `.` is found, map the rest of the string to a version.
550        // Otherwise, we hit the `eof` and there's no version.
551        let version = if delimiter.is_some() {
552            Some(PackageVersion::parser.parse_next(input)?)
553        } else {
554            None
555        };
556
557        Ok(Self { name, version })
558    }
559}
560
561impl Display for Soname {
562    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
563        match &self.version {
564            Some(version) => write!(f, "{name}.{version}", name = self.name),
565            None => write!(f, "{name}", name = self.name),
566        }
567    }
568}
569
570impl FromStr for Soname {
571    type Err = Error;
572
573    /// Recognizes a [`Soname`] in a string slice.
574    ///
575    /// The string slice must be in the format of `<name>.so` or `<name>.so.<version>`.
576    ///
577    /// # Errors
578    ///
579    /// Returns an error if a [`Soname`] can not be parsed from input.
580    ///
581    /// # Examples
582    ///
583    /// ```
584    /// use std::str::FromStr;
585    ///
586    /// use alpm_types::Soname;
587    /// # fn main() -> Result<(), alpm_types::Error> {
588    /// assert_eq!(
589    ///     Soname::from_str("libexample.so.1")?,
590    ///     Soname::new("libexample.so".parse()?, Some("1".parse()?)),
591    /// );
592    /// assert_eq!(
593    ///     Soname::from_str("libexample.so")?,
594    ///     Soname::new("libexample.so".parse()?, None),
595    /// );
596    /// # Ok(())
597    /// # }
598    /// ```
599    fn from_str(s: &str) -> Result<Self, Self::Err> {
600        Ok(Self::parser.parse(s)?)
601    }
602}
603
604/// Representation of [soname] data of a shared object based on the [alpm-sonamev2] specification.
605///
606/// Soname data may be used as [alpm-package-relation] of type _provision_ or _run-time dependency_
607/// in [`PackageInfoV1`] and [`PackageInfoV2`]. The data consists of the arbitrarily
608/// defined `prefix`, which denotes the use name of a specific library directory, and the `soname`,
609/// which refers to the value of either the _SONAME_ or a _NEEDED_ field in the _dynamic section_ of
610/// an [ELF] file.
611///
612/// # Examples
613///
614/// This example assumpes that `lib` is used as the `prefix` for the library directory `/usr/lib`
615/// and the following files are contained in it:
616///
617/// ```bash
618/// /usr/lib/libexample.so -> libexample.so.1
619/// /usr/lib/libexample.so.1 -> libexample.so.1.0.0
620/// /usr/lib/libexample.so.1.0.0
621/// ```
622///
623/// The above file `/usr/lib/libexample.so.1.0.0` represents an [ELF] file, that exposes
624/// `libexample.so.1` as value of the _SONAME_ field in its _dynamic section_. This data can be
625/// represented as follows, using [`SonameV2`]:
626///
627/// ```rust
628/// use alpm_types::{Soname, SonameV2};
629///
630/// # fn main() -> Result<(), alpm_types::Error> {
631/// let soname_data = SonameV2 {
632///     prefix: "lib".parse()?,
633///     soname: Soname {
634///         name: "libexample.so".parse()?,
635///         version: Some("1".parse()?),
636///     },
637/// };
638/// assert_eq!(soname_data.to_string(), "lib:libexample.so.1");
639/// # Ok(())
640/// # }
641/// ```
642///
643/// [alpm-sonamev2]: https://alpm.archlinux.page/specifications/alpm-sonamev2.7.html
644/// [alpm-package-relation]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
645/// [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
646/// [soname]: https://en.wikipedia.org/wiki/Soname
647/// [`PackageInfoV1`]: https://docs.rs/alpm_pkginfo/latest/alpm_pkginfo/struct.PackageInfoV1.html
648/// [`PackageInfoV2`]: https://docs.rs/alpm_pkginfo/latest/alpm_pkginfo/struct.PackageInfoV2.html
649#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
650pub struct SonameV2 {
651    /// The directory prefix of the shared object file.
652    pub prefix: SharedLibraryPrefix,
653    /// The _soname_ of a shared object file.
654    pub soname: Soname,
655}
656
657impl SonameV2 {
658    /// Creates a new [`SonameV2`].
659    ///
660    /// # Examples
661    ///
662    /// ```
663    /// use alpm_types::SonameV2;
664    ///
665    /// # fn main() -> Result<(), alpm_types::Error> {
666    /// SonameV2::new("lib".parse()?, "libexample.so.1".parse()?);
667    /// # Ok(())
668    /// # }
669    /// ```
670    pub fn new(prefix: SharedLibraryPrefix, soname: Soname) -> Self {
671        Self { prefix, soname }
672    }
673}
674
675impl AlpmParser for SonameV2 {
676    /// Recognizes a [`SonameV2`] in a string slice.
677    ///
678    /// The passed data must be in the format `<prefix>:<soname>`. (e.g. `lib:libexample.so.1`)
679    ///
680    /// See [`Soname::parser`] for details on the format of `<soname>`.
681    ///
682    /// # Errors
683    ///
684    /// Returns an error if `input` does not begin with a valid [`SonameV2`].
685    fn parser(input: &mut &str) -> ModalResult<Self> {
686        // Parse everything from the start to the first `:` and parse as `SharedLibraryPrefix`.
687        let prefix = repeat_till(1.., any, peek(alt((":", eof))))
688            .try_map(|(name, _): (String, &str)| SharedLibraryPrefix::from_str(&name))
689            .context(StrContext::Label("prefix for a shared object lookup path"))
690            .parse_next(input)?;
691
692        ":".context(StrContext::Label("shared library prefix delimiter"))
693            .context(StrContext::Expected(StrContextValue::Description(
694                "shared library prefix `:`",
695            )))
696            .parse_next(input)?;
697
698        let soname = Soname::parser.parse_next(input)?;
699
700        Ok(Self { prefix, soname })
701    }
702
703    fn delimiter_error_context<'a, O, P>(
704        parser: P,
705    ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
706    where
707        P: Parser<&'a str, O, ErrMode<ContextError>>,
708    {
709        parser
710            .context(StrContext::Label("sonamev2"))
711            .context(StrContext::Expected(StrContextValue::Description(
712                "end of input.",
713            )))
714    }
715}
716
717impl FromStr for SonameV2 {
718    type Err = Error;
719
720    /// Creates a [`SonameV2`] from a string slice.
721    ///
722    /// The string slice must be in the format `<prefix>:<soname>`.
723    ///
724    /// Delegates to [`SonameV2::parser`].
725    ///
726    /// # Errors
727    ///
728    /// Returns an error if [`SonameV2::parser`] fails.
729    ///
730    /// # Examples
731    ///
732    /// ```
733    /// use std::str::FromStr;
734    ///
735    /// use alpm_types::{Soname, SonameV2};
736    ///
737    /// # fn main() -> Result<(), alpm_types::Error> {
738    /// assert_eq!(
739    ///     SonameV2::from_str("lib:libexample.so.1")?,
740    ///     SonameV2::new(
741    ///         "lib".parse()?,
742    ///         Soname::new("libexample.so".parse()?, Some("1".parse()?))
743    ///     ),
744    /// );
745    /// # Ok(())
746    /// # }
747    /// ```
748    fn from_str(s: &str) -> Result<Self, Self::Err> {
749        Ok(Self::parser_until_eof.parse(s)?)
750    }
751}
752
753impl Display for SonameV2 {
754    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
755        write!(
756            f,
757            "{prefix}:{soname}",
758            prefix = self.prefix,
759            soname = self.soname
760        )
761    }
762}
763
764#[cfg(test)]
765mod tests {
766    use insta::assert_snapshot;
767    use rstest::rstest;
768
769    use super::*;
770    use crate::configure_insta;
771
772    #[rstest]
773    #[case("example.so", SonameV1::Basic("example.so".parse().unwrap()))]
774    #[case("example.so=1.0.0-64", SonameV1::Explicit {
775        name: "example.so".parse().unwrap(),
776        version: "1.0.0".parse().unwrap(),
777        architecture: ElfArchitectureFormat::Bit64,
778    })]
779    fn sonamev1_from_string(
780        #[case] input: &str,
781        #[case] expected_result: SonameV1,
782    ) -> testresult::TestResult<()> {
783        let soname = SonameV1::from_str(input)?;
784        assert_eq!(expected_result, soname);
785        assert_eq!(input, soname.to_string());
786        Ok(())
787    }
788
789    #[rstest]
790    #[case(
791        "libwlroots-0.18.so=libwlroots-0.18.so-64",
792        SonameV1::Unversioned {
793            name: "libwlroots-0.18.so".parse().unwrap(),
794            soname: "libwlroots-0.18.so".parse().unwrap(),
795            architecture: ElfArchitectureFormat::Bit64,
796        },
797    )]
798    #[case(
799        "libexample.so=otherlibexample.so-64",
800        SonameV1::Unversioned {
801            name: "libexample.so".parse().unwrap(),
802            soname: "otherlibexample.so".parse().unwrap(),
803            architecture: ElfArchitectureFormat::Bit64,
804        },
805    )]
806    fn sonamev1_from_string_without_version(
807        #[case] input: &str,
808        #[case] expected_result: SonameV1,
809    ) -> testresult::TestResult<()> {
810        let soname = SonameV1::from_str(input)?;
811        assert_eq!(expected_result, soname);
812        assert_eq!(input, soname.to_string());
813        Ok(())
814    }
815
816    #[rstest]
817    #[case("noso")]
818    #[case("invalidversion.so=1🐀2-64")]
819    #[case("nodelimiter.so=1.64")]
820    #[case("noarchitecture.so=1-")]
821    #[case("invalidarchitecture.so=1-82")]
822    #[case("invalidsoname.so~1.64")]
823    fn invalid_sonamev1_parser(#[case] input: &str) {
824        let Err(Error::ParseError(err_msg)) = SonameV1::from_str(input) else {
825            panic!("parsing '{input}' as FullVersion did not fail as expected")
826        };
827
828        let (test_name, _guard) = configure_insta();
829        assert_snapshot!(test_name, err_msg.to_string());
830    }
831
832    #[rstest]
833    #[case(
834        "otherlibexample.so",
835        VersionOrSoname::Soname(
836            SharedObjectName::new("otherlibexample.so").unwrap())
837    )]
838    #[case(
839        "1.0.0",
840        VersionOrSoname::Version(
841            PackageVersion::from_str("1.0.0").unwrap())
842    )]
843    fn version_or_soname_from_string(
844        #[case] input: &str,
845        #[case] expected_result: VersionOrSoname,
846    ) -> testresult::TestResult<()> {
847        let version = VersionOrSoname::from_str(input)?;
848        assert_eq!(expected_result, version);
849        assert_eq!(input, version.to_string());
850        Ok(())
851    }
852
853    #[rstest]
854    #[case(
855        "lib:libexample.so",
856        SonameV2 {
857            prefix: "lib".parse().unwrap(),
858            soname: Soname {
859                name: "libexample.so".parse().unwrap(),
860                version: None,
861            },
862        },
863    )]
864    #[case(
865        "usr:libexample.so.1",
866        SonameV2 {
867            prefix: "usr".parse().unwrap(),
868            soname: Soname {
869                name: "libexample.so".parse().unwrap(),
870                version: "1".parse().ok(),
871            },
872        },
873    )]
874    #[case(
875        "lib:libexample.so.1.2.3",
876        SonameV2 {
877            prefix: "lib".parse().unwrap(),
878            soname: Soname {
879                name: "libexample.so".parse().unwrap(),
880                version: "1.2.3".parse().ok(),
881            },
882        },
883    )]
884    #[case(
885        "lib:libexample.so.so.420",
886        SonameV2 {
887            prefix: "lib".parse().unwrap(),
888            soname: Soname {
889                name: "libexample.so.so".parse().unwrap(),
890                version: "420".parse().ok(),
891            },
892        },
893    )]
894    #[case(
895        "lib:libexample.so.test",
896        SonameV2 {
897            prefix: "lib".parse().unwrap(),
898            soname: Soname {
899                name: "libexample.so".parse().unwrap(),
900                version: "test".parse().ok(),
901            },
902        },
903    )]
904    fn sonamev2_from_string(
905        #[case] input: &str,
906        #[case] expected_result: SonameV2,
907    ) -> testresult::TestResult<()> {
908        let soname = SonameV2::from_str(input)?;
909        assert_eq!(expected_result, soname);
910        assert_eq!(input, soname.to_string());
911        Ok(())
912    }
913
914    #[rstest]
915    #[case("libexample.so.1")]
916    #[case("lib:libexample.so-abc")]
917    #[case("lib:libexample.so.10-10")]
918    #[case("lib:libexample.so.1.0.0-64")]
919    fn invalid_sonamev2_parser(#[case] input: &str) {
920        let Err(Error::ParseError(err_msg)) = SonameV2::from_str(input) else {
921            panic!("'{input}' erroneously parsed as a SonameV2")
922        };
923
924        let (test_name, _guard) = configure_insta();
925        assert_snapshot!(test_name, err_msg.to_string());
926    }
927}