#![allow(dead_code)]
use async_trait::async_trait;
use std::sync::OnceLock;
use crate::connection::client_context::ClientContext;
use crate::core::TdsResult;
use crate::io::packet_writer::{PacketWriter, TdsPacketWriter};
use crate::message::login::{Feature, FeatureExtension};
const UNKNOWN_VAL: &str = "Unknown";
const FORMAT_VERSION: &str = "1";
const DEFAULT_DRIVER_NAME: &str = "MS-TDS";
const MAX_ARCH_LEN: usize = 10;
const MAX_OS_TYPE_LEN: usize = 10;
const MAX_OS_DETAILS_LEN: usize = 44;
const MAX_DRIVER_NAME_LEN: usize = 12;
const MAX_DRIVER_VER_LEN: usize = 24;
const MAX_RUNTIME_LEN: usize = 44;
static SYSTEM_ENV_CACHE: OnceLock<SystemEnvironmentInfo> = OnceLock::new();
#[derive(Debug, Clone)]
struct SystemEnvironmentInfo {
architecture: String,
os_type: String,
os_details: String,
fallback_runtime: String,
}
impl SystemEnvironmentInfo {
fn detect() -> Self {
SystemEnvironmentInfo {
architecture: sanitize_field(std::env::consts::ARCH, MAX_ARCH_LEN),
os_type: sanitize_field(get_os_type(), MAX_OS_TYPE_LEN),
os_details: sanitize_field(&get_os_details(), MAX_OS_DETAILS_LEN),
fallback_runtime: sanitize_field(UNKNOWN_VAL, MAX_RUNTIME_LEN),
}
}
}
fn get_os_type_from_name(os_name: &str) -> &'static str {
match os_name {
"windows" => "Windows",
"linux" => "Linux",
"macos" => "macOS",
"freebsd" => "FreeBSD",
"android" => "Android",
_ => UNKNOWN_VAL,
}
}
fn get_os_type() -> &'static str {
get_os_type_from_name(std::env::consts::OS)
}
#[cfg(target_os = "windows")]
fn get_windows_version() -> String {
use windows::Wdk::System::SystemServices::RtlGetVersion;
use windows::Win32::System::SystemInformation::OSVERSIONINFOW;
let mut info: OSVERSIONINFOW = unsafe { std::mem::zeroed() };
info.dwOSVersionInfoSize = std::mem::size_of::<OSVERSIONINFOW>() as u32;
let status = unsafe { RtlGetVersion(&mut info) };
if status.is_ok() {
return format!(
"Windows {}.{}.{}",
info.dwMajorVersion, info.dwMinorVersion, info.dwBuildNumber
);
}
UNKNOWN_VAL.to_string()
}
fn get_os_details() -> String {
#[cfg(target_os = "windows")]
{
get_windows_version()
}
#[cfg(not(target_os = "windows"))]
{
let mut info: libc::utsname = unsafe { std::mem::zeroed() };
if unsafe { libc::uname(&mut info) } == 0 {
let sysname =
unsafe { std::ffi::CStr::from_ptr(info.sysname.as_ptr()) }.to_string_lossy();
let release =
unsafe { std::ffi::CStr::from_ptr(info.release.as_ptr()) }.to_string_lossy();
format!("{} {}", sysname, release)
} else {
UNKNOWN_VAL.to_string()
}
}
}
#[derive(Debug, Clone)]
pub struct UserAgentFeature {
payload: String,
}
impl UserAgentFeature {
pub fn new(context: &ClientContext) -> Self {
let env_info = SYSTEM_ENV_CACHE.get_or_init(SystemEnvironmentInfo::detect);
let driver_name = {
let name = &context.user_agent.library_name;
sanitize_field(
if name.is_empty() {
DEFAULT_DRIVER_NAME
} else {
name
},
MAX_DRIVER_NAME_LEN,
)
};
let driver_version = {
let ver = &context.user_agent.driver_version;
let base_ver = context.driver_version.to_string();
sanitize_field(
if ver.is_empty() { &base_ver } else { ver },
MAX_DRIVER_VER_LEN,
)
};
let runtime_details = {
let v = &context.user_agent.runtime;
if v.is_empty() || v == UNKNOWN_VAL {
env_info.fallback_runtime.clone()
} else {
sanitize_field(v, MAX_RUNTIME_LEN)
}
};
let payload = format!(
"{}|{}|{}|{}|{}|{}|{}",
FORMAT_VERSION,
driver_name,
driver_version,
env_info.architecture,
env_info.os_type,
env_info.os_details,
runtime_details
);
UserAgentFeature { payload }
}
}
#[async_trait]
impl Feature for UserAgentFeature {
fn feature_identifier(&self) -> FeatureExtension {
FeatureExtension::UserAgent
}
fn is_requested(&self) -> bool {
true
}
fn data_length(&self) -> i32 {
let utf16_len = self.payload.encode_utf16().count() * 2;
(size_of::<u8>() + size_of::<i32>() + utf16_len) as i32
}
async fn serialize(&self, packet_writer: &mut PacketWriter) -> TdsResult<()> {
let utf16_len = self.payload.encode_utf16().count() * 2;
packet_writer
.write_byte_async(self.feature_identifier().as_u8())
.await?;
packet_writer.write_i32_async(utf16_len as i32).await?;
packet_writer
.write_string_unicode_async(&self.payload)
.await?;
Ok(())
}
fn deserialize(&mut self, _data: &[u8]) -> TdsResult<()> {
Ok(())
}
fn is_acknowledged(&self) -> bool {
false
}
fn set_acknowledged(&mut self, _acknowledged: bool) {}
fn clone_box(&self) -> Box<dyn Feature> {
Box::new(self.clone())
}
}
fn sanitize_field(val: &str, max_len: usize) -> String {
let mut sanitized = String::with_capacity(val.len().min(max_len));
for ch in val.chars() {
if sanitized.len() >= max_len {
break;
}
if ch.is_ascii_alphanumeric()
|| ch == ' '
|| ch == '.'
|| ch == '+'
|| ch == '_'
|| ch == '-'
{
sanitized.push(ch);
}
}
let trimmed = sanitized.trim();
if trimmed.is_empty() {
return UNKNOWN_VAL.to_string();
}
if trimmed.len() > max_len {
trimmed[..max_len].to_string()
} else {
trimmed.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sanitize_field_valid_chars() {
assert_eq!(
sanitize_field("Rust 1.76.0+build_1-rc", 50),
"Rust 1.76.0+build_1-rc"
);
}
#[test]
fn test_sanitize_field_removes_invalid_chars() {
assert_eq!(
sanitize_field("Ubuntu|Linux>22.04,x86", 50),
"UbuntuLinux22.04x86"
);
}
#[test]
fn test_sanitize_field_truncation() {
let long_str = "VeryLongStringMoreThan12Characters";
assert_eq!(sanitize_field(long_str, 12), "VeryLongStri");
assert_eq!(sanitize_field(long_str, 12).chars().count(), 12);
}
#[test]
fn test_sanitize_field_whitespace_trimming() {
assert_eq!(sanitize_field(" Space Trimmed ", 50), "Space Trimmed");
assert_eq!(sanitize_field(" ", 50), "Unknown"); }
#[test]
fn test_sanitize_field_fallback() {
assert_eq!(sanitize_field("", 50), "Unknown");
assert_eq!(sanitize_field("|||<>|||", 50), "Unknown");
}
#[test]
fn test_os_type_mapping() {
assert_eq!(get_os_type_from_name("windows"), "Windows");
assert_eq!(get_os_type_from_name("linux"), "Linux");
assert_eq!(get_os_type_from_name("macos"), "macOS");
assert_eq!(get_os_type_from_name("freebsd"), "FreeBSD");
assert_eq!(get_os_type_from_name("android"), "Android");
assert_eq!(get_os_type_from_name("ios"), "Unknown");
assert_eq!(get_os_type_from_name("solaris"), "Unknown");
}
#[test]
fn test_user_agent_builder_defaults() {
let context = ClientContext::with_data_source("tcp:test");
let feature = UserAgentFeature::new(&context);
let parts: Vec<&str> = feature.payload.split('|').collect();
assert_eq!(
parts.len(),
7,
"User payload must contain exactly 7 pipe-delimited fields"
);
assert_eq!(parts[0], "1");
assert_eq!(parts[1], "MS-TDS");
assert!(!parts[3].is_empty()); assert!(!parts[4].is_empty()); assert!(!parts[6].is_empty()); }
#[test]
fn test_user_agent_builder_custom_ffi() {
let mut context = ClientContext::with_data_source("tcp:test");
context.library_name = "mssql-python".to_string();
context.user_agent.set_library_name("MS-PYTHON".to_string());
context.user_agent.set_runtime("CPython 3.12.3".to_string());
let feature = UserAgentFeature::new(&context);
let parts: Vec<&str> = feature.payload.split('|').collect();
assert_eq!(parts[1], "MS-PYTHON"); assert_eq!(parts[6], "CPython 3.12.3"); }
#[test]
fn test_system_environment_info_detection() {
let env_info = SystemEnvironmentInfo::detect();
assert!(
!env_info.architecture.is_empty(),
"Architecture should be populated"
);
assert!(!env_info.os_type.is_empty(), "OS type should be populated");
assert!(
!env_info.os_details.is_empty(),
"OS details should be populated"
);
assert_eq!(
env_info.fallback_runtime, "Unknown",
"Fallback runtime should default to Unknown"
);
println!("Environment APIs detected on this test run:");
println!(" Architecture: {}", env_info.architecture);
println!(" OS Type: {}", env_info.os_type);
println!(" OS Details: {}", env_info.os_details);
println!(" Fallback Runtime: {}", env_info.fallback_runtime);
}
#[test]
#[cfg(target_os = "windows")]
fn test_get_windows_version_no_panic() {
let version = get_windows_version();
assert!(!version.is_empty());
}
#[test]
#[cfg(not(target_os = "windows"))]
fn test_get_os_details_execution() {
let os_details = get_os_details();
assert!(!os_details.is_empty());
assert_ne!(os_details, UNKNOWN_VAL);
}
#[test]
fn test_useragent_feature_methods() {
let context = ClientContext::with_data_source("tcp:test");
let mut feature = UserAgentFeature::new(&context);
assert!(!feature.is_acknowledged());
let buf = bytes::BytesMut::new();
assert!(feature.deserialize(&buf).is_ok());
}
}