winer 0.1.5

Helper library for Wine
Documentation
//! # winer
//!
//! Helper library for Wine
//!
//! [![license][license badge]][repo]
//! [![crates.io version][crates.io version badge]][crate]
//! [![docs.rs documentation][docs.rs badge]][docs]
//!
//! Currently supports Wine version APIs.
//!
//! ```rust
//! println!("{}", winer::get_build_str().unwrap_or("N/A"));
//! ```
//!
//! [repo]: https://github.com/xotty/winer
//! [crate]: https://crates.io/crates/winer
//! [docs]: https://docs.rs/winer
//!
//! [license badge]: https://img.shields.io/github/license/xotty/winer?style=flat
//! [crates.io version badge]: https://img.shields.io/crates/v/winer?style=flat
//! [docs.rs badge]: https://img.shields.io/docsrs/winer?style=flat

#![deny(unused_imports, unused_must_use)]
#![allow(clippy::missing_transmute_annotations)]
#![cfg_attr(not(test), no_std)]

#[cfg(not(windows))]
compile_error!("This crate only works for Windows targets.");

use core::ffi::{CStr, c_char, c_void};
use core::mem::{MaybeUninit, transmute};
use core::ptr::{null, null_mut};

use windows::Wdk::System::SystemInformation::{NtQuerySystemInformation, SYSTEM_INFORMATION_CLASS};
use windows::Win32::System::LibraryLoader::{GetModuleHandleW, GetProcAddress};
use windows::core::{s, w};

/// Host system information
///
/// It usually corresponds to `uname -s` (sysname) and `uname -r` (release) on
/// the host.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostVersion {
    sysname: &'static CStr,
    release: &'static CStr,
}

impl HostVersion {
    /// Gets host system name [`CStr`].
    pub fn sysname(&self) -> &'static CStr {
        self.sysname
    }

    /// Gets host system name string.
    pub fn sysname_str(&self) -> Option<&'static str> {
        self.sysname.to_str().ok()
    }

    /// Gets host system release [`CStr`].
    pub fn release(&self) -> &'static CStr {
        self.release
    }

    /// Gets host system release string.
    pub fn release_str(&self) -> Option<&'static str> {
        self.release.to_str().ok()
    }
}

type GetVersionFn = unsafe extern "C" fn() -> *const c_char;
type GetBuildIdFn = unsafe extern "C" fn() -> *const c_char;
type GetHostVersionFn = unsafe extern "C" fn(*mut *const c_char, *mut *const c_char);

fn locate_wine_get_version() -> Option<GetVersionFn> {
    unsafe {
        let module = GetModuleHandleW(w!("ntdll.dll")).ok()?;
        let proc = GetProcAddress(module, s!("wine_get_version"))?;
        Some(transmute::<_, GetVersionFn>(proc))
    }
}

/// Tells whether the program is running under Wine.
///
/// It just checks the existence of the procedure `wine_get_version`.
pub fn is_wine() -> bool {
    locate_wine_get_version().is_some()
}

/// Gets Wine version by calling `wine_get_version` in `ntdll` under the hood.
pub fn get_version() -> Option<&'static CStr> {
    Some(unsafe {
        let proc = locate_wine_get_version()?;
        CStr::from_ptr(proc())
    })
}

/// Gets Wine version string, a convenient helper that maps [`get_version`].
///
/// A version string looks like `9.0`.
pub fn get_version_str() -> Option<&'static str> {
    get_version().and_then(|cstr| cstr.to_str().ok())
}

/// Gets Wine build by calling `wine_get_build_id` in `ntdll`.
pub fn get_build() -> Option<&'static CStr> {
    Some(unsafe {
        let module = GetModuleHandleW(w!("ntdll.dll")).ok()?;
        let proc = GetProcAddress(module, s!("wine_get_build_id"))?;
        CStr::from_ptr(transmute::<_, GetBuildIdFn>(proc)())
    })
}

/// Gets Wine build string, a convenient helper that maps [`get_build`].
///
/// A build string looks like `wine-9.0 (Ubuntu 9.0~repack-4build3)`.
pub fn get_build_str() -> Option<&'static str> {
    get_build().and_then(|cstr| cstr.to_str().ok())
}

/// Gets host system information by calling `wine_get_host_version`.
pub fn get_host_version() -> Option<HostVersion> {
    let (sysname, release) = unsafe {
        let module = GetModuleHandleW(w!("ntdll.dll")).ok()?;
        let proc = GetProcAddress(module, s!("wine_get_host_version"))?;
        let mut sysname_ptr: *const c_char = null();
        let mut release_ptr: *const c_char = null();
        transmute::<_, GetHostVersionFn>(proc)(&mut sysname_ptr, &mut release_ptr);
        (CStr::from_ptr(sysname_ptr), CStr::from_ptr(release_ptr))
    };
    Some(HostVersion { sysname, release })
}

/// Wine extension of system information class
///
/// References:
/// * <https://gitlab.winehq.org/wine/wine/-/blob/ba6adef9bfc209f1247ba88acec64b58d97100c3/include/winternl.h#L2157>
pub const SYSTEM_WINE_VERSION_INFORMATION: SYSTEM_INFORMATION_CLASS =
    SYSTEM_INFORMATION_CLASS(1000i32);

