alpm_types/relation.rs
1use std::{
2 fmt::{Display, Formatter},
3 str::FromStr,
4};
5
6use serde::{Deserialize, Serialize};
7use winnow::{
8 ModalResult,
9 Parser,
10 ascii::{digit1, space1},
11 combinator::{
12 alt,
13 cut_err,
14 eof,
15 fail,
16 opt,
17 peek,
18 repeat,
19 repeat_till,
20 separated_pair,
21 seq,
22 terminated,
23 },
24 error::{StrContext, StrContextValue},
25 stream::Stream,
26 token::{any, rest, take_till, take_until, take_while},
27};
28
29use crate::{
30 ElfArchitectureFormat,
31 Error,
32 Name,
33 PackageVersion,
34 SharedObjectName,
35 VersionRequirement,
36};
37
38/// Provides either a [`PackageVersion`] or a [`SharedObjectName`].
39///
40/// This enum is used when creating [`SonameV1`].
41#[derive(Clone, Debug, Eq, PartialEq)]
42pub enum VersionOrSoname {
43 /// A version for a [`SonameV1`].
44 Version(PackageVersion),
45
46 /// A soname for a [`SonameV1`].
47 Soname(SharedObjectName),
48}
49
50impl FromStr for VersionOrSoname {
51 type Err = Error;
52
53 fn from_str(s: &str) -> Result<Self, Self::Err> {
54 Ok(Self::parser.parse(s)?)
55 }
56}
57
58impl VersionOrSoname {
59 /// Recognizes a [`PackageVersion`] or [`SharedObjectName`] in a string slice.
60 ///
61 /// First attempts to recognize a [`SharedObjectName`] and if that fails, falls back to
62 /// recognizing a [`PackageVersion`].
63 pub fn parser(input: &mut &str) -> ModalResult<Self> {
64 // In the following, we're doing our own `alt` implementation.
65 // The reason for this is that we build our type parsers so that they throw errors
66 // if they encounter unexpected input instead of backtracking.
67 let checkpoint = input.checkpoint();
68 let soname_result = SharedObjectName::parser.parse_next(input);
69 if soname_result.is_ok() {
70 let soname = soname_result?;
71 return Ok(VersionOrSoname::Soname(soname));
72 }
73
74 input.reset(&checkpoint);
75 let version_result = rest.and_then(PackageVersion::parser).parse_next(input);
76 if version_result.is_ok() {
77 let version = version_result?;
78 return Ok(VersionOrSoname::Version(version));
79 }
80
81 cut_err(fail)
82 .context(StrContext::Expected(StrContextValue::Description(
83 "version or shared object name",
84 )))
85 .parse_next(input)
86 }
87}
88
89impl Display for VersionOrSoname {
90 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
91 match self {
92 VersionOrSoname::Version(version) => write!(f, "{version}"),
93 VersionOrSoname::Soname(soname) => write!(f, "{soname}"),
94 }
95 }
96}
97
98/// Representation of [soname] data of a shared object based on the [alpm-sonamev1] specification.
99///
100/// Soname data may be used as [alpm-package-relation] of type _provision_ and _run-time
101/// dependency_.
102/// This type distinguishes between three forms: _basic_, _unversioned_ and _explicit_.
103///
104/// - [`SonameV1::Basic`] is used when only the `name` of a _shared object_ file is used. This form
105/// can be used in files that may contain static data about package sources (e.g. [PKGBUILD] or
106/// [SRCINFO] files).
107/// - [`SonameV1::Unversioned`] is used when the `name` of a _shared object_ file, its _soname_
108/// (which does _not_ expose a specific version) and its `architecture` (derived from the [ELF]
109/// class of the file) are used. This form can be used in files that may contain dynamic data
110/// derived from a specific package build environment (i.e. [PKGINFO]). It is discouraged to use
111/// this form in files that contain static data about package sources (e.g. [PKGBUILD] or
112/// [SRCINFO] files).
113/// - [`SonameV1::Explicit`] is used when the `name` of a _shared object_ file, the `version` from
114/// its _soname_ and its `architecture` (derived from the [ELF] class of the file) are used. This
115/// form can be used in files that may contain dynamic data derived from a specific package build
116/// environment (i.e. [PKGINFO]). It is discouraged to use this form in files that contain static
117/// data about package sources (e.g. [PKGBUILD] or [SRCINFO] files).
118///
119/// # Warning
120///
121/// This type is **deprecated** and `SonameV2` should be preferred instead!
122/// Due to the loose nature of the [alpm-sonamev1] specification, the _basic_ form overlaps with the
123/// specification of [`Name`] and the _explicit_ form overlaps with that of [`PackageRelation`].
124///
125/// # Examples
126///
127/// ```
128/// use alpm_types::{ElfArchitectureFormat, SonameV1};
129///
130/// # fn main() -> Result<(), alpm_types::Error> {
131/// let basic_soname = SonameV1::Basic("example.so".parse()?);
132/// let unversioned_soname = SonameV1::Unversioned {
133/// name: "example.so".parse()?,
134/// soname: "example.so".parse()?,
135/// architecture: ElfArchitectureFormat::Bit64,
136/// };
137/// let explicit_soname = SonameV1::Explicit {
138/// name: "example.so".parse()?,
139/// version: "1.0.0".parse()?,
140/// architecture: ElfArchitectureFormat::Bit64,
141/// };
142/// # Ok(())
143/// # }
144/// ```
145///
146/// [alpm-package-relation]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
147/// [alpm-sonamev1]: https://alpm.archlinux.page/specifications/alpm-sonamev1.7.html
148/// [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
149/// [soname]: https://en.wikipedia.org/wiki/Soname
150/// [PKGBUILD]: https://man.archlinux.org/man/PKGBUILD.5
151/// [SRCINFO]: https://alpm.archlinux.page/specifications/SRCINFO.5.html
152/// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
153#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
154pub enum SonameV1 {
155 /// Basic representation of a _shared object_ file.
156 ///
157 /// Tracks the `name` of a _shared object_ file.
158 /// This form is used when referring to _shared object_ files without their soname data.
159 ///
160 /// # Examples
161 ///
162 /// ```
163 /// use std::str::FromStr;
164 ///
165 /// use alpm_types::SonameV1;
166 ///
167 /// # fn main() -> Result<(), alpm_types::Error> {
168 /// let soname = SonameV1::from_str("example.so")?;
169 /// assert_eq!(soname, SonameV1::Basic("example.so".parse()?));
170 /// # Ok(())
171 /// # }
172 /// ```
173 Basic(SharedObjectName),
174
175 /// Unversioned representation of an ELF file's soname data.
176 ///
177 /// Tracks the `name` of a _shared object_ file, its _soname_ instead of a version and its
178 /// `architecture`. This form is used if the _soname data_ of a _shared object_ does not
179 /// expose a version.
180 ///
181 /// # Examples
182 ///
183 /// ```
184 /// use std::str::FromStr;
185 ///
186 /// use alpm_types::{ElfArchitectureFormat, SonameV1};
187 ///
188 /// # fn main() -> Result<(), alpm_types::Error> {
189 /// let soname = SonameV1::from_str("example.so=example.so-64")?;
190 /// assert_eq!(
191 /// soname,
192 /// SonameV1::Unversioned {
193 /// name: "example.so".parse()?,
194 /// soname: "example.so".parse()?,
195 /// architecture: ElfArchitectureFormat::Bit64,
196 /// }
197 /// );
198 /// # Ok(())
199 /// # }
200 /// ```
201 Unversioned {
202 /// The least specific name of the shared object file.
203 name: SharedObjectName,
204 /// The value of the shared object's _SONAME_ field in its _dynamic section_.
205 soname: SharedObjectName,
206 /// The ELF architecture format of the shared object file.
207 architecture: ElfArchitectureFormat,
208 },
209
210 /// Explicit representation of an ELF file's soname data.
211 ///
212 /// Tracks the `name` of a _shared object_ file, the `version` of its _soname_ and its
213 /// `architecture`. This form is used if the _soname data_ of a _shared object_ exposes a
214 /// specific version.
215 ///
216 /// # Examples
217 ///
218 /// ```
219 /// use std::str::FromStr;
220 ///
221 /// use alpm_types::{ElfArchitectureFormat, SonameV1};
222 ///
223 /// # fn main() -> Result<(), alpm_types::Error> {
224 /// let soname = SonameV1::from_str("example.so=1.0.0-64")?;
225 /// assert_eq!(
226 /// soname,
227 /// SonameV1::Explicit {
228 /// name: "example.so".parse()?,
229 /// version: "1.0.0".parse()?,
230 /// architecture: ElfArchitectureFormat::Bit64,
231 /// }
232 /// );
233 /// # Ok(())
234 /// # }
235 Explicit {
236 /// The least specific name of the shared object file.
237 name: SharedObjectName,
238 /// The version of the shared object file (as exposed in its _soname_ data).
239 version: PackageVersion,
240 /// The ELF architecture format of the shared object file.
241 architecture: ElfArchitectureFormat,
242 },
243}
244
245impl SonameV1 {
246 /// Creates a new [`SonameV1`].
247 ///
248 /// Depending on input, this function returns different variants of [`SonameV1`]:
249 ///
250 /// - [`SonameV1::Basic`], if both `version_or_soname` and `architecture` are [`None`]
251 /// - [`SonameV1::Unversioned`], if `version_or_soname` is [`VersionOrSoname::Soname`] and
252 /// `architecture` is [`Some`]
253 /// - [`SonameV1::Explicit`], if `version_or_soname` is [`VersionOrSoname::Version`] and
254 /// `architecture` is [`Some`]
255 ///
256 /// # Examples
257 ///
258 /// ```
259 /// use alpm_types::{ElfArchitectureFormat, SonameV1};
260 ///
261 /// # fn main() -> Result<(), alpm_types::Error> {
262 /// let basic_soname = SonameV1::new("example.so".parse()?, None, None)?;
263 /// assert_eq!(basic_soname, SonameV1::Basic("example.so".parse()?));
264 ///
265 /// let unversioned_soname = SonameV1::new(
266 /// "example.so".parse()?,
267 /// Some("example.so".parse()?),
268 /// Some(ElfArchitectureFormat::Bit64),
269 /// )?;
270 /// assert_eq!(
271 /// unversioned_soname,
272 /// SonameV1::Unversioned {
273 /// name: "example.so".parse()?,
274 /// soname: "example.so".parse()?,
275 /// architecture: "64".parse()?
276 /// }
277 /// );
278 ///
279 /// let explicit_soname = SonameV1::new(
280 /// "example.so".parse()?,
281 /// Some("1.0.0".parse()?),
282 /// Some(ElfArchitectureFormat::Bit64),
283 /// )?;
284 /// assert_eq!(
285 /// explicit_soname,
286 /// SonameV1::Explicit {
287 /// name: "example.so".parse()?,
288 /// version: "1.0.0".parse()?,
289 /// architecture: "64".parse()?
290 /// }
291 /// );
292 /// # Ok(())
293 /// # }
294 /// ```
295 pub fn new(
296 name: SharedObjectName,
297 version_or_soname: Option<VersionOrSoname>,
298 architecture: Option<ElfArchitectureFormat>,
299 ) -> Result<Self, Error> {
300 match (version_or_soname, architecture) {
301 (None, None) => Ok(Self::Basic(name)),
302 (Some(VersionOrSoname::Version(version)), Some(architecture)) => Ok(Self::Explicit {
303 name,
304 version,
305 architecture,
306 }),
307 (Some(VersionOrSoname::Soname(soname)), Some(architecture)) => Ok(Self::Unversioned {
308 name,
309 soname,
310 architecture,
311 }),
312 (None, Some(_)) => Err(Error::InvalidSonameV1(
313 "SonameV1 needs a version when specifying architecture",
314 )),
315 (Some(_), None) => Err(Error::InvalidSonameV1(
316 "SonameV1 needs an architecture when specifying version",
317 )),
318 }
319 }
320
321 /// Parses a [`SonameV1`] from a string slice.
322 pub fn parser(input: &mut &str) -> ModalResult<Self> {
323 // Parse the shared object name.
324 let name = Self::parse_shared_object_name(input)?;
325
326 // Parse the version delimiter `=`.
327 //
328 // If it doesn't exist, it is the basic form.
329 if Self::parse_version_delimiter(input).is_err() {
330 return Ok(SonameV1::Basic(name));
331 }
332
333 // Take all input until we hit the delimiter and architecture.
334 let (raw_version_or_soname, _): (String, _) =
335 cut_err(repeat_till(1.., any, peek(("-", digit1, eof))))
336 .context(StrContext::Expected(StrContextValue::Description(
337 "a version or shared object name, followed by an ELF architecture format",
338 )))
339 .parse_next(input)?;
340
341 // Two cases are possible here:
342 //
343 // 1. Unversioned: `name=soname-architecture`
344 // 2. Explicit: `name=version-architecture`
345 let version_or_soname =
346 VersionOrSoname::parser.parse_next(&mut raw_version_or_soname.as_str())?;
347
348 // Parse the `-` delimiter
349 Self::parse_architecture_delimiter(input)?;
350
351 // Parse the architecture
352 let architecture = Self::parse_architecture(input)?;
353
354 match version_or_soname {
355 VersionOrSoname::Version(version) => Ok(SonameV1::Explicit {
356 name,
357 version,
358 architecture,
359 }),
360 VersionOrSoname::Soname(soname) => Ok(SonameV1::Unversioned {
361 name,
362 soname,
363 architecture,
364 }),
365 }
366 }
367
368 /// Parses the shared object name until the version delimiter `=`.
369 fn parse_shared_object_name(input: &mut &str) -> ModalResult<SharedObjectName> {
370 repeat_till(1.., any, peek(alt(("=", eof))))
371 .try_map(|(name, _): (String, &str)| SharedObjectName::from_str(&name))
372 .context(StrContext::Label("shared object name"))
373 .parse_next(input)
374 }
375
376 /// Parses the version delimiter `=`.
377 ///
378 /// This function discards the result for only checking if the version delimiter is present.
379 fn parse_version_delimiter(input: &mut &str) -> ModalResult<()> {
380 cut_err("=")
381 .context(StrContext::Label("version delimiter"))
382 .context(StrContext::Expected(StrContextValue::Description(
383 "version delimiter `=`",
384 )))
385 .parse_next(input)
386 .map(|_| ())
387 }
388
389 /// Parses the architecture delimiter `-`.
390 fn parse_architecture_delimiter(input: &mut &str) -> ModalResult<()> {
391 cut_err("-")
392 .context(StrContext::Label("architecture delimiter"))
393 .context(StrContext::Expected(StrContextValue::Description(
394 "architecture delimiter `-`",
395 )))
396 .parse_next(input)
397 .map(|_| ())
398 }
399
400 /// Parses the architecture.
401 fn parse_architecture(input: &mut &str) -> ModalResult<ElfArchitectureFormat> {
402 cut_err(take_while(1.., |c: char| c.is_ascii_digit()))
403 .try_map(ElfArchitectureFormat::from_str)
404 .context(StrContext::Label("architecture"))
405 .parse_next(input)
406 }
407}
408
409impl FromStr for SonameV1 {
410 type Err = Error;
411 /// Parses a [`SonameV1`] from a string slice.
412 ///
413 /// The string slice must be in the format `name[=version-architecture]`.
414 ///
415 /// # Errors
416 ///
417 /// Returns an error if a [`SonameV1`] can not be parsed from input.
418 ///
419 /// # Examples
420 ///
421 /// ```
422 /// use std::str::FromStr;
423 ///
424 /// use alpm_types::{ElfArchitectureFormat, SonameV1};
425 ///
426 /// # fn main() -> Result<(), alpm_types::Error> {
427 /// assert_eq!(
428 /// SonameV1::from_str("example.so=1.0.0-64")?,
429 /// SonameV1::Explicit {
430 /// name: "example.so".parse()?,
431 /// version: "1.0.0".parse()?,
432 /// architecture: ElfArchitectureFormat::Bit64,
433 /// },
434 /// );
435 /// assert_eq!(
436 /// SonameV1::from_str("example.so=example.so-64")?,
437 /// SonameV1::Unversioned {
438 /// name: "example.so".parse()?,
439 /// soname: "example.so".parse()?,
440 /// architecture: ElfArchitectureFormat::Bit64,
441 /// },
442 /// );
443 /// assert_eq!(
444 /// SonameV1::from_str("example.so")?,
445 /// SonameV1::Basic("example.so".parse()?),
446 /// );
447 /// # Ok(())
448 /// # }
449 /// ```
450 fn from_str(s: &str) -> Result<Self, Self::Err> {
451 Ok(Self::parser.parse(s)?)
452 }
453}
454
455impl Display for SonameV1 {
456 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
457 match self {
458 Self::Basic(name) => write!(f, "{name}"),
459 Self::Unversioned {
460 name,
461 soname,
462 architecture,
463 } => write!(f, "{name}={soname}-{architecture}"),
464 Self::Explicit {
465 name,
466 version,
467 architecture,
468 } => write!(f, "{name}={version}-{architecture}"),
469 }
470 }
471}
472
473/// A prefix associated with a library lookup directory.
474///
475/// Library lookup directories are used when detecting shared object files on a system.
476/// Each such lookup directory can be assigned to a _prefix_, which allows identifying them in other
477/// contexts. E.g. `lib` may serve as _prefix_ for the lookup directory `/usr/lib`.
478///
479/// This is a type alias for [`Name`].
480pub type SharedLibraryPrefix = Name;
481
482/// The value of a shared object's _soname_.
483///
484/// This data may be present in the _SONAME_ or _NEEDED_ fields of a shared object's _dynamic
485/// section_.
486///
487/// The _soname_ data may contain only a shared object name (e.g. `libexample.so`) or a shared
488/// object name, that also encodes version information (e.g. `libexample.so.1`).
489#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
490pub struct Soname {
491 /// The name part of a shared object's _soname_.
492 pub name: SharedObjectName,
493 /// The optional version part of a shared object's _soname_.
494 pub version: Option<PackageVersion>,
495}
496
497impl Soname {
498 /// Creates a new [`Soname`].
499 pub fn new(name: SharedObjectName, version: Option<PackageVersion>) -> Self {
500 Self { name, version }
501 }
502
503 /// Recognizes a [`Soname`] in a string slice.
504 ///
505 /// The passed data can be in the following formats:
506 ///
507 /// - `<name>.so`: A shared object name without a version. (e.g. `libexample.so`)
508 /// - `<name>.so.<version>`: A shared object name with a version. (e.g. `libexample.so.1`)
509 /// - The version must be a valid [`PackageVersion`].
510 pub fn parser(input: &mut &str) -> ModalResult<Self> {
511 let name = cut_err(
512 (
513 // Parse the name of the shared object until eof or the `.so` is hit.
514 repeat_till::<_, _, String, _, _, _, _>(1.., any, peek(alt((".so", eof)))),
515 // Parse at least one or more `.so` suffix(es).
516 cut_err(repeat::<_, _, String, _, _>(1.., ".so"))
517 .context(StrContext::Label("suffix"))
518 .context(StrContext::Expected(StrContextValue::Description(
519 "shared object name suffix '.so'",
520 ))),
521 )
522 // Take both parts and map them onto a SharedObjectName
523 .take()
524 .and_then(Name::parser)
525 .map(SharedObjectName),
526 )
527 .context(StrContext::Label("shared object name"))
528 .parse_next(input)?;
529
530 // Parse the version delimiter.
531 let delimiter = cut_err(alt((".", eof)))
532 .context(StrContext::Label("version delimiter"))
533 .context(StrContext::Expected(StrContextValue::Description(
534 "version delimiter `.`",
535 )))
536 .parse_next(input)?;
537
538 // If a `.` is found, map the rest of the string to a version.
539 // Otherwise, we hit the `eof` and there's no version.
540 let version = match delimiter {
541 "" => None,
542 "." => Some(rest.and_then(PackageVersion::parser).parse_next(input)?),
543 _ => unreachable!(),
544 };
545
546 Ok(Self { name, version })
547 }
548}
549
550impl Display for Soname {
551 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
552 match &self.version {
553 Some(version) => write!(f, "{name}.{version}", name = self.name),
554 None => write!(f, "{name}", name = self.name),
555 }
556 }
557}
558
559impl FromStr for Soname {
560 type Err = Error;
561
562 /// Recognizes a [`Soname`] in a string slice.
563 ///
564 /// The string slice must be in the format of `<name>.so` or `<name>.so.<version>`.
565 ///
566 /// # Errors
567 ///
568 /// Returns an error if a [`Soname`] can not be parsed from input.
569 ///
570 /// # Examples
571 ///
572 /// ```
573 /// use std::str::FromStr;
574 ///
575 /// use alpm_types::Soname;
576 /// # fn main() -> Result<(), alpm_types::Error> {
577 /// assert_eq!(
578 /// Soname::from_str("libexample.so.1")?,
579 /// Soname::new("libexample.so".parse()?, Some("1".parse()?)),
580 /// );
581 /// assert_eq!(
582 /// Soname::from_str("libexample.so")?,
583 /// Soname::new("libexample.so".parse()?, None),
584 /// );
585 /// # Ok(())
586 /// # }
587 /// ```
588 fn from_str(s: &str) -> Result<Self, Self::Err> {
589 Ok(Self::parser.parse(s)?)
590 }
591}
592
593/// Representation of [soname] data of a shared object based on the [alpm-sonamev2] specification.
594///
595/// Soname data may be used as [alpm-package-relation] of type _provision_ or _run-time dependency_
596/// in [`PackageInfoV1`] and [`PackageInfoV2`]. The data consists of the arbitrarily
597/// defined `prefix`, which denotes the use name of a specific library directory, and the `soname`,
598/// which refers to the value of either the _SONAME_ or a _NEEDED_ field in the _dynamic section_ of
599/// an [ELF] file.
600///
601/// # Examples
602///
603/// This example assumpes that `lib` is used as the `prefix` for the library directory `/usr/lib`
604/// and the following files are contained in it:
605///
606/// ```bash
607/// /usr/lib/libexample.so -> libexample.so.1
608/// /usr/lib/libexample.so.1 -> libexample.so.1.0.0
609/// /usr/lib/libexample.so.1.0.0
610/// ```
611///
612/// The above file `/usr/lib/libexample.so.1.0.0` represents an [ELF] file, that exposes
613/// `libexample.so.1` as value of the _SONAME_ field in its _dynamic section_. This data can be
614/// represented as follows, using [`SonameV2`]:
615///
616/// ```rust
617/// use alpm_types::{Soname, SonameV2};
618///
619/// # fn main() -> Result<(), alpm_types::Error> {
620/// let soname_data = SonameV2 {
621/// prefix: "lib".parse()?,
622/// soname: Soname {
623/// name: "libexample.so".parse()?,
624/// version: Some("1".parse()?),
625/// },
626/// };
627/// assert_eq!(soname_data.to_string(), "lib:libexample.so.1");
628/// # Ok(())
629/// # }
630/// ```
631///
632/// [alpm-sonamev2]: https://alpm.archlinux.page/specifications/alpm-sonamev2.7.html
633/// [alpm-package-relation]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
634/// [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
635/// [soname]: https://en.wikipedia.org/wiki/Soname
636/// [`PackageInfoV1`]: https://docs.rs/alpm_pkginfo/latest/alpm_pkginfo/struct.PackageInfoV1.html
637/// [`PackageInfoV2`]: https://docs.rs/alpm_pkginfo/latest/alpm_pkginfo/struct.PackageInfoV2.html
638#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
639pub struct SonameV2 {
640 /// The directory prefix of the shared object file.
641 pub prefix: SharedLibraryPrefix,
642 /// The _soname_ of a shared object file.
643 pub soname: Soname,
644}
645
646impl SonameV2 {
647 /// Creates a new [`SonameV2`].
648 ///
649 /// # Examples
650 ///
651 /// ```
652 /// use alpm_types::SonameV2;
653 ///
654 /// # fn main() -> Result<(), alpm_types::Error> {
655 /// SonameV2::new("lib".parse()?, "libexample.so.1".parse()?);
656 /// # Ok(())
657 /// # }
658 /// ```
659 pub fn new(prefix: SharedLibraryPrefix, soname: Soname) -> Self {
660 Self { prefix, soname }
661 }
662
663 /// Recognizes a [`SonameV2`] in a string slice.
664 ///
665 /// The passed data must be in the format `<prefix>:<soname>`. (e.g. `lib:libexample.so.1`)
666 ///
667 /// See [`Soname::parser`] for details on the format of `<soname>`.
668 ///
669 /// # Errors
670 ///
671 /// Returns an error if no [`SonameV2`] can be created from `input`.
672 pub fn parser(input: &mut &str) -> ModalResult<Self> {
673 // Parse everything from the start to the first `:` and parse as `SharedLibraryPrefix`.
674 let prefix = cut_err(
675 repeat_till(1.., any, peek(alt((":", eof))))
676 .try_map(|(name, _): (String, &str)| SharedLibraryPrefix::from_str(&name)),
677 )
678 .context(StrContext::Label("prefix for a shared object lookup path"))
679 .parse_next(input)?;
680
681 cut_err(":")
682 .context(StrContext::Label("shared library prefix delimiter"))
683 .context(StrContext::Expected(StrContextValue::Description(
684 "shared library prefix `:`",
685 )))
686 .parse_next(input)?;
687
688 let soname = Soname::parser.parse_next(input)?;
689
690 Ok(Self { prefix, soname })
691 }
692}
693
694impl FromStr for SonameV2 {
695 type Err = Error;
696
697 /// Parses a [`SonameV2`] from a string slice.
698 ///
699 /// The string slice must be in the format `<prefix>:<soname>`.
700 ///
701 /// # Errors
702 ///
703 /// Returns an error if a [`SonameV2`] can not be parsed from input.
704 ///
705 /// # Examples
706 ///
707 /// ```
708 /// use std::str::FromStr;
709 ///
710 /// use alpm_types::{Soname, SonameV2};
711 ///
712 /// # fn main() -> Result<(), alpm_types::Error> {
713 /// assert_eq!(
714 /// SonameV2::from_str("lib:libexample.so.1")?,
715 /// SonameV2::new(
716 /// "lib".parse()?,
717 /// Soname::new("libexample.so".parse()?, Some("1".parse()?))
718 /// ),
719 /// );
720 /// # Ok(())
721 /// # }
722 /// ```
723 fn from_str(s: &str) -> Result<Self, Self::Err> {
724 Ok(Self::parser.parse(s)?)
725 }
726}
727
728impl Display for SonameV2 {
729 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
730 write!(
731 f,
732 "{prefix}:{soname}",
733 prefix = self.prefix,
734 soname = self.soname
735 )
736 }
737}
738
739/// A package relation
740///
741/// Describes a relation to a component.
742/// Package relations may either consist of only a [`Name`] *or* of a [`Name`] and a
743/// [`VersionRequirement`].
744///
745/// ## Note
746///
747/// A [`PackageRelation`] covers all [alpm-package-relations] *except* optional
748/// dependencies, as those behave differently.
749///
750/// [alpm-package-relations]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
751#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
752pub struct PackageRelation {
753 /// The name of the package
754 pub name: Name,
755 /// The version requirement for the package
756 pub version_requirement: Option<VersionRequirement>,
757}
758
759impl PackageRelation {
760 /// Creates a new [`PackageRelation`]
761 ///
762 /// # Examples
763 ///
764 /// ```
765 /// use alpm_types::{PackageRelation, VersionComparison, VersionRequirement};
766 ///
767 /// # fn main() -> Result<(), alpm_types::Error> {
768 /// PackageRelation::new(
769 /// "example".parse()?,
770 /// Some(VersionRequirement {
771 /// comparison: VersionComparison::Less,
772 /// version: "1.0.0".parse()?,
773 /// }),
774 /// );
775 ///
776 /// PackageRelation::new("example".parse()?, None);
777 /// # Ok(())
778 /// # }
779 /// ```
780 pub fn new(name: Name, version_requirement: Option<VersionRequirement>) -> Self {
781 Self {
782 name,
783 version_requirement,
784 }
785 }
786
787 /// Parses a [`PackageRelation`] from a string slice.
788 ///
789 /// Consumes all of its input.
790 ///
791 /// # Examples
792 ///
793 /// See [`Self::from_str`] for code examples.
794 ///
795 /// # Errors
796 ///
797 /// Returns an error if `input` is not a valid _package-relation_.
798 pub fn parser(input: &mut &str) -> ModalResult<Self> {
799 seq!(Self {
800 name: take_till(1.., ('<', '>', '=')).and_then(Name::parser).context(StrContext::Label("package name")),
801 version_requirement: opt(VersionRequirement::parser),
802 _: eof.context(StrContext::Expected(StrContextValue::Description("end of relation version requirement"))),
803 })
804 .parse_next(input)
805 }
806}
807
808impl Display for PackageRelation {
809 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
810 if let Some(version_requirement) = self.version_requirement.as_ref() {
811 write!(f, "{}{}", self.name, version_requirement)
812 } else {
813 write!(f, "{}", self.name)
814 }
815 }
816}
817
818impl FromStr for PackageRelation {
819 type Err = Error;
820 /// Parses a [`PackageRelation`] from a string slice.
821 ///
822 /// Delegates to [`PackageRelation::parser`].
823 ///
824 /// # Errors
825 ///
826 /// Returns an error if [`PackageRelation::parser`] fails.
827 ///
828 /// # Examples
829 ///
830 /// ```
831 /// use std::str::FromStr;
832 ///
833 /// use alpm_types::{PackageRelation, VersionComparison, VersionRequirement};
834 ///
835 /// # fn main() -> Result<(), alpm_types::Error> {
836 /// assert_eq!(
837 /// PackageRelation::from_str("example<1.0.0")?,
838 /// PackageRelation::new(
839 /// "example".parse()?,
840 /// Some(VersionRequirement {
841 /// comparison: VersionComparison::Less,
842 /// version: "1.0.0".parse()?
843 /// })
844 /// ),
845 /// );
846 ///
847 /// assert_eq!(
848 /// PackageRelation::from_str("example<=1.0.0")?,
849 /// PackageRelation::new(
850 /// "example".parse()?,
851 /// Some(VersionRequirement {
852 /// comparison: VersionComparison::LessOrEqual,
853 /// version: "1.0.0".parse()?
854 /// })
855 /// ),
856 /// );
857 ///
858 /// assert_eq!(
859 /// PackageRelation::from_str("example=1.0.0")?,
860 /// PackageRelation::new(
861 /// "example".parse()?,
862 /// Some(VersionRequirement {
863 /// comparison: VersionComparison::Equal,
864 /// version: "1.0.0".parse()?
865 /// })
866 /// ),
867 /// );
868 ///
869 /// assert_eq!(
870 /// PackageRelation::from_str("example>1.0.0")?,
871 /// PackageRelation::new(
872 /// "example".parse()?,
873 /// Some(VersionRequirement {
874 /// comparison: VersionComparison::Greater,
875 /// version: "1.0.0".parse()?
876 /// })
877 /// ),
878 /// );
879 ///
880 /// assert_eq!(
881 /// PackageRelation::from_str("example>=1.0.0")?,
882 /// PackageRelation::new(
883 /// "example".parse()?,
884 /// Some(VersionRequirement {
885 /// comparison: VersionComparison::GreaterOrEqual,
886 /// version: "1.0.0".parse()?
887 /// })
888 /// ),
889 /// );
890 ///
891 /// assert_eq!(
892 /// PackageRelation::from_str("example")?,
893 /// PackageRelation::new("example".parse()?, None),
894 /// );
895 ///
896 /// assert!(PackageRelation::from_str("example<").is_err());
897 /// # Ok(())
898 /// # }
899 /// ```
900 fn from_str(s: &str) -> Result<Self, Self::Err> {
901 Ok(Self::parser.parse(s)?)
902 }
903}
904
905/// An optional dependency for a package.
906///
907/// This type is used for representing dependencies that are not essential for base functionality
908/// of a package, but may be necessary to make use of certain features of a package.
909///
910/// An [`OptionalDependency`] consists of a package relation and an optional description separated
911/// by a colon (`:`).
912///
913/// - The package relation component must be a valid [`PackageRelation`].
914/// - If a description is provided it must be at least one character long.
915///
916/// Refer to [alpm-package-relation] of type [optional dependency] for details on the format.
917/// ## Examples
918///
919/// ```
920/// use std::str::FromStr;
921///
922/// use alpm_types::{Name, OptionalDependency};
923///
924/// # fn main() -> Result<(), alpm_types::Error> {
925/// // Create OptionalDependency from &str
926/// let opt_depend = OptionalDependency::from_str("example: this is an example dependency")?;
927///
928/// // Get the name
929/// assert_eq!("example", opt_depend.name().as_ref());
930///
931/// // Get the description
932/// assert_eq!(
933/// Some("this is an example dependency"),
934/// opt_depend.description().as_deref()
935/// );
936///
937/// // Format as String
938/// assert_eq!(
939/// "example: this is an example dependency",
940/// format!("{opt_depend}")
941/// );
942/// # Ok(())
943/// # }
944/// ```
945///
946/// [alpm-package-relation]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
947/// [optional dependency]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html#optional-dependency
948#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
949pub struct OptionalDependency {
950 package_relation: PackageRelation,
951 description: Option<String>,
952}
953
954impl OptionalDependency {
955 /// Create a new OptionalDependency in a Result
956 pub fn new(
957 package_relation: PackageRelation,
958 description: Option<String>,
959 ) -> OptionalDependency {
960 OptionalDependency {
961 package_relation,
962 description,
963 }
964 }
965
966 /// Return the name of the optional dependency
967 pub fn name(&self) -> &Name {
968 &self.package_relation.name
969 }
970
971 /// Return the version requirement of the optional dependency
972 pub fn version_requirement(&self) -> &Option<VersionRequirement> {
973 &self.package_relation.version_requirement
974 }
975
976 /// Return the description for the optional dependency, if it exists
977 pub fn description(&self) -> &Option<String> {
978 &self.description
979 }
980
981 /// Recognizes an [`OptionalDependency`] in a string slice.
982 ///
983 /// Consumes all of its input.
984 ///
985 /// # Errors
986 ///
987 /// Returns an error if `input` is not a valid _alpm-package-relation_ of type _optional
988 /// dependency_.
989 pub fn parser(input: &mut &str) -> ModalResult<Self> {
990 let description_parser = terminated(
991 // Descriptions may consist of any character except '\n' and '\r'.
992 // Descriptions are a also at the end of a `OptionalDependency`.
993 // We enforce forbidding `\n` and `\r` by only taking until either of them
994 // is hit and checking for `eof` afterwards.
995 // This will **always** succeed unless `\n` and `\r` are hit, in which case an
996 // error is thrown.
997 take_till(0.., ('\n', '\r')),
998 eof,
999 )
1000 .context(StrContext::Label("optional dependency description"))
1001 .context(StrContext::Expected(StrContextValue::Description(
1002 r"no carriage returns or newlines",
1003 )))
1004 .map(|d: &str| match d.trim_ascii() {
1005 "" => None,
1006 t => Some(t.to_string()),
1007 });
1008
1009 let (package_relation, description) = alt((
1010 // look for a ":" followed by at least one whitespace, then dispatch either side to the
1011 // relevant parser without allowing backtracking.
1012 separated_pair(
1013 take_until(1.., ":").and_then(cut_err(PackageRelation::parser)),
1014 (":", space1),
1015 rest.and_then(cut_err(description_parser)),
1016 ),
1017 // if we can't find ": ", then assume it's all PackageRelation
1018 // and assert we've reached the end of input
1019 (rest.and_then(PackageRelation::parser), eof.value(None)),
1020 ))
1021 .parse_next(input)?;
1022
1023 Ok(Self {
1024 package_relation,
1025 description,
1026 })
1027 }
1028}
1029
1030impl FromStr for OptionalDependency {
1031 type Err = Error;
1032
1033 /// Creates a new [`OptionalDependency`] from a string slice.
1034 ///
1035 /// Delegates to [`OptionalDependency::parser`].
1036 ///
1037 /// # Errors
1038 ///
1039 /// Returns an error if [`OptionalDependency::parser`] fails.
1040 fn from_str(s: &str) -> Result<Self, Self::Err> {
1041 Ok(Self::parser.parse(s)?)
1042 }
1043}
1044
1045impl Display for OptionalDependency {
1046 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
1047 match self.description {
1048 Some(ref description) => write!(fmt, "{}: {}", self.package_relation, description),
1049 None => write!(fmt, "{}", self.package_relation),
1050 }
1051 }
1052}
1053
1054/// Group of a package
1055///
1056/// Represents an arbitrary collection of packages that share a common
1057/// characteristic or functionality.
1058///
1059/// While group names can be any valid UTF-8 string, it is recommended to follow
1060/// the format of [`Name`] (`[a-z\d\-._@+]` but must not start with `[-.]`)
1061/// to ensure consistency and ease of use.
1062///
1063/// This is a type alias for [`String`].
1064///
1065/// ## Examples
1066/// ```
1067/// use alpm_types::Group;
1068///
1069/// // Create a Group
1070/// let group: Group = "package-group".to_string();
1071/// ```
1072pub type Group = String;
1073
1074/// Provides either a [`PackageRelation`], a [`SonameV1`] or a [`SonameV2`].
1075///
1076/// This enum is used for [alpm-package-relations] of type _run-time dependency_ and _provision_
1077/// e.g. in [PKGINFO], [SRCINFO] or [alpm-db-desc] files.
1078///
1079/// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
1080/// [SRCINFO]: https://alpm.archlinux.page/specifications/SRCINFO.5.html
1081/// [alpm-db-desc]: https://alpm.archlinux.page/specifications/alpm-db-desc.5.html
1082/// [alpm-package-relations]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
1083#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1084#[serde(untagged)]
1085pub enum RelationOrSoname {
1086 /// A package relation (as [`PackageRelation`]).
1087 Relation(PackageRelation),
1088 /// A shared object name following [alpm-sonamev1].
1089 ///
1090 /// [alpm-sonamev1]: https://alpm.archlinux.page/specifications/alpm-sonamev1.7.html
1091 SonameV1(SonameV1),
1092 /// A shared object name following [alpm-sonamev2].
1093 ///
1094 /// [alpm-sonamev2]: https://alpm.archlinux.page/specifications/alpm-sonamev2.7.html
1095 SonameV2(SonameV2),
1096}
1097
1098impl PartialEq<PackageRelation> for RelationOrSoname {
1099 fn eq(&self, other: &PackageRelation) -> bool {
1100 self.to_string() == other.to_string()
1101 }
1102}
1103
1104impl PartialEq<SonameV1> for RelationOrSoname {
1105 fn eq(&self, other: &SonameV1) -> bool {
1106 self.to_string() == other.to_string()
1107 }
1108}
1109
1110impl PartialEq<SonameV2> for RelationOrSoname {
1111 fn eq(&self, other: &SonameV2) -> bool {
1112 self.to_string() == other.to_string()
1113 }
1114}
1115
1116impl RelationOrSoname {
1117 /// Recognizes a [`SonameV2`], a [`SonameV1`] or a [`PackageRelation`] in a string slice.
1118 ///
1119 /// First attempts to recognize a [`SonameV2`], then a [`SonameV1`] and if that fails, falls
1120 /// back to recognizing a [`PackageRelation`].
1121 /// Depending on recognized type, a [`RelationOrSoname`] is created accordingly.
1122 pub fn parser(input: &mut &str) -> ModalResult<Self> {
1123 // Implement a custom `winnow::combinator::alt`, as all type parsers are built in
1124 // such a way that they return errors on unexpected input instead of backtracking.
1125 let checkpoint = input.checkpoint();
1126 let sonamev2_result = SonameV2::parser.parse_next(input);
1127 if sonamev2_result.is_ok() {
1128 let sonamev2 = sonamev2_result?;
1129 return Ok(RelationOrSoname::SonameV2(sonamev2));
1130 }
1131
1132 input.reset(&checkpoint);
1133 let sonamev1_result = SonameV1::parser.parse_next(input);
1134 if sonamev1_result.is_ok() {
1135 let sonamev1 = sonamev1_result?;
1136 return Ok(RelationOrSoname::SonameV1(sonamev1));
1137 }
1138
1139 input.reset(&checkpoint);
1140 let relation_result = rest.and_then(PackageRelation::parser).parse_next(input);
1141 if relation_result.is_ok() {
1142 let relation = relation_result?;
1143 return Ok(RelationOrSoname::Relation(relation));
1144 }
1145
1146 cut_err(fail)
1147 .context(StrContext::Expected(StrContextValue::Description(
1148 "alpm-sonamev2, alpm-sonamev1 or alpm-package-relation",
1149 )))
1150 .parse_next(input)
1151 }
1152}
1153
1154impl Display for RelationOrSoname {
1155 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1156 match self {
1157 RelationOrSoname::Relation(version) => write!(f, "{version}"),
1158 RelationOrSoname::SonameV1(soname) => write!(f, "{soname}"),
1159 RelationOrSoname::SonameV2(soname) => write!(f, "{soname}"),
1160 }
1161 }
1162}
1163
1164impl FromStr for RelationOrSoname {
1165 type Err = Error;
1166
1167 /// Creates a [`RelationOrSoname`] from a string slice.
1168 ///
1169 /// Relies on [`RelationOrSoname::parser`] to recognize types in `input` and create a
1170 /// [`RelationOrSoname`] accordingly.
1171 ///
1172 /// # Errors
1173 ///
1174 /// Returns an error if no [`RelationOrSoname`] can be created from `input`.
1175 ///
1176 /// # Examples
1177 ///
1178 /// ```
1179 /// use alpm_types::{PackageRelation, RelationOrSoname, SonameV1, SonameV2};
1180 ///
1181 /// # fn main() -> Result<(), alpm_types::Error> {
1182 /// let relation: RelationOrSoname = "example=1.0.0".parse()?;
1183 /// assert_eq!(
1184 /// relation,
1185 /// RelationOrSoname::Relation(PackageRelation::new(
1186 /// "example".parse()?,
1187 /// Some("=1.0.0".parse()?)
1188 /// ))
1189 /// );
1190 ///
1191 /// let sonamev2: RelationOrSoname = "lib:example.so.1".parse()?;
1192 /// assert_eq!(
1193 /// sonamev2,
1194 /// RelationOrSoname::SonameV2(SonameV2::new("lib".parse()?, "example.so.1".parse()?))
1195 /// );
1196 ///
1197 /// let sonamev1: RelationOrSoname = "example.so".parse()?;
1198 /// assert_eq!(
1199 /// sonamev1,
1200 /// RelationOrSoname::SonameV1(SonameV1::new("example.so".parse()?, None, None)?)
1201 /// );
1202 /// # Ok(())
1203 /// # }
1204 /// ```
1205 fn from_str(s: &str) -> Result<Self, Self::Err> {
1206 Self::parser
1207 .parse(s)
1208 .map_err(|error| Error::ParseError(error.to_string()))
1209 }
1210}
1211
1212#[cfg(test)]
1213mod tests {
1214 use proptest::{prop_assert_eq, proptest, test_runner::Config as ProptestConfig};
1215 use rstest::rstest;
1216 use testresult::TestResult;
1217
1218 use super::*;
1219 use crate::VersionComparison;
1220
1221 const COMPARATOR_REGEX: &str = r"(<|<=|=|>=|>)";
1222 /// NOTE: [`Epoch`][alpm_types::Epoch] is implicitly constrained by [`std::usize::MAX`].
1223 /// However, it's unrealistic to ever reach that many forced downgrades for a package, hence
1224 /// we don't test that fully
1225 const EPOCH_REGEX: &str = r"[1-9]{1}[0-9]{0,10}";
1226 const NAME_REGEX: &str = r"[a-z0-9_@+]+[a-z0-9\-._@+]*";
1227 const PKGREL_REGEX: &str = r"[1-9][0-9]{0,8}(|[.][1-9][0-9]{0,8})";
1228 const PKGVER_REGEX: &str = r"([[:alnum:]][[:alnum:]_+.]*)";
1229 const DESCRIPTION_REGEX: &str = "[^\n\r]*";
1230
1231 proptest! {
1232 #![proptest_config(ProptestConfig::with_cases(1000))]
1233
1234
1235 #[test]
1236 fn valid_package_relation_from_str(s in format!("{NAME_REGEX}(|{COMPARATOR_REGEX}(|{EPOCH_REGEX}:){PKGVER_REGEX}(|-{PKGREL_REGEX}))").as_str()) {
1237 println!("s: {s}");
1238 let name = PackageRelation::from_str(&s).unwrap();
1239 prop_assert_eq!(s, format!("{}", name));
1240 }
1241 }
1242
1243 proptest! {
1244 #[test]
1245 fn opt_depend_from_str(
1246 name in NAME_REGEX,
1247 desc in DESCRIPTION_REGEX,
1248 use_desc in proptest::bool::ANY
1249 ) {
1250 let desc_trimmed = desc.trim_ascii();
1251 let desc_is_blank = desc_trimmed.is_empty();
1252
1253 let (raw_in, formatted_expected) = if use_desc {
1254 // Raw input and expected formatted output.
1255 // These are different because `desc` will be trimmed by the parser;
1256 // if it is *only* ascii whitespace then it will be skipped altogether.
1257 (
1258 format!("{name}: {desc}"),
1259 if !desc_is_blank {
1260 format!("{name}: {desc_trimmed}")
1261 } else {
1262 name.clone()
1263 }
1264 )
1265 } else {
1266 (name.clone(), name.clone())
1267 };
1268
1269 println!("input string: {raw_in}");
1270 let opt_depend = OptionalDependency::from_str(&raw_in).unwrap();
1271 let formatted_actual = format!("{opt_depend}");
1272 prop_assert_eq!(
1273 formatted_expected,
1274 formatted_actual,
1275 "Formatted output doesn't match input"
1276 );
1277 }
1278 }
1279
1280 #[rstest]
1281 #[case(
1282 "python>=3",
1283 Ok(PackageRelation {
1284 name: Name::new("python").unwrap(),
1285 version_requirement: Some(VersionRequirement {
1286 comparison: VersionComparison::GreaterOrEqual,
1287 version: "3".parse().unwrap(),
1288 }),
1289 }),
1290 )]
1291 #[case(
1292 "java-environment>=17",
1293 Ok(PackageRelation {
1294 name: Name::new("java-environment").unwrap(),
1295 version_requirement: Some(VersionRequirement {
1296 comparison: VersionComparison::GreaterOrEqual,
1297 version: "17".parse().unwrap(),
1298 }),
1299 }),
1300 )]
1301 fn valid_package_relation(
1302 #[case] input: &str,
1303 #[case] expected: Result<PackageRelation, Error>,
1304 ) {
1305 assert_eq!(PackageRelation::from_str(input), expected);
1306 }
1307
1308 #[rstest]
1309 #[case(
1310 "example: this is an example dependency",
1311 OptionalDependency {
1312 package_relation: PackageRelation {
1313 name: Name::new("example").unwrap(),
1314 version_requirement: None,
1315 },
1316 description: Some("this is an example dependency".to_string()),
1317 },
1318 )]
1319 #[case(
1320 "example-two: a description with lots of whitespace padding ",
1321 OptionalDependency {
1322 package_relation: PackageRelation {
1323 name: Name::new("example-two").unwrap(),
1324 version_requirement: None,
1325 },
1326 description: Some("a description with lots of whitespace padding".to_string())
1327 },
1328 )]
1329 #[case(
1330 "dep_name",
1331 OptionalDependency {
1332 package_relation: PackageRelation {
1333 name: Name::new("dep_name").unwrap(),
1334 version_requirement: None,
1335 },
1336 description: None,
1337 },
1338 )]
1339 #[case(
1340 "dep_name: ",
1341 OptionalDependency {
1342 package_relation: PackageRelation {
1343 name: Name::new("dep_name").unwrap(),
1344 version_requirement: None,
1345 },
1346 description: None,
1347 },
1348 )]
1349 #[case(
1350 "dep_name_with_special_chars-123: description with !@#$%^&*",
1351 OptionalDependency {
1352 package_relation: PackageRelation {
1353 name: Name::new("dep_name_with_special_chars-123").unwrap(),
1354 version_requirement: None,
1355 },
1356 description: Some("description with !@#$%^&*".to_string()),
1357 },
1358 )]
1359 // versioned optional dependencies
1360 #[case(
1361 "elfutils=0.192: for translations",
1362 OptionalDependency {
1363 package_relation: PackageRelation {
1364 name: Name::new("elfutils").unwrap(),
1365 version_requirement: Some(VersionRequirement {
1366 comparison: VersionComparison::Equal,
1367 version: "0.192".parse().unwrap(),
1368 }),
1369 },
1370 description: Some("for translations".to_string()),
1371 },
1372 )]
1373 #[case(
1374 "python>=3: For Python bindings",
1375 OptionalDependency {
1376 package_relation: PackageRelation {
1377 name: Name::new("python").unwrap(),
1378 version_requirement: Some(VersionRequirement {
1379 comparison: VersionComparison::GreaterOrEqual,
1380 version: "3".parse().unwrap(),
1381 }),
1382 },
1383 description: Some("For Python bindings".to_string()),
1384 },
1385 )]
1386 #[case(
1387 "java-environment>=17: required by extension-wiki-publisher and extension-nlpsolver",
1388 OptionalDependency {
1389 package_relation: PackageRelation {
1390 name: Name::new("java-environment").unwrap(),
1391 version_requirement: Some(VersionRequirement {
1392 comparison: VersionComparison::GreaterOrEqual,
1393 version: "17".parse().unwrap(),
1394 }),
1395 },
1396 description: Some("required by extension-wiki-publisher and extension-nlpsolver".to_string()),
1397 },
1398 )]
1399 fn opt_depend_from_string(#[case] input: &str, #[case] expected: OptionalDependency) {
1400 let opt_depend_result = OptionalDependency::from_str(input);
1401 let Ok(optional_dependency) = opt_depend_result else {
1402 panic!(
1403 "Encountered unexpected error when parsing optional dependency: {opt_depend_result:?}"
1404 )
1405 };
1406
1407 assert_eq!(
1408 expected, optional_dependency,
1409 "Optional dependency has not been correctly parsed."
1410 );
1411 }
1412
1413 #[rstest]
1414 #[case(
1415 "example: this is an example dependency",
1416 "example: this is an example dependency"
1417 )]
1418 #[case(
1419 "example-two: a description with lots of whitespace padding ",
1420 "example-two: a description with lots of whitespace padding"
1421 )]
1422 #[case(
1423 "tabs: a description with a tab directly after the colon",
1424 "tabs: a description with a tab directly after the colon"
1425 )]
1426 #[case("dep_name", "dep_name")]
1427 #[case("dep_name: ", "dep_name")]
1428 #[case(
1429 "dep_name_with_special_chars-123: description with !@#$%^&*",
1430 "dep_name_with_special_chars-123: description with !@#$%^&*"
1431 )]
1432 // versioned optional dependencies
1433 #[case("elfutils=0.192: for translations", "elfutils=0.192: for translations")]
1434 #[case("python>=3: For Python bindings", "python>=3: For Python bindings")]
1435 #[case(
1436 "java-environment>=17: required by extension-wiki-publisher and extension-nlpsolver",
1437 "java-environment>=17: required by extension-wiki-publisher and extension-nlpsolver"
1438 )]
1439 fn opt_depend_to_string(#[case] input: &str, #[case] expected: &str) {
1440 let opt_depend_result = OptionalDependency::from_str(input);
1441 let Ok(optional_dependency) = opt_depend_result else {
1442 panic!(
1443 "Encountered unexpected error when parsing optional dependency: {opt_depend_result:?}"
1444 )
1445 };
1446 assert_eq!(
1447 expected,
1448 optional_dependency.to_string(),
1449 "OptionalDependency to_string is erroneous."
1450 );
1451 }
1452
1453 #[rstest]
1454 #[case(
1455 "#invalid-name: this is an example dependency",
1456 "invalid first character of package name"
1457 )]
1458 #[case(": no_name_colon", "invalid first character of package name")]
1459 #[case(
1460 "name:description with no leading whitespace",
1461 "invalid character in package name"
1462 )]
1463 #[case(
1464 "dep-name>=10: \n\ndescription with\rnewlines",
1465 "expected no carriage returns or newlines"
1466 )]
1467 fn opt_depend_invalid_string_parse_error(#[case] input: &str, #[case] err_snippet: &str) {
1468 let Err(Error::ParseError(err_msg)) = OptionalDependency::from_str(input) else {
1469 panic!("'{input}' did not fail to parse as expected")
1470 };
1471 assert!(
1472 err_msg.contains(err_snippet),
1473 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1474 );
1475 }
1476
1477 #[rstest]
1478 #[case("example.so", SonameV1::Basic("example.so".parse().unwrap()))]
1479 #[case("example.so=1.0.0-64", SonameV1::Explicit {
1480 name: "example.so".parse().unwrap(),
1481 version: "1.0.0".parse().unwrap(),
1482 architecture: ElfArchitectureFormat::Bit64,
1483 })]
1484 fn sonamev1_from_string(
1485 #[case] input: &str,
1486 #[case] expected_result: SonameV1,
1487 ) -> testresult::TestResult<()> {
1488 let soname = SonameV1::from_str(input)?;
1489 assert_eq!(expected_result, soname);
1490 assert_eq!(input, soname.to_string());
1491 Ok(())
1492 }
1493
1494 #[rstest]
1495 #[case(
1496 "libwlroots-0.18.so=libwlroots-0.18.so-64",
1497 SonameV1::Unversioned {
1498 name: "libwlroots-0.18.so".parse().unwrap(),
1499 soname: "libwlroots-0.18.so".parse().unwrap(),
1500 architecture: ElfArchitectureFormat::Bit64,
1501 },
1502 )]
1503 #[case(
1504 "libexample.so=otherlibexample.so-64",
1505 SonameV1::Unversioned {
1506 name: "libexample.so".parse().unwrap(),
1507 soname: "otherlibexample.so".parse().unwrap(),
1508 architecture: ElfArchitectureFormat::Bit64,
1509 },
1510 )]
1511 fn sonamev1_from_string_without_version(
1512 #[case] input: &str,
1513 #[case] expected_result: SonameV1,
1514 ) -> testresult::TestResult<()> {
1515 let soname = SonameV1::from_str(input)?;
1516 assert_eq!(expected_result, soname);
1517 assert_eq!(input, soname.to_string());
1518 Ok(())
1519 }
1520
1521 #[rstest]
1522 #[case("noso", "invalid shared object name")]
1523 #[case("invalidversion.so=1*2-64", "expected version or shared object name")]
1524 #[case(
1525 "nodelimiter.so=1.64",
1526 "expected a version or shared object name, followed by an ELF architecture format"
1527 )]
1528 #[case(
1529 "noarchitecture.so=1-",
1530 "expected a version or shared object name, followed by an ELF architecture format"
1531 )]
1532 #[case("invalidarchitecture.so=1-82", "invalid architecture")]
1533 #[case("invalidsoname.so~1.64", "unexpected trailing content")]
1534 fn invalid_sonamev1_parser(#[case] input: &str, #[case] error_snippet: &str) {
1535 let result = SonameV1::from_str(input);
1536 assert!(result.is_err(), "Expected SonameV1 parsing to fail");
1537 let err = result.unwrap_err();
1538 let pretty_error = err.to_string();
1539 assert!(
1540 pretty_error.contains(error_snippet),
1541 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
1542 );
1543 }
1544
1545 #[rstest]
1546 #[case(
1547 "otherlibexample.so",
1548 VersionOrSoname::Soname(
1549 SharedObjectName::new("otherlibexample.so").unwrap())
1550 )]
1551 #[case(
1552 "1.0.0",
1553 VersionOrSoname::Version(
1554 PackageVersion::from_str("1.0.0").unwrap())
1555 )]
1556 fn version_or_soname_from_string(
1557 #[case] input: &str,
1558 #[case] expected_result: VersionOrSoname,
1559 ) -> testresult::TestResult<()> {
1560 let version = VersionOrSoname::from_str(input)?;
1561 assert_eq!(expected_result, version);
1562 assert_eq!(input, version.to_string());
1563 Ok(())
1564 }
1565
1566 #[rstest]
1567 #[case(
1568 "lib:libexample.so",
1569 SonameV2 {
1570 prefix: "lib".parse().unwrap(),
1571 soname: Soname {
1572 name: "libexample.so".parse().unwrap(),
1573 version: None,
1574 },
1575 },
1576 )]
1577 #[case(
1578 "usr:libexample.so.1",
1579 SonameV2 {
1580 prefix: "usr".parse().unwrap(),
1581 soname: Soname {
1582 name: "libexample.so".parse().unwrap(),
1583 version: "1".parse().ok(),
1584 },
1585 },
1586 )]
1587 #[case(
1588 "lib:libexample.so.1.2.3",
1589 SonameV2 {
1590 prefix: "lib".parse().unwrap(),
1591 soname: Soname {
1592 name: "libexample.so".parse().unwrap(),
1593 version: "1.2.3".parse().ok(),
1594 },
1595 },
1596 )]
1597 #[case(
1598 "lib:libexample.so.so.420",
1599 SonameV2 {
1600 prefix: "lib".parse().unwrap(),
1601 soname: Soname {
1602 name: "libexample.so.so".parse().unwrap(),
1603 version: "420".parse().ok(),
1604 },
1605 },
1606 )]
1607 #[case(
1608 "lib:libexample.so.test",
1609 SonameV2 {
1610 prefix: "lib".parse().unwrap(),
1611 soname: Soname {
1612 name: "libexample.so".parse().unwrap(),
1613 version: "test".parse().ok(),
1614 },
1615 },
1616 )]
1617 fn sonamev2_from_string(
1618 #[case] input: &str,
1619 #[case] expected_result: SonameV2,
1620 ) -> testresult::TestResult<()> {
1621 let soname = SonameV2::from_str(input)?;
1622 assert_eq!(expected_result, soname);
1623 assert_eq!(input, soname.to_string());
1624 Ok(())
1625 }
1626
1627 #[rstest]
1628 #[case("libexample.so.1", "invalid shared library prefix delimiter")]
1629 #[case("lib:libexample.so-abc", "invalid version delimiter")]
1630 #[case("lib:libexample.so.10-10", "invalid pkgver character")]
1631 #[case("lib:libexample.so.1.0.0-64", "invalid pkgver character")]
1632 fn invalid_sonamev2_parser(#[case] input: &str, #[case] error_snippet: &str) {
1633 let result = SonameV2::from_str(input);
1634 assert!(result.is_err(), "Expected SonameV2 parsing to fail");
1635 let err = result.unwrap_err();
1636 let pretty_error = err.to_string();
1637 assert!(
1638 pretty_error.contains(error_snippet),
1639 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
1640 );
1641 }
1642
1643 #[rstest]
1644 #[case(
1645 "example",
1646 RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), None))
1647 )]
1648 #[case(
1649 "example=1.0.0",
1650 RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), "=1.0.0".parse().ok()))
1651 )]
1652 #[case(
1653 "example>=1.0.0",
1654 RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), ">=1.0.0".parse().ok()))
1655 )]
1656 #[case(
1657 "lib:example.so.1",
1658 RelationOrSoname::SonameV2(
1659 SonameV2::new(
1660 "lib".parse().unwrap(),
1661 Soname::from_str("example.so.1").unwrap(),
1662 )
1663 )
1664 )]
1665 #[case(
1666 "lib:example.so",
1667 RelationOrSoname::SonameV2(
1668 SonameV2::new(
1669 "lib".parse().unwrap(),
1670 Soname::from_str("example.so").unwrap(),
1671 )
1672 )
1673 )]
1674 #[case(
1675 "example.so",
1676 RelationOrSoname::SonameV1(
1677 SonameV1::new(
1678 "example.so".parse().unwrap(),
1679 None,
1680 None,
1681 ).unwrap()
1682 )
1683 )]
1684 #[case(
1685 "example.so=1.0.0-64",
1686 RelationOrSoname::SonameV1(
1687 SonameV1::new(
1688 "example.so".parse().unwrap(),
1689 Some(VersionOrSoname::Version("1.0.0".parse().unwrap())),
1690 Some(ElfArchitectureFormat::Bit64),
1691 ).unwrap()
1692 )
1693 )]
1694 #[case(
1695 "libexample.so=otherlibexample.so-64",
1696 RelationOrSoname::SonameV1(
1697 SonameV1::new(
1698 "libexample.so".parse().unwrap(),
1699 Some(VersionOrSoname::Soname("otherlibexample.so".parse().unwrap())),
1700 Some(ElfArchitectureFormat::Bit64),
1701 ).unwrap()
1702 )
1703 )]
1704 fn test_relation_or_soname_parser(
1705 #[case] mut input: &str,
1706 #[case] expected: RelationOrSoname,
1707 ) -> TestResult {
1708 let input_str = input.to_string();
1709 let result = RelationOrSoname::parser(&mut input)?;
1710 assert_eq!(result, expected);
1711 assert_eq!(result.to_string(), input_str);
1712 Ok(())
1713 }
1714}