rustenium-identity 0.1.4

A versatile stealth overlay for rustenium
Documentation
use crate::identity::{Browser, Identity, Os};
use crate::error::IdentityError;

/// Build the full version string from the version parts, e.g. [124, 0, 6367, 78] → "124.0.6367.78"
fn full_version(parts: &[u16]) -> String {
    parts.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(".")
}

/// Build the User-Agent string for a given identity.
pub fn build_user_agent(identity: &Identity) -> Result<String, IdentityError> {
    let chrome_full = full_version(&identity.browser_version);

    match (&identity.os, &identity.browser) {
        // §4.1 Chrome on Windows
        (Os::Windows, Browser::Chrome) => {
            let nt_version = windows_nt_version(&identity.os_version);
            Ok(format!(
                "Mozilla/5.0 (Windows NT {}; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36",
                nt_version, chrome_full
            ))
        }

        // §4.2 Chrome on macOS
        (Os::Macintosh, Browser::Chrome) => {
            let osx_underscored = identity.os_version.replace('.', "_");
            Ok(format!(
                "Mozilla/5.0 (Macintosh; Intel Mac OS X {}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36",
                osx_underscored, chrome_full
            ))
        }

        // §4.3 Chrome on Android
        (Os::Android, Browser::Chrome) => {
            let device = identity.device_model.as_deref().unwrap_or("Pixel 7");
            Ok(format!(
                "Mozilla/5.0 (Linux; Android {}; {}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Mobile Safari/537.36",
                identity.os_version, device, chrome_full
            ))
        }

        // §4.3 Edge on Android
        (Os::Android, Browser::Edge) => {
            let device = identity.device_model.as_deref().unwrap_or("Pixel 7");
            Ok(format!(
                "Mozilla/5.0 (Linux; Android {}; {}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Mobile EdgA/{} Safari/537.36",
                identity.os_version, device, chrome_full, chrome_full
            ))
        }

        // Safari on iOS
        (Os::Ios, Browser::Safari) => {
            let os_underscored = identity.os_version.replace('.', "_");
            let nav_platform = identity.platform.navigator_platform.as_str();
            Ok(format!(
                "Mozilla/5.0 ({}; CPU iPhone OS {} like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/{} Mobile/15E148 Safari/604.1",
                nav_platform, os_underscored, full_version(&identity.browser_version)
            ))
        }

        // Chrome on iOS (WebKit-based, uses CriOS)
        (Os::Ios, Browser::Chrome) => {
            let os_underscored = identity.os_version.replace('.', "_");
            let nav_platform = identity.platform.navigator_platform.as_str();
            Ok(format!(
                "Mozilla/5.0 ({}; CPU iPhone OS {} like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/{} Mobile/15E148 Safari/604.1",
                nav_platform, os_underscored, chrome_full
            ))
        }

        // Edge on iOS (WebKit-based, uses EdgiOS)
        (Os::Ios, Browser::Edge) => {
            let os_underscored = identity.os_version.replace('.', "_");
            let nav_platform = identity.platform.navigator_platform.as_str();
            Ok(format!(
                "Mozilla/5.0 ({}; CPU iPhone OS {} like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/{} EdgiOS/{} Mobile/15E148 Safari/604.1",
                nav_platform, os_underscored, full_version(&identity.browser_version), chrome_full
            ))
        }

        // Safari on macOS
        (Os::Macintosh, Browser::Safari) => {
            let osx_underscored = identity.os_version.replace('.', "_");
            Ok(format!(
                "Mozilla/5.0 (Macintosh; Intel Mac OS X {}) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/{} Safari/605.1.15",
                osx_underscored, full_version(&identity.browser_version)
            ))
        }

        // Chrome on Linux
        (Os::Linux, Browser::Chrome) => {
            Ok(format!(
                "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36",
                chrome_full
            ))
        }

        // Edge on Windows
        (Os::Windows, Browser::Edge) => {
            let nt_version = windows_nt_version(&identity.os_version);
            Ok(format!(
                "Mozilla/5.0 (Windows NT {}; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36 Edg/{}",
                nt_version, chrome_full, chrome_full
            ))
        }

        // Edge on macOS
        (Os::Macintosh, Browser::Edge) => {
            let osx_underscored = identity.os_version.replace('.', "_");
            Ok(format!(
                "Mozilla/5.0 (Macintosh; Intel Mac OS X {}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36 Edg/{}",
                osx_underscored, chrome_full, chrome_full
            ))
        }

        (os, browser) => Err(IdentityError::UaError(format!(
            "unsupported OS/browser combination: {:?}/{:?}",
            os, browser
        ))),
    }
}

/// Map common Windows version strings to NT version numbers.
fn windows_nt_version(os_version: &str) -> &str {
    match os_version {
        "11" => "10.0",
        "10" => "10.0",
        "8.1" => "6.3",
        "8" => "6.2",
        "7" => "6.1",
        _ => "10.0",
    }
}