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    VERSIONFLAG_ANY_IS_PATCH, VERSIONFLAG_LOWER_BOUND, VERSIONFLAG_P_IS_PATCH,
24    VERSIONFLAG_UPPER_BOUND, version_compare2, version_compare4,
25};
26
27use std::cmp::Ordering;
28use std::ffi::CString;
29
30/// Compare two version strings.
31///
32/// Returns [`Ordering::Less`], [`Ordering::Equal`], or [`Ordering::Greater`].
33///
34/// # Panics
35///
36/// Panics if either version string contains an interior null byte.
37///
38/// # Examples
39///
40/// ```
41/// use std::cmp::Ordering;
42/// assert_eq!(libversion_sys::compare("1.0", "1.1"), Ordering::Less);
43/// assert_eq!(libversion_sys::compare("1.0", "1.0.0"), Ordering::Equal);
44/// ```
45pub fn compare(v1: &str, v2: &str) -> Ordering {
46    let v1 = CString::new(v1).expect("v1 contains interior null byte");
47    let v2 = CString::new(v2).expect("v2 contains interior null byte");
48    let result = unsafe { ffi::version_compare2(v1.as_ptr(), v2.as_ptr()) };
49    result.cmp(&0)
50}
51
52/// Compare two version strings with per-version flags.
53///
54/// See [`VERSIONFLAG_P_IS_PATCH`], [`VERSIONFLAG_ANY_IS_PATCH`],
55/// [`VERSIONFLAG_LOWER_BOUND`], [`VERSIONFLAG_UPPER_BOUND`].
56///
57/// # Panics
58///
59/// Panics if either version string contains an interior null byte.
60///
61/// # Examples
62///
63/// ```
64/// use std::cmp::Ordering;
65/// use libversion_sys::VERSIONFLAG_P_IS_PATCH;
66///
67/// // By default "p" means "pre", but with the flag it means "patch" (post-release)
68/// assert_eq!(
69///     libversion_sys::compare_with_flags("1.0p1", "1.0post1", VERSIONFLAG_P_IS_PATCH, 0),
70///     Ordering::Equal,
71/// );
72/// ```
73pub fn compare_with_flags(v1: &str, v2: &str, v1_flags: u32, v2_flags: u32) -> Ordering {
74    let v1 = CString::new(v1).expect("v1 contains interior null byte");
75    let v2 = CString::new(v2).expect("v2 contains interior null byte");
76    let result = unsafe {
77        ffi::version_compare4(v1.as_ptr(), v2.as_ptr(), v1_flags as i32, v2_flags as i32)
78    };
79    result.cmp(&0)
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn basic_comparison() {
88        assert_eq!(compare("0.99", "1.11"), Ordering::Less);
89        assert_eq!(compare("1.0", "1.0.0"), Ordering::Equal);
90        assert_eq!(compare("1.0", "0.99"), Ordering::Greater);
91    }
92
93    #[test]
94    fn prerelease() {
95        assert_eq!(compare("1.0alpha1", "1.0"), Ordering::Less);
96        assert_eq!(compare("1.0alpha1", "1.0rc1"), Ordering::Less);
97        assert_eq!(compare("1.0rc1", "1.0"), Ordering::Less);
98    }
99
100    #[test]
101    fn postrelease() {
102        assert_eq!(compare("1.0patch1", "1.0"), Ordering::Greater);
103        assert_eq!(compare("1.0.1", "1.0"), Ordering::Greater);
104    }
105
106    #[test]
107    fn p_is_patch_flag() {
108        // Without flag: p == pre (pre-release)
109        assert_eq!(compare("1.0p1", "1.0"), Ordering::Less);
110
111        // With flag: p == patch (post-release)
112        assert_eq!(
113            compare_with_flags("1.0p1", "1.0", VERSIONFLAG_P_IS_PATCH, 0),
114            Ordering::Greater,
115        );
116    }
117
118    #[test]
119    fn ffi_direct() {
120        let v1 = CString::new("1.0").unwrap();
121        let v2 = CString::new("2.0").unwrap();
122        let result = unsafe { ffi::version_compare2(v1.as_ptr(), v2.as_ptr()) };
123        assert_eq!(result, -1);
124    }
125}