libversion-sys 0.1.0

FFI bindings to libversion, an advanced version string comparison library
Documentation
//! FFI bindings and safe wrapper for [libversion](https://github.com/repology/libversion),
//! an advanced version string comparison library.
//!
//! # Raw FFI
//!
//! The [`ffi`] module exposes the raw C functions and constants directly.
//!
//! # Safe API
//!
//! [`compare`] and [`compare_with_flags`] provide safe Rust wrappers that return
//! [`std::cmp::Ordering`].

#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]

/// Raw FFI bindings generated by bindgen.
pub mod ffi {
    include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
}

pub use ffi::{
    VERSIONFLAG_ANY_IS_PATCH, VERSIONFLAG_LOWER_BOUND, VERSIONFLAG_P_IS_PATCH,
    VERSIONFLAG_UPPER_BOUND, version_compare2, version_compare4,
};

use std::cmp::Ordering;
use std::ffi::CString;

/// Compare two version strings.
///
/// Returns [`Ordering::Less`], [`Ordering::Equal`], or [`Ordering::Greater`].
///
/// # Panics
///
/// Panics if either version string contains an interior null byte.
///
/// # Examples
///
/// ```
/// use std::cmp::Ordering;
/// assert_eq!(libversion_sys::compare("1.0", "1.1"), Ordering::Less);
/// assert_eq!(libversion_sys::compare("1.0", "1.0.0"), Ordering::Equal);
/// ```
pub fn compare(v1: &str, v2: &str) -> Ordering {
    let v1 = CString::new(v1).expect("v1 contains interior null byte");
    let v2 = CString::new(v2).expect("v2 contains interior null byte");
    let result = unsafe { ffi::version_compare2(v1.as_ptr(), v2.as_ptr()) };
    result.cmp(&0)
}

/// Compare two version strings with per-version flags.
///
/// See [`VERSIONFLAG_P_IS_PATCH`], [`VERSIONFLAG_ANY_IS_PATCH`],
/// [`VERSIONFLAG_LOWER_BOUND`], [`VERSIONFLAG_UPPER_BOUND`].
///
/// # Panics
///
/// Panics if either version string contains an interior null byte.
///
/// # Examples
///
/// ```
/// use std::cmp::Ordering;
/// use libversion_sys::VERSIONFLAG_P_IS_PATCH;
///
/// // By default "p" means "pre", but with the flag it means "patch" (post-release)
/// assert_eq!(
///     libversion_sys::compare_with_flags("1.0p1", "1.0post1", VERSIONFLAG_P_IS_PATCH, 0),
///     Ordering::Equal,
/// );
/// ```
pub fn compare_with_flags(v1: &str, v2: &str, v1_flags: u32, v2_flags: u32) -> Ordering {
    let v1 = CString::new(v1).expect("v1 contains interior null byte");
    let v2 = CString::new(v2).expect("v2 contains interior null byte");
    let result = unsafe {
        ffi::version_compare4(v1.as_ptr(), v2.as_ptr(), v1_flags as i32, v2_flags as i32)
    };
    result.cmp(&0)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn basic_comparison() {
        assert_eq!(compare("0.99", "1.11"), Ordering::Less);
        assert_eq!(compare("1.0", "1.0.0"), Ordering::Equal);
        assert_eq!(compare("1.0", "0.99"), Ordering::Greater);
    }

    #[test]
    fn prerelease() {
        assert_eq!(compare("1.0alpha1", "1.0"), Ordering::Less);
        assert_eq!(compare("1.0alpha1", "1.0rc1"), Ordering::Less);
        assert_eq!(compare("1.0rc1", "1.0"), Ordering::Less);
    }

    #[test]
    fn postrelease() {
        assert_eq!(compare("1.0patch1", "1.0"), Ordering::Greater);
        assert_eq!(compare("1.0.1", "1.0"), Ordering::Greater);
    }

    #[test]
    fn p_is_patch_flag() {
        // Without flag: p == pre (pre-release)
        assert_eq!(compare("1.0p1", "1.0"), Ordering::Less);

        // With flag: p == patch (post-release)
        assert_eq!(
            compare_with_flags("1.0p1", "1.0", VERSIONFLAG_P_IS_PATCH, 0),
            Ordering::Greater,
        );
    }

    #[test]
    fn ffi_direct() {
        let v1 = CString::new("1.0").unwrap();
        let v2 = CString::new("2.0").unwrap();
        let result = unsafe { ffi::version_compare2(v1.as_ptr(), v2.as_ptr()) };
        assert_eq!(result, -1);
    }
}