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