rsmediainfo 0.2.0

Rust wrapper for MediaInfo library
//! Platform detection and library path utilities.
//!
//! Centralizes the per-target conventions for naming the MediaInfo shared
//! library and for deciding where to look for it on disk.

use std::path::{Path, PathBuf};

/// Returns the platform-specific library names to search for.
///
/// Library names are ordered by preference. The function returns the names
/// that should be tried when loading the MediaInfo library dynamically.
///
/// Platform-specific names:
/// - Windows: `MediaInfo.dll`
/// - macOS: `libmediainfo.0.dylib`, `libmediainfo.dylib`
/// - Linux/Unix: `libmediainfo.so.0`
pub fn get_library_names() -> Vec<&'static str> {
    if cfg!(windows) {
        vec!["MediaInfo.dll"]
    } else if cfg!(target_os = "macos") {
        vec!["libmediainfo.0.dylib", "libmediainfo.dylib"]
    } else {
        // Linux and other Unix-like systems
        vec!["libmediainfo.so.0"]
    }
}

/// Checks if the host operating system is Windows.
///
/// The result is decided at compile time via `cfg!(windows)`. Historically
/// some library loaders also branched on legacy Microsoft platforms such as
/// MS-DOS, OS/2, and Windows CE, but those operating systems do not have
/// stable target triples in the Rust ecosystem and cannot run this crate, so
/// they are intentionally not part of the check.
#[allow(dead_code)]
pub fn is_windows() -> bool {
    cfg!(windows)
}

/// Returns a list of library paths to search for the MediaInfo library.
///
/// The search has two stages:
///
/// 1. If `bundled_search_dir` is `Some` and contains one of the platform's
///    expected library file names, that absolute path is returned alone so
///    the loader uses the bundled copy and never falls through to a system
///    install.
/// 2. Otherwise, every platform-default name is returned as a bare file
///    name so the loader can let the operating system resolve it through
///    its usual library search path (`LD_LIBRARY_PATH`, `PATH`, etc.).
///
/// `bundled_search_dir` is intended to be the directory the host application
/// ships its bundled library next to (typically the directory containing the
/// running executable).
pub fn get_library_paths(bundled_search_dir: Option<&Path>) -> Vec<PathBuf> {
    let library_names = get_library_names();
    let mut paths = Vec::new();

    // First, check for bundled library in the specified directory
    if let Some(dir) = bundled_search_dir {
        for name in &library_names {
            let bundled_path = dir.join(name);
            if bundled_path.is_file() {
                // If we find a bundled library, only return that path
                return vec![bundled_path];
            }
        }
    }

    // If no bundled library found, return the library names for system search
    for name in library_names {
        paths.push(PathBuf::from(name));
    }

    paths
}

/// Returns the size of wchar_t on the current platform.
///
/// This is important for FFI string conversions:
/// - Windows: wchar_t is 2 bytes (UTF-16)
/// - Unix/macOS: wchar_t is 4 bytes (UTF-32)
#[allow(dead_code)]
pub fn wchar_size() -> usize {
    if cfg!(windows) {
        2 // UTF-16
    } else {
        4 // UTF-32
    }
}

/// Checks if the current platform uses UTF-16 for wchar_t.
///
/// Returns true on Windows, false on Unix-like systems.
#[allow(dead_code)]
pub fn uses_utf16() -> bool {
    cfg!(windows)
}

/// Returns a descriptive string for the current platform.
///
/// This is useful for debugging and error messages.
#[allow(dead_code)]
pub fn platform_name() -> &'static str {
    if cfg!(windows) {
        "Windows"
    } else if cfg!(target_os = "macos") {
        "macOS"
    } else if cfg!(target_os = "linux") {
        "Linux"
    } else if cfg!(unix) {
        "Unix"
    } else {
        "Unknown"
    }
}

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

    #[test]
    fn test_library_names_not_empty() {
        let names = get_library_names();
        assert!(!names.is_empty());
    }

    #[test]
    fn test_library_names_have_correct_extension() {
        let names = get_library_names();
        for name in names {
            if cfg!(windows) {
                assert!(name.ends_with(".dll"));
            } else if cfg!(target_os = "macos") {
                assert!(name.ends_with(".dylib"));
            } else {
                assert!(name.contains(".so"));
            }
        }
    }

    #[test]
    fn test_wchar_size() {
        let size = wchar_size();
        if cfg!(windows) {
            assert_eq!(size, 2);
        } else {
            assert_eq!(size, 4);
        }
    }

    #[test]
    fn test_uses_utf16() {
        let uses = uses_utf16();
        if cfg!(windows) {
            assert!(uses);
        } else {
            assert!(!uses);
        }
    }

    #[test]
    fn test_platform_name() {
        let name = platform_name();
        assert!(!name.is_empty());
        // The name should be one of the expected values
        assert!(["Windows", "macOS", "Linux", "Unix", "Unknown"].contains(&name));
    }

    #[test]
    fn test_get_library_paths_without_bundled() {
        let paths = get_library_paths(None);
        assert!(!paths.is_empty());
    }

    #[test]
    fn test_get_library_paths_prefers_bundled_dir() {
        let dir = tempdir().expect("failed to create temp directory");
        let name = get_library_names()[0];
        let bundled_path = dir.path().join(name);

        std::fs::write(&bundled_path, b"").expect("failed to create dummy library file");

        let paths = get_library_paths(Some(dir.path()));
        assert_eq!(paths, vec![bundled_path]);
    }

    #[test]
    fn test_is_windows_consistent() {
        // is_windows should match cfg!(windows)
        assert_eq!(is_windows(), cfg!(windows));
    }
}