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