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}