libpulse_binding/
version.rs

1// Copyright 2017 Lyndon Brown
2//
3// This file is part of the PulseAudio Rust language binding.
4//
5// Licensed under the MIT license or the Apache license (version 2.0), at your option. You may not
6// copy, modify, or distribute this file except in compliance with said license. You can find copies
7// of these licenses either in the LICENSE-MIT and LICENSE-APACHE files, or alternatively at
8// <http://opensource.org/licenses/MIT> and <http://www.apache.org/licenses/LICENSE-2.0>
9// respectively.
10//
11// Portions of documentation are copied from the LGPL 2.1+ licensed PulseAudio C headers on a
12// fair-use basis, as discussed in the overall project readme (available in the git repository).
13
14//! Version related constants and functions.
15//!
16//! This module contains functions and constants relating to the version of the PulseAudio (PA)
17//! client system library.
18//!
19//! # Dynamic compatibility
20//!
21//! As discussed in the project `COMPATIBILITY.md` file, compatibility is offered for multiple
22//! versions of the PA client system library, with feature flags adapting the crate to changes made
23//! in the API of newer PA versions.
24//!
25//! Note that the minimum supported version of PA is v5.0.
26//!
27//! # Runtime checking
28//!
29//! The following functions are provided to retrieve and compare the version of the actual PA client
30//! system library in use at runtime:
31//!
32//!  - The [`get_library_version()`] function obtains the version string the system library
33//!    provides.
34//!  - The [`get_library_version_numbers()`] function uses the previous function and attempts to
35//!    parse the version string it returns into numeric form for comparison purposes.
36//!  - The [`compare_with_library_version()`] function uses the previous function and allows
37//!    comparing a provided major and minor version number with what it returned.
38//!  - The [`library_version_is_too_old()`] function uses the previous function to compare against
39//!    the [`TARGET_VERSION`] constant version numbers. This constant varies depending upon PA
40//!    version feature flags, and thus this can be used to check that a program is not being run on
41//!    a system with too old of a version of PA, helping combat the “forward” compatibility problem
42//!    discussed in the project `COMPATIBILITY.md` documentation.
43//!
44//! # Dynamic constants
45//!
46//! The version constants defined here mostly relate to those provided in the PA C headers, and are
47//! likely of little use to most projects. They are set dynamically, depending upon the feature
48//! flags used, or in other words the level of minimum compatibility support selected. Note that PA
49//! version feature flags are only introduced when new versions of PA introduce changes to its API
50//! that would require one. The version numbers associated with each PA version feature flag are
51//! those from the PA version that required introduction of that feature flag.
52//!
53//! As an example to clarify, if the “newest” PA version feature flag enabled is `pa_v8` (which
54//! obviously corresponds to a minimum compatibility level of PA version 8.0), then the
55//! [`TARGET_VERSION`] constant is set to `(8, 0)`. The “next-newest” feature flag is `pa_v11`,
56//! which if enabled would bump it up to `(11, 0)`.
57
58use capi;
59use std::borrow::Cow;
60use std::cmp::Ordering;
61use std::ffi::CStr;
62
63// Re-export from sys
64pub use capi::version::{Compatibility, get_compatibility};
65pub use capi::version::{TARGET_VERSION_STRING, TARGET_VERSION};
66pub use capi::version::{PA_API_VERSION as API_VERSION, PA_PROTOCOL_VERSION as PROTOCOL_VERSION};
67
68/// Kinds of errors from trying to parse the runtime PulseAudio system library version string.
69#[derive(Debug, Copy, Clone, PartialEq, Eq)]
70#[non_exhaustive]
71enum ErrorKind {
72    /// Error parsing part as integer.
73    ParseIntError,
74    /// Missing version part.
75    MissingPart,
76    /// Too many parts found in the string (unexpected; something is wrong).
77    ExtraParts,
78}
79
80/// Error from trying to parse the runtime PulseAudio system library version string.
81#[derive(Debug, Clone, Eq, PartialEq)]
82pub struct Error {
83    /// The problematic version sring which could not be parsed.
84    ver_str: Cow<'static, str>,
85}
86
87impl Error {
88    #[inline]
89    fn new(ver_str: Cow<'static, str>) -> Self {
90        Self { ver_str }
91    }
92}
93
94impl std::error::Error for Error {}
95
96impl std::fmt::Display for Error {
97    #[inline]
98    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
99        format!("failed to parse PulseAudio system library version string '{}'", &self.ver_str)
100            .fmt(f)
101    }
102}
103
104/// Checks whether the version of the running system library is older than the version corresponding
105/// to the compatibility level selected via the available feature flags.
106///
107/// Returns `Ok(true)` if the library version is older, `Ok(false)` if equal or newer, or `Err` if a
108/// problem occurred processing the version string.
109#[inline]
110pub fn library_version_is_too_old() -> Result<bool, Error> {
111    match compare_with_library_version(TARGET_VERSION.0, TARGET_VERSION.1)? {
112        Ordering::Less | Ordering::Equal => Ok(false),
113        Ordering::Greater => Ok(true),
114    }
115}
116
117/// Compares the supplied version with that of the runtime system library.
118///
119/// Returns the comparison, or `Err` if a problem occurred parsing the library version string. The
120/// comparison will represent `supplied.cmp(&library)`.
121#[inline]
122pub fn compare_with_library_version(major: u8, minor: u8) -> Result<std::cmp::Ordering, Error> {
123    let (lib_major, lib_minor, _) = get_library_version_numbers()?;
124    Ok((major).cmp(&lib_major).then_with(|| minor.cmp(&lib_minor)))
125}
126
127/// Tries to convert the runtime system library version to numeric major, minor and micro form, for
128/// comparison purposes.
129///
130/// Note, currently micro is always zero. This is the case even in beta/rc versions (like 13.99.1)
131/// due to the fact that the version string returned by PA always has micro fixed to zero.
132///
133/// Returns `Err` if parsing the version number string fails.
134#[inline]
135pub fn get_library_version_numbers() -> Result<(u8, u8, u8), Error> {
136    let ver = get_library_version().to_string_lossy();
137    pa_version_str_to_num(&ver).or_else(|_e| Err(Error::new(ver)))
138}
139
140/// Convert PulseAudio version string to major, minor and micro numbers.
141///
142/// The version number string should come from `pa_get_library_version()` and thus currently will
143/// always consist of exactly `$MAJOR.$MINOR.0` per the compiled version.h header. Note that the
144/// micro number is fixed to zero.
145#[inline]
146fn pa_version_str_to_num(ver: &str) -> Result<(u8, u8, u8), ErrorKind> {
147    let mut parts = ver.split('.');
148    let major: u8 =
149        parts.next().ok_or(ErrorKind::MissingPart)?.parse().or(Err(ErrorKind::ParseIntError))?;
150    let minor: u8 =
151        parts.next().ok_or(ErrorKind::MissingPart)?.parse().or(Err(ErrorKind::ParseIntError))?;
152    // Note, we want to be very strict about accepting only properly formatted values, as anything
153    // otherwise suggests a wierd problem, thus we do parse the micro number even though it will
154    // always be zero.
155    let micro: u8 =
156        parts.next().ok_or(ErrorKind::MissingPart)?.parse().or(Err(ErrorKind::ParseIntError))?;
157    match parts.next().is_some() {
158        true => Err(ErrorKind::ExtraParts), // Something isn’t right
159        false => Ok((major, minor, micro)),
160    }
161}
162
163/// Gets the version string of the (PulseAudio client system) library actually in use at runtime.
164#[inline]
165pub fn get_library_version() -> &'static CStr {
166    unsafe { CStr::from_ptr(capi::pa_get_library_version()) }
167}
168
169#[test]
170fn test_ver_str_to_num() {
171    assert_eq!(pa_version_str_to_num(""),         Err(ErrorKind::ParseIntError));
172    assert_eq!(pa_version_str_to_num(" "),        Err(ErrorKind::ParseIntError));
173    assert_eq!(pa_version_str_to_num("."),        Err(ErrorKind::ParseIntError));
174    assert_eq!(pa_version_str_to_num("a"),        Err(ErrorKind::ParseIntError));
175    assert_eq!(pa_version_str_to_num("a.a"),      Err(ErrorKind::ParseIntError));
176    assert_eq!(pa_version_str_to_num("a.1"),      Err(ErrorKind::ParseIntError));
177    assert_eq!(pa_version_str_to_num("14"),       Err(ErrorKind::MissingPart));
178    assert_eq!(pa_version_str_to_num("14.0"),     Err(ErrorKind::MissingPart));
179    assert_eq!(pa_version_str_to_num("14.0.0"),   Ok((14, 0, 0)));
180    assert_eq!(pa_version_str_to_num("14.1.0"),   Ok((14, 1, 0)));
181    assert_eq!(pa_version_str_to_num("14.2.0."),  Err(ErrorKind::ExtraParts));
182    assert_eq!(pa_version_str_to_num("14.2.0.0"), Err(ErrorKind::ExtraParts));
183    assert_eq!(pa_version_str_to_num("12.2a"),    Err(ErrorKind::ParseIntError));
184    assert_eq!(pa_version_str_to_num("12.a"),     Err(ErrorKind::ParseIntError));
185    assert_eq!(pa_version_str_to_num("12.a.1"),   Err(ErrorKind::ParseIntError));
186}
187
188#[test]
189fn test_getting_pa_version() {
190    let actual_ver_str =
191        unsafe { CStr::from_ptr(capi::pa_get_library_version()).to_string_lossy() };
192    let (major, minor, micro) = get_library_version_numbers().unwrap();
193    assert_eq!(format!("{}.{}.{}", major, minor, micro), actual_ver_str);
194}
195
196#[test]
197fn test_comparing_pa_version() {
198    let (major, minor, _micro) = get_library_version_numbers().unwrap();
199    assert_eq!(compare_with_library_version(major, minor).unwrap(), Ordering::Equal);
200    assert_eq!(compare_with_library_version(major + 1, minor).unwrap(), Ordering::Greater);
201    assert_eq!(compare_with_library_version(major - 1, minor).unwrap(), Ordering::Less);
202    assert_eq!(compare_with_library_version(major, minor + 1).unwrap(), Ordering::Greater);
203    assert_eq!(compare_with_library_version(major - 1, minor + 1).unwrap(), Ordering::Less);
204    if minor > 0 {
205        assert_eq!(compare_with_library_version(major, minor - 1).unwrap(), Ordering::Less);
206        assert_eq!(compare_with_library_version(major + 1, minor - 1).unwrap(), Ordering::Greater);
207    }
208}
209
210#[test]
211fn test_lib_ver_not_too_old() {
212    assert_eq!(library_version_is_too_old(), Ok(false));
213}