pub const SDK_ID: &str = "rust";
pub fn sdk_version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
pub fn platform() -> &'static str {
if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else if cfg!(target_os = "windows") {
"windows"
} else {
"unknown"
}
}
const VERSION_MAX: usize = 64;
const PLATFORM_MAX: usize = 32;
const SDK_ID_MAX: usize = 16;
fn clamp(s: &str, max: usize) -> &str {
let mut units = 0;
for (byte_idx, ch) in s.char_indices() {
let next = units + ch.len_utf16();
if next > max {
return &s[..byte_idx];
}
units = next;
}
s
}
const OS_VERSION_MAX: usize = 32;
pub(crate) fn arch() -> Option<&'static str> {
match std::env::consts::ARCH {
"aarch64" => Some("arm64"),
"x86_64" => Some("x86_64"),
_ => None,
}
}
pub(crate) fn os_version() -> Option<&'static str> {
use std::sync::OnceLock;
static CACHE: OnceLock<Option<String>> = OnceLock::new();
CACHE
.get_or_init(|| read_os_version_raw().as_deref().and_then(dotted_numeric))
.as_deref()
}
fn dotted_numeric(raw: &str) -> Option<String> {
let bytes = raw.as_bytes();
let start = bytes.iter().position(u8::is_ascii_digit)?;
let run = &raw[start..];
let end = run
.bytes()
.position(|b| !b.is_ascii_digit() && b != b'.')
.unwrap_or(run.len());
let v = run[..end].trim_end_matches('.');
let well_formed = v.len() <= OS_VERSION_MAX
&& v.split('.')
.all(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()));
well_formed.then(|| v.to_string())
}
const GIB: u64 = 1024 * 1024 * 1024;
fn cpu_cores_bucket(n: usize) -> Option<&'static str> {
Some(match n {
0 => return None,
1..=2 => "1-2",
3..=4 => "3-4",
5..=8 => "5-8",
9..=16 => "9-16",
_ => "17+",
})
}
fn memory_bucket(bytes: u64) -> Option<&'static str> {
Some(match bytes {
0 => return None,
b if b < 4 * GIB => "<4GB",
b if b < 8 * GIB => "4-8GB",
b if b < 16 * GIB => "8-16GB",
b if b < 32 * GIB => "16-32GB",
b if b < 64 * GIB => "32-64GB",
_ => "64GB+",
})
}
pub(crate) fn cpu_cores() -> Option<&'static str> {
use std::sync::OnceLock;
static CACHE: OnceLock<Option<&'static str>> = OnceLock::new();
*CACHE.get_or_init(|| {
std::thread::available_parallelism()
.ok()
.and_then(|n| cpu_cores_bucket(n.get()))
})
}
pub(crate) fn memory() -> Option<&'static str> {
use std::sync::OnceLock;
static CACHE: OnceLock<Option<&'static str>> = OnceLock::new();
*CACHE.get_or_init(|| read_total_memory_bytes().and_then(memory_bucket))
}
#[cfg(target_os = "macos")]
fn read_os_version_raw() -> Option<String> {
let out = std::process::Command::new("sw_vers")
.arg("-productVersion")
.output()
.ok()?;
Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
#[cfg(target_os = "linux")]
fn read_os_version_raw() -> Option<String> {
std::fs::read_to_string("/proc/sys/kernel/osrelease")
.ok()
.map(|s| s.trim().to_string())
}
#[cfg(target_os = "windows")]
fn read_os_version_raw() -> Option<String> {
let out = std::process::Command::new("cmd")
.args(["/c", "ver"])
.output()
.ok()?;
Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
fn read_os_version_raw() -> Option<String> {
None
}
#[cfg(target_os = "macos")]
fn read_total_memory_bytes() -> Option<u64> {
let out = std::process::Command::new("sysctl")
.args(["-n", "hw.memsize"])
.output()
.ok()?;
String::from_utf8_lossy(&out.stdout).trim().parse().ok()
}
#[cfg(target_os = "linux")]
fn read_total_memory_bytes() -> Option<u64> {
let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
let line = meminfo.lines().find(|l| l.starts_with("MemTotal:"))?;
let kib: u64 = line.split_whitespace().nth(1)?.parse().ok()?;
kib.checked_mul(1024)
}
#[cfg(target_os = "windows")]
fn read_total_memory_bytes() -> Option<u64> {
fn first_number(s: &str) -> Option<u64> {
let start = s.bytes().position(|b| b.is_ascii_digit())?;
let run = &s[start..];
let end = run
.bytes()
.position(|b| !b.is_ascii_digit())
.unwrap_or(run.len());
run[..end].parse().ok()
}
let wmic = std::process::Command::new("wmic")
.args(["ComputerSystem", "get", "TotalPhysicalMemory"])
.output()
.ok()
.and_then(|o| first_number(&String::from_utf8_lossy(&o.stdout)));
if wmic.is_some() {
return wmic;
}
let out = std::process::Command::new("powershell")
.args([
"-NoProfile",
"-Command",
"(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory",
])
.output()
.ok()?;
first_number(&String::from_utf8_lossy(&out.stdout))
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
fn read_total_memory_bytes() -> Option<u64> {
None
}
pub fn apply(map: &mut serde_json::Map<String, serde_json::Value>, app_version: Option<&str>) {
map.insert(
"sdk_version".into(),
clamp(sdk_version(), VERSION_MAX).into(),
);
map.insert("platform".into(), clamp(platform(), PLATFORM_MAX).into());
map.insert("sdk".into(), clamp(SDK_ID, SDK_ID_MAX).into());
if let Some(av) = app_version {
map.insert("app_version".into(), clamp(av, VERSION_MAX).into());
}
}
pub fn apply_device(map: &mut serde_json::Map<String, serde_json::Value>) {
if let Some(v) = os_version() {
map.insert("os_version".into(), v.into());
}
if let Some(a) = arch() {
map.insert("arch".into(), a.into());
}
if let Some(c) = cpu_cores() {
map.insert("cpu_cores".into(), c.into());
}
if let Some(m) = memory() {
map.insert("memory".into(), m.into());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn apply_sets_sdk_and_platform_and_optional_app() {
let mut m = serde_json::Map::new();
apply(&mut m, Some("1.2.3"));
assert_eq!(m["sdk_version"], serde_json::json!(sdk_version()));
assert!(m.contains_key("platform"));
assert_eq!(m["sdk"], serde_json::json!("rust"));
assert_eq!(m["app_version"], serde_json::json!("1.2.3"));
let mut m2 = serde_json::Map::new();
apply(&mut m2, None);
assert!(!m2.contains_key("app_version"));
}
#[test]
fn apply_clamps_app_version_to_the_server_cap() {
let mut m = serde_json::Map::new();
apply(&mut m, Some(&"9".repeat(200)));
assert_eq!(m["app_version"].as_str().unwrap().len(), VERSION_MAX);
}
#[test]
fn clamp_respects_char_boundaries() {
let s = "é".repeat(100);
let out = clamp(&s, VERSION_MAX);
assert_eq!(out.chars().count(), VERSION_MAX);
assert!(s.starts_with(out));
}
#[test]
fn clamp_counts_utf16_code_units_like_the_server() {
let s = "😀".repeat(100);
let out = clamp(&s, VERSION_MAX);
assert_eq!(out.encode_utf16().count(), VERSION_MAX);
assert_eq!(out.chars().count(), VERSION_MAX / 2);
assert!(s.starts_with(out));
}
#[test]
fn clamp_rounds_down_when_the_cap_splits_a_pair() {
let s = "😀".repeat(10);
let out = clamp(&s, 5);
assert_eq!(out.encode_utf16().count(), 4);
assert_eq!(out.chars().count(), 2);
}
#[test]
fn clamp_handles_mixed_width_text() {
let s = format!("{}{}", "a".repeat(10), "😀".repeat(50));
let out = clamp(&s, VERSION_MAX);
assert!(out.encode_utf16().count() <= VERSION_MAX);
assert_eq!(out.encode_utf16().count(), VERSION_MAX);
assert!(s.starts_with(out));
}
#[test]
fn dotted_numeric_extracts_from_all_platform_shapes() {
assert_eq!(dotted_numeric("15.5"), Some("15.5".into())); assert_eq!(dotted_numeric("6.8.0-45-generic"), Some("6.8.0".into())); assert_eq!(
dotted_numeric("Microsoft Windows [Version 10.0.22631.3737]"),
Some("10.0.22631.3737".into()) );
assert_eq!(dotted_numeric("14."), Some("14".into())); }
#[test]
fn dotted_numeric_rejects_junk() {
assert_eq!(dotted_numeric("Sonoma"), None);
assert_eq!(dotted_numeric(""), None);
assert_eq!(dotted_numeric(&"1.".repeat(30)), None); }
#[test]
fn device_values_match_the_server_vocabulary() {
if let Some(a) = arch() {
assert!(a == "arm64" || a == "x86_64");
}
if let Some(v) = os_version() {
assert_eq!(dotted_numeric(v).as_deref(), Some(v));
}
}
#[test]
fn apply_device_sets_only_present_fields_and_never_device_class() {
let mut m = serde_json::Map::new();
apply_device(&mut m);
assert_eq!(m.contains_key("os_version"), os_version().is_some());
assert_eq!(m.contains_key("arch"), arch().is_some());
assert!(!m.contains_key("device_class"));
}
#[test]
fn cpu_cores_bucket_boundaries() {
assert_eq!(cpu_cores_bucket(1), Some("1-2"));
assert_eq!(cpu_cores_bucket(2), Some("1-2"));
assert_eq!(cpu_cores_bucket(3), Some("3-4"));
assert_eq!(cpu_cores_bucket(4), Some("3-4"));
assert_eq!(cpu_cores_bucket(5), Some("5-8"));
assert_eq!(cpu_cores_bucket(8), Some("5-8"));
assert_eq!(cpu_cores_bucket(9), Some("9-16"));
assert_eq!(cpu_cores_bucket(16), Some("9-16"));
assert_eq!(cpu_cores_bucket(17), Some("17+"));
assert_eq!(cpu_cores_bucket(128), Some("17+"));
}
#[test]
fn memory_bucket_boundaries() {
const GIB: u64 = 1024 * 1024 * 1024;
assert_eq!(memory_bucket(39 * GIB / 10), Some("<4GB")); assert_eq!(memory_bucket(4 * GIB), Some("4-8GB"));
assert_eq!(memory_bucket(79 * GIB / 10), Some("4-8GB")); assert_eq!(memory_bucket(8 * GIB), Some("8-16GB"));
assert_eq!(memory_bucket(16 * GIB), Some("16-32GB"));
assert_eq!(memory_bucket(32 * GIB), Some("32-64GB"));
assert_eq!(memory_bucket(64 * GIB), Some("64GB+"));
assert_eq!(memory_bucket(128 * GIB), Some("64GB+"));
}
#[test]
fn memory_bucket_does_not_pre_round() {
const GIB: u64 = 1024 * 1024 * 1024;
assert_eq!(memory_bucket(8 * GIB - 1), Some("4-8GB"));
assert_eq!(memory_bucket(16 * GIB - 1), Some("8-16GB"));
assert_eq!(memory_bucket(0), None);
assert_eq!(cpu_cores_bucket(0), None);
}
#[test]
fn device_bucket_values_match_the_server_vocabulary() {
const CORES: [&str; 5] = ["1-2", "3-4", "5-8", "9-16", "17+"];
const MEM: [&str; 6] = ["<4GB", "4-8GB", "8-16GB", "16-32GB", "32-64GB", "64GB+"];
if let Some(c) = cpu_cores() {
assert!(CORES.contains(&c), "unexpected cpu_cores bucket {c:?}");
}
if let Some(m) = memory() {
assert!(MEM.contains(&m), "unexpected memory bucket {m:?}");
}
}
#[test]
fn apply_device_sets_cpu_cores_and_memory_when_readable() {
let mut m = serde_json::Map::new();
apply_device(&mut m);
assert_eq!(m.contains_key("cpu_cores"), cpu_cores().is_some());
assert_eq!(m.contains_key("memory"), memory().is_some());
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
{
assert!(m.contains_key("cpu_cores"));
assert!(m.contains_key("memory"));
}
}
#[test]
fn apply_device_never_sends_raw_hardware_values() {
let mut m = serde_json::Map::new();
apply_device(&mut m);
for k in ["cpu_cores", "memory"] {
if let Some(v) = m.get(k) {
assert!(v.is_string(), "{k} must be a bucket string, not a number");
}
}
assert!(!m.contains_key("cpu_core_count"));
assert!(!m.contains_key("memory_bytes"));
}
#[test]
fn built_in_values_are_within_the_caps() {
assert!(sdk_version().len() <= VERSION_MAX);
assert!(SDK_ID.len() <= SDK_ID_MAX);
assert!(platform().len() <= PLATFORM_MAX);
}
}