//! Helpers shared by the Linux and Windows user services (0.1.0 semantics). use crate::core::error::CoreError; use once_cell::sync::Lazy; use regex::Regex; use serde_json::Value; use std::collections::BTreeMap; pub const SAMPLE_IPV4: &str = "192.0.2.10"; pub const SAMPLE_IPV6: &str = "2001:db8::10"; pub fn fail(code: &str, message: &str) -> CoreError { CoreError::new(code, message) } /// `String(value ?? "")`. pub fn js_string(value: &Value) -> String { match value { Value::Null => String::new(), Value::String(s) => s.clone(), other => other.to_string(), } } /// JavaScript `Number(text)` for decimal strings (empty → 0, invalid → None). pub fn js_number(text: &str) -> Option { let trimmed = text.trim(); if trimmed.is_empty() { return Some(0.0); } if trimmed.chars().any(|c| c.is_ascii_alphabetic() && c != 'e' && c != 'E') { return None; } trimmed.parse::().ok() } pub fn js_number_value(value: &Value) -> Option { match value { Value::Number(n) => n.as_f64(), Value::String(s) => js_number(s), Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }), Value::Null => Some(0.0), _ => None, } } pub fn truthy(value: &Value) -> bool { match value { Value::Null => false, Value::Bool(b) => *b, Value::Number(n) => n.as_f64().is_some_and(|f| f != 0.0 && !f.is_nan()), Value::String(s) => !s.is_empty(), _ => true, } } pub fn clean_text(value: &str, limit: usize) -> String { let replaced: String = value.chars().map(|c| if (c as u32) < 0x20 || c as u32 == 0x7f { ' ' } else { c }).collect(); let trimmed = replaced.trim(); // JavaScript slices UTF-16 code units; BMP text is the common case. let mut out = String::new(); let mut units = 0; for c in trimmed.chars() { units += c.len_utf16(); if units > limit { break; } out.push(c); } out } pub fn now_iso() -> String { chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true) } static SECTION: Lazy = Lazy::new(|| Regex::new(r"\x1eCHJ_SECTION=([a-z0-9]+)\r?\n").unwrap()); pub fn split_sections(text: &str) -> BTreeMap { let mut sections = BTreeMap::new(); let mut current: Option = None; let mut start = 0; for caps in SECTION.captures_iter(text) { let whole = caps.get(0).unwrap(); if let Some(name) = current.take() { sections.insert(name, text[start..whole.start()].to_string()); } current = Some(caps[1].to_string()); start = whole.end(); } if let Some(name) = current { sections.insert(name, text[start..].to_string()); } sections } /// First `key=value` wins. pub fn key_values(text: &str) -> BTreeMap { let mut values = BTreeMap::new(); for line in text.split('\n') { let line = line.strip_suffix('\r').unwrap_or(line); if let Some(index) = line.find('=') { if index > 0 && !values.contains_key(&line[..index]) { values.insert(line[..index].to_string(), line[index + 1..].to_string()); } } } values } pub fn lines(text: &str) -> Vec<&str> { text.split('\n').map(|l| l.strip_suffix('\r').unwrap_or(l)).filter(|l| !l.is_empty()).collect() } #[derive(Debug, Clone)] pub struct Addresses { pub ipv4: String, pub ipv6: String, pub ipv4_sample: bool, pub ipv6_sample: bool, } impl Addresses { pub fn get(&self, family: &str) -> String { if family == "ipv4" { self.ipv4.clone() } else { self.ipv6.clone() } } pub fn sample(&self, family: &str) -> bool { if family == "ipv4" { self.ipv4_sample } else { self.ipv6_sample } } } pub fn family_addresses(client: &str) -> Addresses { let family = match client.parse::() { Ok(std::net::IpAddr::V4(_)) => 4, Ok(std::net::IpAddr::V6(_)) => 6, Err(_) => 0, }; Addresses { ipv4: if family == 4 { client.to_string() } else { SAMPLE_IPV4.into() }, ipv6: if family == 6 { client.to_string() } else { SAMPLE_IPV6.into() }, ipv4_sample: family != 4, ipv6_sample: family != 6, } }