use std::fmt;
use crate::options::{CorrelationId, UserAgentSuffix, WorkloadId};
const MAX_USER_AGENT_LENGTH: usize = 255;
const AZSDK_USER_AGENT_PREFIX: &str = "azsdk-rust-";
const SDK_NAME: &str = "cosmos-driver";
const SDK_VERSION: &str = env!("CARGO_PKG_VERSION");
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UserAgent {
full_user_agent: String,
suffix: Option<String>,
}
impl Default for UserAgent {
fn default() -> Self {
Self::new(None::<&str>, None::<&str>)
}
}
impl UserAgent {
fn driver_base_user_agent() -> String {
let os_name = std::env::consts::OS;
let os_arch = std::env::consts::ARCH;
let rust_version = option_env!("RUSTC_VERSION").unwrap_or("unknown");
let mut value = String::with_capacity(
AZSDK_USER_AGENT_PREFIX.len()
+ SDK_NAME.len()
+ 1
+ SDK_VERSION.len()
+ 1
+ os_name.len()
+ 1
+ os_arch.len()
+ 7
+ rust_version.len(),
);
value.push_str(AZSDK_USER_AGENT_PREFIX);
value.push_str(SDK_NAME);
value.push('/');
value.push_str(SDK_VERSION);
value.push(' ');
value.push_str(os_name);
value.push('/');
value.push_str(os_arch);
value.push_str(" rustc/");
value.push_str(rust_version);
value
}
fn base_user_agent(wrapping_sdk_identifier: Option<&str>) -> String {
let driver = Self::driver_base_user_agent();
let wrapping = wrapping_sdk_identifier
.map(strip_non_ascii)
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
match wrapping {
Some(mut w) => {
let reserved_for_suffix = UserAgentSuffix::MAX_LENGTH + 1;
let driver_with_sep = driver.len() + 1;
let preferred_max_wrap =
MAX_USER_AGENT_LENGTH.saturating_sub(driver_with_sep + reserved_for_suffix);
let absolute_max_wrap = MAX_USER_AGENT_LENGTH.saturating_sub(driver_with_sep);
let max_wrap = if preferred_max_wrap == 0 {
absolute_max_wrap
} else {
preferred_max_wrap
};
if w.len() > max_wrap {
w.truncate(max_wrap);
}
if w.is_empty() {
return driver;
}
let mut value = String::with_capacity(w.len() + 1 + driver.len());
value.push_str(&w);
value.push(' ');
value.push_str(&driver);
value
}
None => driver,
}
}
fn new(
wrapping_sdk_identifier: Option<impl AsRef<str>>,
suffix: Option<impl Into<String>>,
) -> Self {
let base = strip_non_ascii(&Self::base_user_agent(
wrapping_sdk_identifier.as_ref().map(AsRef::as_ref),
));
let normalized_suffix = suffix.map(Into::into).map(|s| strip_non_ascii(&s));
let max_suffix_len = MAX_USER_AGENT_LENGTH.saturating_sub(base.len() + 1);
let effective_suffix = normalized_suffix.and_then(|s| {
if s.is_empty() || max_suffix_len == 0 {
None
} else {
Some(s[..s.len().min(max_suffix_len)].to_string())
}
});
let mut full_user_agent = String::with_capacity(
base.len() + effective_suffix.as_ref().map_or(0, |s| 1 + s.len()),
);
full_user_agent.push_str(&base);
if let Some(s) = &effective_suffix {
full_user_agent.push(' ');
full_user_agent.push_str(s);
}
Self {
full_user_agent,
suffix: effective_suffix,
}
}
pub(crate) fn from_wrapping_sdk_identifier(wrapping_sdk_identifier: Option<&str>) -> Self {
Self::new(wrapping_sdk_identifier, None::<&str>)
}
pub(crate) fn from_suffix(
wrapping_sdk_identifier: Option<&str>,
suffix: &UserAgentSuffix,
) -> Self {
Self::new(wrapping_sdk_identifier, Some(suffix.as_str()))
}
pub(crate) fn from_workload_id(
wrapping_sdk_identifier: Option<&str>,
workload_id: WorkloadId,
) -> Self {
Self::new(
wrapping_sdk_identifier,
Some(format!("w{}", workload_id.value())),
)
}
pub(crate) fn from_correlation_id(
wrapping_sdk_identifier: Option<&str>,
correlation_id: &CorrelationId,
) -> Self {
Self::new(wrapping_sdk_identifier, Some(correlation_id.as_str()))
}
pub fn as_str(&self) -> &str {
&self.full_user_agent
}
pub fn suffix(&self) -> Option<&str> {
self.suffix.as_deref()
}
}
impl fmt::Display for UserAgent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.full_user_agent)
}
}
fn strip_non_ascii(input: &str) -> String {
input
.chars()
.map(|c| {
if c.is_ascii() && !c.is_ascii_control() {
c
} else {
'_'
}
})
.collect()
}
pub(crate) fn normalize_wrapping_sdk_identifier(value: &str) -> Option<String> {
let trimmed = value.trim();
if trimmed.is_empty() {
return None;
}
let normalized = strip_non_ascii(trimmed);
if normalized.is_empty() {
None
} else {
Some(normalized)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn user_agent_default_has_base_prefix() {
let ua = UserAgent::default();
assert!(ua.as_str().starts_with("azsdk-rust-cosmos-driver/"));
assert!(ua.suffix().is_none());
}
#[test]
fn user_agent_with_suffix() {
let ua = UserAgent::new(None::<&str>, Some("my-app"));
assert!(ua.as_str().contains("my-app"));
assert_eq!(ua.suffix(), Some("my-app"));
}
#[test]
fn user_agent_from_user_agent_suffix() {
let suffix = UserAgentSuffix::new("myapp-westus2");
let ua = UserAgent::from_suffix(None, &suffix);
assert!(ua.as_str().contains("myapp-westus2"));
}
#[test]
fn user_agent_from_workload_id() {
let workload_id = WorkloadId::new(25);
let ua = UserAgent::from_workload_id(None, workload_id);
assert!(ua.as_str().contains("w25"));
}
#[test]
fn user_agent_from_correlation_id() {
let correlation_id = CorrelationId::new("aks-prod-eastus");
let ua = UserAgent::from_correlation_id(None, &correlation_id);
assert!(ua.as_str().contains("aks-prod-eastus"));
}
#[test]
fn user_agent_strips_non_ascii() {
let input = "test café";
let stripped = strip_non_ascii(input);
assert!(stripped.is_ascii());
}
#[test]
fn user_agent_with_wrapping_sdk_identifier_prepends() {
let ua = UserAgent::from_wrapping_sdk_identifier(Some("azsdk-rust-cosmos/0.34.0"));
assert!(
ua.as_str()
.starts_with("azsdk-rust-cosmos/0.34.0 azsdk-rust-cosmos-driver/"),
"unexpected user agent: {}",
ua.as_str()
);
assert!(ua.suffix().is_none());
}
#[test]
fn user_agent_wrapping_plus_suffix() {
let suffix = UserAgentSuffix::new("myapp-westus2");
let ua = UserAgent::from_suffix(Some("azsdk-rust-cosmos/0.34.0"), &suffix);
let s = ua.as_str();
assert!(
s.starts_with("azsdk-rust-cosmos/0.34.0 azsdk-rust-cosmos-driver/"),
"missing wrapping prefix in: {s}"
);
assert!(s.ends_with(" myapp-westus2"), "missing suffix in: {s}");
}
#[test]
fn user_agent_wrapping_identifier_strips_non_ascii() {
let ua = UserAgent::from_wrapping_sdk_identifier(Some("azsdk-rust-café/0.1.0"));
assert!(ua.as_str().is_ascii());
assert!(ua.as_str().starts_with("azsdk-rust-caf_/0.1.0 "));
}
#[test]
fn user_agent_empty_wrapping_identifier_treated_as_absent() {
let ua_empty = UserAgent::from_wrapping_sdk_identifier(Some(""));
let ua_ws = UserAgent::from_wrapping_sdk_identifier(Some(" "));
let ua_default = UserAgent::default();
assert_eq!(ua_empty.as_str(), ua_default.as_str());
assert_eq!(ua_ws.as_str(), ua_default.as_str());
}
#[test]
fn user_agent_respects_max_length_with_wrapping_and_suffix() {
let long_wrap = format!("azsdk-rust-{}", "x".repeat(200));
let suffix = UserAgentSuffix::new("a".repeat(25));
let ua = UserAgent::from_suffix(Some(&long_wrap), &suffix);
assert!(
ua.as_str().len() <= MAX_USER_AGENT_LENGTH,
"len={} value={}",
ua.as_str().len(),
ua.as_str()
);
}
#[test]
fn user_agent_preserves_suffix_when_wrapping_is_pathological() {
let long_wrap = format!("azsdk-rust-{}", "x".repeat(500));
let suffix = UserAgentSuffix::new("myapp-westus2");
let ua = UserAgent::from_suffix(Some(&long_wrap), &suffix);
assert!(
ua.as_str().len() <= MAX_USER_AGENT_LENGTH,
"exceeded cap: {}",
ua.as_str()
);
assert_eq!(ua.suffix(), Some("myapp-westus2"));
assert!(
ua.as_str().ends_with(" myapp-westus2"),
"suffix lost: {}",
ua.as_str()
);
}
}