diff --git a/crates/utils/src/sys/user_agent.rs b/crates/utils/src/sys/user_agent.rs index 60700904..28ed7dd6 100644 --- a/crates/utils/src/sys/user_agent.rs +++ b/crates/utils/src/sys/user_agent.rs @@ -13,9 +13,11 @@ // limitations under the License. use rustfs_config::VERSION; +use std::borrow::Cow; use std::env; use std::fmt; -#[cfg(not(any(target_os = "openbsd", target_os = "freebsd", target_os = "netbsd")))] +use std::sync::OnceLock; +#[cfg(not(target_os = "openbsd"))] use sysinfo::System; /// Business Type Enumeration @@ -25,7 +27,7 @@ pub enum ServiceType { Core, Event, Logger, - Custom(String), + Custom(Cow<'static, str>), } impl ServiceType { @@ -35,71 +37,65 @@ impl ServiceType { ServiceType::Core => "core", ServiceType::Event => "event", ServiceType::Logger => "logger", - ServiceType::Custom(s) => s.as_str(), + ServiceType::Custom(s) => s, } } } /// UserAgent structure to hold User-Agent information /// including OS platform, architecture, version, and service type. -/// It provides methods to generate a formatted User-Agent string. -/// # Examples -/// ``` -/// use rustfs_utils::{get_user_agent, ServiceType}; -/// -/// let ua = get_user_agent(ServiceType::Core); -/// println!("User-Agent: {}", ua); -/// ``` #[derive(Debug)] struct UserAgent { - os_platform: String, - arch: String, - version: String, + os_platform: &'static str, + arch: &'static str, + version: &'static str, service: ServiceType, } +static OS_PLATFORM: OnceLock = OnceLock::new(); + impl UserAgent { /// Create a new UserAgent instance and accept business type parameters - /// - /// # Arguments - /// * `service` - The type of service for which the User-Agent is being created. - /// # Returns - /// A new instance of `UserAgent` with the current OS platform, architecture, version, and service type. fn new(service: ServiceType) -> Self { - let os_platform = Self::get_os_platform(); - let arch = env::consts::ARCH.to_string(); - let version = VERSION.to_string(); - UserAgent { - os_platform, - arch, - version, + os_platform: Self::get_os_platform(), + arch: env::consts::ARCH, + version: VERSION, service, } } - /// Obtain operating system platform information - fn get_os_platform() -> String { - if cfg!(target_os = "windows") { - Self::get_windows_platform() - } else if cfg!(target_os = "macos") { - Self::get_macos_platform() - } else if cfg!(target_os = "linux") { - Self::get_linux_platform() - } else { - "Unknown".to_string() - } + /// Obtain operating system platform information using a thread-safe cache. + /// + /// The value is computed once on first use via `OnceLock` and then reused + /// for all subsequent calls for the lifetime of the program. + fn get_os_platform() -> &'static str { + OS_PLATFORM.get_or_init(|| { + if cfg!(target_os = "windows") { + Self::get_windows_platform() + } else if cfg!(target_os = "macos") { + Self::get_macos_platform() + } else if cfg!(target_os = "linux") { + Self::get_linux_platform() + } else if cfg!(target_os = "freebsd") { + Self::get_freebsd_platform() + } else if cfg!(target_os = "netbsd") { + Self::get_netbsd_platform() + } else { + "Unknown".to_string() + } + }) } /// Get Windows platform information #[cfg(windows)] fn get_windows_platform() -> String { - // Priority to using sysinfo to get versions - let version = match System::os_version() { - Some(version) => version, - None => "Windows NT Unknown".to_string(), - }; - format!("Windows NT {version}") + let version = System::os_version().unwrap_or_else(|| "NT Unknown".to_string()); + if version.starts_with("Windows") { + version + } else { + format!("Windows NT {version}") + } } #[cfg(not(windows))] @@ -110,16 +106,14 @@ impl UserAgent { /// Get macOS platform information #[cfg(target_os = "macos")] fn get_macos_platform() -> String { - let binding = System::os_version().unwrap_or("14.5.0".to_string()); - let version = binding.split('.').collect::>(); - let major = version.first().unwrap_or(&"14").to_string(); - let minor = version.get(1).unwrap_or(&"5").to_string(); - let patch = version.get(2).unwrap_or(&"0").to_string(); + let version_str = System::os_version().unwrap_or_else(|| "14.0.0".to_string()); + let mut parts = version_str.split('.'); + let major = parts.next().unwrap_or("14"); + let minor = parts.next().unwrap_or("0"); + let patch = parts.next().unwrap_or("0"); - let arch = env::consts::ARCH; - let cpu_info = if arch == "aarch64" { "Apple" } else { "Intel" }; + let cpu_info = if env::consts::ARCH == "aarch64" { "Apple" } else { "Intel" }; - // Convert to User-Agent format format!("Macintosh; {cpu_info} Mac OS X {major}_{minor}_{patch}") } @@ -131,40 +125,47 @@ impl UserAgent { /// Get Linux platform information #[cfg(target_os = "linux")] fn get_linux_platform() -> String { - format!("X11; {}", System::long_os_version().unwrap_or("Linux Unknown".to_string())) + let os_name = System::long_os_version().unwrap_or_else(|| "Linux Unknown".to_string()); + format!("X11; {os_name}") } #[cfg(not(target_os = "linux"))] fn get_linux_platform() -> String { "N/A".to_string() } + + #[cfg(target_os = "freebsd")] + fn get_freebsd_platform() -> String { + format!("FreeBSD; {}", env::consts::ARCH) + } + + #[cfg(not(target_os = "freebsd"))] + fn get_freebsd_platform() -> String { + "N/A".to_string() + } + + #[cfg(target_os = "netbsd")] + fn get_netbsd_platform() -> String { + format!("NetBSD; {}", env::consts::ARCH) + } + + #[cfg(not(target_os = "netbsd"))] + fn get_netbsd_platform() -> String { + "N/A".to_string() + } } -/// Implement Display trait to format User-Agent impl fmt::Display for UserAgent { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.service == ServiceType::Basis { - return write!(f, "Mozilla/5.0 ({}; {}) RustFS/{}", self.os_platform, self.arch, self.version); + write!(f, "Mozilla/5.0 ({}; {}) RustFS/{}", self.os_platform, self.arch, self.version)?; + if self.service != ServiceType::Basis { + write!(f, " ({})", self.service.as_str())?; } - write!( - f, - "Mozilla/5.0 ({}; {}) RustFS/{} ({})", - self.os_platform, - self.arch, - self.version, - self.service.as_str() - ) + Ok(()) } } /// Get the User-Agent string and accept business type parameters -/// -/// # Arguments -/// * `service` - The type of service for which the User-Agent is being created. -/// -/// # Returns -/// A formatted User-Agent string. -/// pub fn get_user_agent(service: ServiceType) -> String { UserAgent::new(service).to_string() } @@ -173,58 +174,33 @@ pub fn get_user_agent(service: ServiceType) -> String { mod tests { use super::*; use rustfs_config::VERSION; - use tracing::debug; + #[test] fn test_user_agent_format_basis() { let ua = get_user_agent(ServiceType::Basis); assert!(ua.starts_with("Mozilla/5.0")); - assert!(ua.contains(&format!("RustFS/{VERSION}").to_string())); - debug!("Basic User-Agent: {}", ua); + assert!(ua.contains(&format!("RustFS/{VERSION}"))); + assert!(!ua.contains("(basis)")); } #[test] fn test_user_agent_format_core() { let ua = get_user_agent(ServiceType::Core); - assert!(ua.starts_with("Mozilla/5.0")); - assert!(ua.contains(&format!("RustFS/{VERSION} (core)").to_string())); - debug!("Core User-Agent: {}", ua); - } - - #[test] - fn test_user_agent_format_event() { - let ua = get_user_agent(ServiceType::Event); - assert!(ua.starts_with("Mozilla/5.0")); - assert!(ua.contains(&format!("RustFS/{VERSION} (event)").to_string())); - debug!("Event User-Agent: {}", ua); - } - - #[test] - fn test_user_agent_format_logger() { - let ua = get_user_agent(ServiceType::Logger); - assert!(ua.starts_with("Mozilla/5.0")); - assert!(ua.contains(&format!("RustFS/{VERSION} (logger)").to_string())); - debug!("Logger User-Agent: {}", ua); + assert!(ua.contains(&format!("RustFS/{VERSION} (core)"))); } #[test] fn test_user_agent_format_custom() { - let ua = get_user_agent(ServiceType::Custom("monitor".to_string())); - assert!(ua.starts_with("Mozilla/5.0")); - assert!(ua.contains(&format!("RustFS/{VERSION} (monitor)").to_string())); - debug!("Monitor User-Agent: {}", ua); + let ua = get_user_agent(ServiceType::Custom("monitor".into())); + assert!(ua.contains(&format!("RustFS/{VERSION} (monitor)"))); } #[test] - fn test_all_service_type() { - // Example: Generate User-Agents of Different Business Types - let ua_core = get_user_agent(ServiceType::Core); - let ua_event = get_user_agent(ServiceType::Event); - let ua_logger = get_user_agent(ServiceType::Logger); - let ua_custom = get_user_agent(ServiceType::Custom("monitor".to_string())); - - debug!("Core User-Agent: {}", ua_core); - debug!("Event User-Agent: {}", ua_event); - debug!("Logger User-Agent: {}", ua_logger); - debug!("Custom User-Agent: {}", ua_custom); + fn test_os_platform_caching() { + let ua1 = UserAgent::new(ServiceType::Basis); + let ua2 = UserAgent::new(ServiceType::Basis); + assert_eq!(ua1.os_platform, ua2.os_platform); + // Ensure they point to the same static memory + assert!(std::ptr::eq(ua1.os_platform.as_ptr(), ua2.os_platform.as_ptr())); } }