/// Wine version information
///
/// There is a special information class [`SYSTEM_WINE_VERSION_INFORMATION`]
/// present in Wine, which can be used in `NtQuerySystemInformation`. It fills
/// a buffer with the version, build and host system information in one call.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Info {
    buffer: [u8; Info::BUFFER_SIZE],
    offsets: [usize; Info::SEGMENTS],
    ends_at: usize,
}

impl Info {
    /// Wine uses a 256-byte buffer for this information.
    ///
    /// References:
    /// * <https://gitlab.winehq.org/wine/wine/-/blob/ba6adef9bfc209f1247ba88acec64b58d97100c3/dlls/ntdll/version.c#L216>
    const BUFFER_SIZE: usize = 256usize;
    const SEGMENTS: usize = 4;

    #[inline]
    fn cstr_at<const N: usize>(&self) -> &CStr {
        let start = self.offsets[N];
        let end = self.offsets.get(N + 1).copied().unwrap_or(self.ends_at);
        unsafe { CStr::from_bytes_with_nul_unchecked(&self.buffer[start..end]) }
    }

    /// Consumes self and returns the raw buffer
    pub fn into_inner(self) -> [u8; Info::BUFFER_SIZE] {
        self.buffer
    }

    /// Gets version.
    pub fn version(&self) -> &CStr {
        self.cstr_at::<0>()
    }

    /// Gets version string.
    pub fn version_str(&self) -> Option<&str> {
        self.version().to_str().ok()
    }

    /// Gets build.
    pub fn build(&self) -> &CStr {
        self.cstr_at::<1>()
    }

    /// Gets build string.
    pub fn build_str(&self) -> Option<&str> {
        self.build().to_str().ok()
    }

    /// Gets host system name.
    pub fn sysname(&self) -> &CStr {
        self.cstr_at::<2>()
    }

    /// Gets host system name string.
    pub fn sysname_str(&self) -> Option<&str> {
        self.sysname().to_str().ok()
    }

    /// Gets host system release.
    pub fn release(&self) -> &CStr {
        self.cstr_at::<3>()
    }

    /// Gets host system release string.
    pub fn release_str(&self) -> Option<&str> {
        self.release().to_str().ok()
    }
}

/// Gets Wine version information.
pub fn get_info() -> Option<Info> {
    let mut buffer = [MaybeUninit::<u8>::uninit(); Info::BUFFER_SIZE];

    // References:
    // https://gitlab.winehq.org/wine/wine/-/blob/ba6adef9bfc209f1247ba88acec64b58d97100c3/dlls/ntdll/unix/system.c#L3684
    unsafe {
        NtQuerySystemInformation(
            SYSTEM_WINE_VERSION_INFORMATION,
            buffer.as_mut_ptr() as *mut c_void,
            Info::BUFFER_SIZE as u32,
            null_mut(),
        )
        .ok()
        .ok()?;
    }

    let buffer: [u8; Info::BUFFER_SIZE] = unsafe { transmute(buffer) };

    // Prefix sum
    let mut offsets = [0usize; Info::SEGMENTS];
    let mut ends_at = 0usize;

    // There are 4 parts in the buffer and they're separated by NUL-byte.
    // `snprintf(info, size, "%s%c%s%c%s%c%s", version, 0, wine_build, 0, buf.sysname, 0, buf.release);`
    let iter = buffer.split_inclusive(|b| *b == 0).take(Info::SEGMENTS);
    for (i, segment) in iter.enumerate() {
        let last = offsets[i];
        if let Some(offset) = offsets.get_mut(i + 1) {
            *offset = last + segment.len();
        } else {
            ends_at = offsets.last().unwrap() + segment.len();
        }
    }

    Some(Info {
        buffer,
        offsets,
        ends_at,
    })
}

#[test]
fn winer_is_not_wiener() {
    is_wine();
    get_version();
    get_build();
    get_host_version();
    get_info();
}

#[test]
fn winer_quality_control() {
    use std::env::var;

    assert_eq!(is_wine(), var("WINER_TEST_VERSION").is_ok());

    assert_eq!(get_version_str(), var("WINER_TEST_VERSION").ok().as_deref());
    assert_eq!(get_build_str(), var("WINER_TEST_BUILD").ok().as_deref());

    let host = get_host_version();
    assert_eq!(
        host.as_ref().and_then(|h| h.sysname_str()),
        var("WINER_TEST_SYSNAME").ok().as_deref(),
    );
    assert_eq!(
        host.as_ref().and_then(|h| h.release_str()),
        var("WINER_TEST_RELEASE").ok().as_deref(),
    );
}

#[test]
fn winer_syscall_extension() {
    use std::env::var;

    let is_wine = var("WINER_TEST_VERSION").is_ok();

    if let Some(info) = get_info().as_ref() {
        assert!(is_wine);
        assert_eq!(
            info.version_str(),
            var("WINER_TEST_VERSION").ok().as_deref(),
        );
        assert_eq!(info.build_str(), var("WINER_TEST_BUILD").ok().as_deref(),);
        assert_eq!(
            info.sysname_str(),
            var("WINER_TEST_SYSNAME").ok().as_deref(),
        );
        assert_eq!(
            info.release_str(),
            var("WINER_TEST_RELEASE").ok().as_deref(),
        );
    } else {
        assert!(!is_wine);
    }
}