Skip to main content

libversion_sys/
lib.rs

1//! FFI bindings and safe wrapper for [libversion](https://github.com/repology/libversion),
2//! an advanced version string comparison library.
3//!
4//! # Raw FFI
5//!
6//! The [`ffi`] module exposes the raw C functions and constants directly.
7//!
8//! # Safe API
9//!
10//! [`compare`] and [`compare_with_flags`] provide safe Rust wrappers that return
11//! [`std::cmp::Ordering`].
12
13#![allow(non_upper_case_globals)]
14#![allow(non_camel_case_types)]
15#![allow(non_snake_case)]
16
17/// Raw FFI bindings generated by bindgen.
18pub mod ffi {
19    include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
20}
21
22pub use ffi::{
23    LIBVERSION_VERSION_MAJOR, LIBVERSION_VERSION_MINOR, LIBVERSION_VERSION_PATCH,
24    VERSIONFLAG_ANY_IS_PATCH, VERSIONFLAG_LOWER_BOUND, VERSIONFLAG_P_IS_PATCH,
25    VERSIONFLAG_UPPER_BOUND, version_compare2, version_compare4,
26};
27
28use std::cmp::Ordering;
29use std::ffi::CString;
30
31/// Compare two version strings.
32///
33/// Returns [`Ordering::Less`], [`Ordering::Equal`], or [`Ordering::Greater`].
34///
35/// # Panics
36///
37/// Panics if either version string contains an interior null byte.
38///
39/// # Examples
40///
41/// ```
42/// use std::cmp::Ordering;
43/// assert_eq!(libversion_sys::compare("1.0", "1.1"), Ordering::Less);
44/// assert_eq!(libversion_sys::compare("1.0", "1.0.0"), Ordering::Equal);
45/// ```
46pub fn compare(v1: &str, v2: &str) -> Ordering {
47    let v1 = CString::new(v1).expect("v1 contains interior null byte");
48    let v2 = CString::new(v2).expect("v2 contains interior null byte");
49    let result = unsafe { ffi::version_compare2(v1.as_ptr(), v2.as_ptr()) };
50    result.cmp(&0)
51}
52
53/// Compare two version strings with per-version flags.
54///
55/// See [`VERSIONFLAG_P_IS_PATCH`], [`VERSIONFLAG_ANY_IS_PATCH`],
56/// [`VERSIONFLAG_LOWER_BOUND`], [`VERSIONFLAG_UPPER_BOUND`].
57///
58/// # Panics
59///
60/// Panics if either version string contains an interior null byte.
61///
62/// # Examples
63///
64/// ```
65/// use std::cmp::Ordering;
66/// use libversion_sys::VERSIONFLAG_P_IS_PATCH;
67///
68/// // By default "p" means "pre", but with the flag it means "patch" (post-release)
69/// assert_eq!(
70///     libversion_sys::compare_with_flags("1.0p1", "1.0post1", VERSIONFLAG_P_IS_PATCH, 0),
71///     Ordering::Equal,
72/// );
73/// ```
74pub fn compare_with_flags(v1: &str, v2: &str, v1_flags: u32, v2_flags: u32) -> Ordering {
75    let v1 = CString::new(v1).expect("v1 contains interior null byte");
76    let v2 = CString::new(v2).expect("v2 contains interior null byte");
77    let result = unsafe {
78        ffi::version_compare4(v1.as_ptr(), v2.as_ptr(), v1_flags as i32, v2_flags as i32)
79    };
80    result.cmp(&0)
81}
82
83/// Returns the libversion release string from the headers used during build.
84pub fn version_string() -> &'static str {
85    std::str::from_utf8(&ffi::LIBVERSION_VERSION[..ffi::LIBVERSION_VERSION.len() - 1])
86        .expect("libversion version string is not valid UTF-8")
87}
88
89/// Rust equivalent of the `LIBVERSION_VERSION_ATLEAST` macro.
90#[allow(clippy::absurd_extreme_comparisons)]
91pub const fn version_atleast(major: u32, minor: u32, patch: u32) -> bool {
92    (LIBVERSION_VERSION_MAJOR > major)
93        || (LIBVERSION_VERSION_MAJOR == major && LIBVERSION_VERSION_MINOR > minor)
94        || (LIBVERSION_VERSION_MAJOR == major
95            && LIBVERSION_VERSION_MINOR == minor
96            && LIBVERSION_VERSION_PATCH >= patch)
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn basic_comparison() {
105        assert_eq!(compare("0.99", "1.11"), Ordering::Less);
106        assert_eq!(compare("1.0", "1.0.0"), Ordering::Equal);
107        assert_eq!(compare("1.0", "0.99"), Ordering::Greater);
108    }
109
110    #[test]
111    fn prerelease() {
112        assert_eq!(compare("1.0alpha1", "1.0"), Ordering::Less);
113        assert_eq!(compare("1.0alpha1", "1.0rc1"), Ordering::Less);
114        assert_eq!(compare("1.0rc1", "1.0"), Ordering::Less);
115    }
116
117    #[test]
118    fn postrelease() {
119        assert_eq!(compare("1.0patch1", "1.0"), Ordering::Greater);
120        assert_eq!(compare("1.0.1", "1.0"), Ordering::Greater);
121    }
122
123    #[test]
124    fn p_is_patch_flag() {
125        // Without flag: p == pre (pre-release)
126        assert_eq!(compare("1.0p1", "1.0"), Ordering::Less);
127
128        // With flag: p == patch (post-release)
129        assert_eq!(
130            compare_with_flags("1.0p1", "1.0", VERSIONFLAG_P_IS_PATCH, 0),
131            Ordering::Greater,
132        );
133    }
134
135    #[test]
136    fn any_is_patch_flag() {
137        assert_eq!(compare("1.0foopatchset1", "1.0"), Ordering::Less);
138        assert_eq!(
139            compare_with_flags("1.0foopatchset1", "1.0", VERSIONFLAG_ANY_IS_PATCH, 0),
140            Ordering::Greater,
141        );
142    }
143
144    #[test]
145    fn lower_bound_flag() {
146        assert_eq!(
147            compare_with_flags("1.0alpha1", "1.0", 0, VERSIONFLAG_LOWER_BOUND),
148            Ordering::Greater,
149        );
150        assert_eq!(
151            compare_with_flags("0.999", "1.0", 0, VERSIONFLAG_LOWER_BOUND),
152            Ordering::Less,
153        );
154    }
155
156    #[test]
157    fn upper_bound_flag() {
158        assert_eq!(
159            compare_with_flags("1.0.1", "1.0", 0, VERSIONFLAG_UPPER_BOUND),
160            Ordering::Less,
161        );
162        assert_eq!(
163            compare_with_flags("1.1", "1.0", 0, VERSIONFLAG_UPPER_BOUND),
164            Ordering::Greater,
165        );
166    }
167
168    #[test]
169    #[should_panic(expected = "v1 contains interior null byte")]
170    fn compare_rejects_interior_null() {
171        let _ = compare("1.0\0rc1", "1.0");
172    }
173
174    #[test]
175    #[should_panic(expected = "v2 contains interior null byte")]
176    fn compare_with_flags_rejects_interior_null() {
177        let _ = compare_with_flags("1.0", "1.0\0rc1", 0, 0);
178    }
179
180    #[test]
181    fn version_metadata() {
182        let parts = version_string()
183            .split('.')
184            .map(|part| part.parse::<u32>().unwrap())
185            .collect::<Vec<_>>();
186
187        assert_eq!(
188            parts,
189            vec![
190                LIBVERSION_VERSION_MAJOR,
191                LIBVERSION_VERSION_MINOR,
192                LIBVERSION_VERSION_PATCH,
193            ]
194        );
195        assert!(version_atleast(
196            LIBVERSION_VERSION_MAJOR,
197            LIBVERSION_VERSION_MINOR,
198            LIBVERSION_VERSION_PATCH,
199        ));
200        assert!(!version_string().is_empty());
201    }
202
203    #[test]
204    fn ffi_direct() {
205        let v1 = CString::new("1.0").unwrap();
206        let v2 = CString::new("2.0").unwrap();
207        let result = unsafe { ffi::version_compare2(v1.as_ptr(), v2.as_ptr()) };
208        assert_eq!(result, -1);
209    }
210
211    #[test]
212    fn ffi_compare4_direct() {
213        let v1 = CString::new("1.0p1").unwrap();
214        let v2 = CString::new("1.0").unwrap();
215        let result = unsafe {
216            ffi::version_compare4(v1.as_ptr(), v2.as_ptr(), VERSIONFLAG_P_IS_PATCH as i32, 0)
217        };
218        assert_eq!(result, 1);
219    }
220}