use std::collections::HashMap;
use std::sync::RwLock;
use std::time::Duration;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Platform {
MacOS,
Linux,
Windows,
Unknown,
}
impl Platform {
pub fn current() -> Self {
#[cfg(target_os = "macos")]
{
Platform::MacOS
}
#[cfg(target_os = "linux")]
{
Platform::Linux
}
#[cfg(target_os = "windows")]
{
Platform::Windows
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
Platform::Unknown
}
}
}
#[derive(Debug, Clone)]
pub struct PlatformConfig {
pub platform: Platform,
pub recommended_scan_duration: Duration,
pub minimum_scan_duration: Duration,
pub recommended_connection_timeout: Duration,
pub recommended_operation_timeout: Duration,
pub operation_delay: Duration,
pub exposes_mac_address: bool,
pub recommended_scan_retries: u32,
pub scan_retry_delay: Duration,
pub max_concurrent_connections: usize,
}
impl PlatformConfig {
pub fn for_current_platform() -> Self {
Self::for_platform(Platform::current())
}
pub fn for_platform(platform: Platform) -> Self {
match platform {
Platform::MacOS => Self::macos(),
Platform::Linux => Self::linux(),
Platform::Windows => Self::windows(),
Platform::Unknown => Self::default(),
}
}
pub fn macos() -> Self {
Self {
platform: Platform::MacOS,
recommended_scan_duration: Duration::from_secs(8),
minimum_scan_duration: Duration::from_secs(5),
recommended_connection_timeout: Duration::from_secs(10),
recommended_operation_timeout: Duration::from_secs(8),
operation_delay: Duration::from_millis(20),
exposes_mac_address: false,
recommended_scan_retries: 3,
scan_retry_delay: Duration::from_millis(500),
max_concurrent_connections: 5,
}
}
pub fn linux() -> Self {
Self {
platform: Platform::Linux,
recommended_scan_duration: Duration::from_secs(5),
minimum_scan_duration: Duration::from_secs(3),
recommended_connection_timeout: Duration::from_secs(15),
recommended_operation_timeout: Duration::from_secs(10),
operation_delay: Duration::from_millis(30),
exposes_mac_address: true,
recommended_scan_retries: 3,
scan_retry_delay: Duration::from_millis(500),
max_concurrent_connections: 7,
}
}
pub fn windows() -> Self {
Self {
platform: Platform::Windows,
recommended_scan_duration: Duration::from_secs(5),
minimum_scan_duration: Duration::from_secs(3),
recommended_connection_timeout: Duration::from_secs(12),
recommended_operation_timeout: Duration::from_secs(10),
operation_delay: Duration::from_millis(25),
exposes_mac_address: true,
recommended_scan_retries: 3,
scan_retry_delay: Duration::from_millis(500),
max_concurrent_connections: 5,
}
}
}
impl Default for PlatformConfig {
fn default() -> Self {
Self {
platform: Platform::Unknown,
recommended_scan_duration: Duration::from_secs(6),
minimum_scan_duration: Duration::from_secs(4),
recommended_connection_timeout: Duration::from_secs(15),
recommended_operation_timeout: Duration::from_secs(10),
operation_delay: Duration::from_millis(30),
exposes_mac_address: true,
recommended_scan_retries: 3,
scan_retry_delay: Duration::from_millis(500),
max_concurrent_connections: 5,
}
}
}
pub fn current_platform() -> Platform {
Platform::current()
}
pub fn platform_config() -> PlatformConfig {
PlatformConfig::for_current_platform()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceAlias {
pub alias: String,
pub serial: Option<String>,
pub name: Option<String>,
pub mac_address: Option<String>,
pub macos_uuid: Option<String>,
pub notes: Option<String>,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
impl DeviceAlias {
pub fn new(alias: impl Into<String>) -> Self {
let now = time::OffsetDateTime::now_utc()
.format(&time::format_description::well_known::Rfc3339)
.ok();
Self {
alias: alias.into(),
serial: None,
name: None,
mac_address: None,
macos_uuid: None,
notes: None,
created_at: now.clone(),
updated_at: now,
}
}
#[must_use]
pub fn with_serial(mut self, serial: impl Into<String>) -> Self {
self.serial = Some(serial.into());
self
}
#[must_use]
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn with_mac(mut self, mac: impl Into<String>) -> Self {
self.mac_address = Some(mac.into());
self
}
#[must_use]
pub fn with_uuid(mut self, uuid: impl Into<String>) -> Self {
self.macos_uuid = Some(uuid.into());
self
}
#[must_use]
pub fn with_notes(mut self, notes: impl Into<String>) -> Self {
self.notes = Some(notes.into());
self
}
pub fn resolve(&self) -> Option<String> {
let platform = Platform::current();
match platform {
Platform::MacOS => {
self.macos_uuid
.clone()
.or_else(|| self.name.clone())
.or_else(|| self.serial.clone())
}
Platform::Linux | Platform::Windows => {
self.mac_address
.clone()
.or_else(|| self.name.clone())
.or_else(|| self.serial.clone())
}
Platform::Unknown => {
self.name.clone().or_else(|| self.serial.clone())
}
}
}
pub fn matches(&self, identifier: &str) -> bool {
self.serial.as_deref() == Some(identifier)
|| self.name.as_deref() == Some(identifier)
|| self.mac_address.as_deref() == Some(identifier)
|| self.macos_uuid.as_deref() == Some(identifier)
}
pub fn update_identifier(&mut self, identifier: &str) {
let platform = Platform::current();
match platform {
Platform::MacOS => {
self.macos_uuid = Some(identifier.to_string());
}
Platform::Linux | Platform::Windows => {
if identifier.contains('-') && identifier.len() > 20 {
self.macos_uuid = Some(identifier.to_string());
} else {
self.mac_address = Some(identifier.to_string());
}
}
Platform::Unknown => {
self.name = Some(identifier.to_string());
}
}
self.updated_at = time::OffsetDateTime::now_utc()
.format(&time::format_description::well_known::Rfc3339)
.ok();
}
}
#[derive(Debug, Default)]
pub struct AliasStore {
aliases: RwLock<HashMap<String, DeviceAlias>>,
}
impl AliasStore {
pub fn new() -> Self {
Self {
aliases: RwLock::new(HashMap::new()),
}
}
pub fn add(&self, alias: DeviceAlias) {
let mut aliases = self.aliases.write().unwrap_or_else(|e| {
tracing::warn!("Alias store lock was poisoned, recovering");
e.into_inner()
});
aliases.insert(alias.alias.clone(), alias);
}
pub fn get(&self, alias_name: &str) -> Option<DeviceAlias> {
let aliases = self.aliases.read().unwrap_or_else(|e| {
tracing::warn!("Alias store lock was poisoned, recovering");
e.into_inner()
});
aliases.get(alias_name).cloned()
}
pub fn remove(&self, alias_name: &str) -> Option<DeviceAlias> {
let mut aliases = self.aliases.write().unwrap_or_else(|e| {
tracing::warn!("Alias store lock was poisoned, recovering");
e.into_inner()
});
aliases.remove(alias_name)
}
pub fn find_by_identifier(&self, identifier: &str) -> Option<DeviceAlias> {
let aliases = self.aliases.read().unwrap_or_else(|e| {
tracing::warn!("Alias store lock was poisoned, recovering");
e.into_inner()
});
aliases.values().find(|a| a.matches(identifier)).cloned()
}
pub fn all(&self) -> Vec<DeviceAlias> {
let aliases = self.aliases.read().unwrap_or_else(|e| {
tracing::warn!("Alias store lock was poisoned, recovering");
e.into_inner()
});
aliases.values().cloned().collect()
}
pub fn len(&self) -> usize {
let aliases = self.aliases.read().unwrap_or_else(|e| {
tracing::warn!("Alias store lock was poisoned, recovering");
e.into_inner()
});
aliases.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn clear(&self) {
let mut aliases = self.aliases.write().unwrap_or_else(|e| {
tracing::warn!("Alias store lock was poisoned, recovering");
e.into_inner()
});
aliases.clear();
}
pub fn resolve(&self, alias_or_identifier: &str) -> String {
if let Some(alias) = self.get(alias_or_identifier) {
alias
.resolve()
.unwrap_or_else(|| alias_or_identifier.to_string())
} else {
alias_or_identifier.to_string()
}
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
let aliases = self.aliases.read().unwrap_or_else(|e| {
tracing::warn!("Alias store lock was poisoned, recovering");
e.into_inner()
});
serde_json::to_string_pretty(&*aliases)
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
let aliases: HashMap<String, DeviceAlias> = serde_json::from_str(json)?;
Ok(Self {
aliases: RwLock::new(aliases),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_platform_detection() {
let platform = Platform::current();
assert!(matches!(
platform,
Platform::MacOS | Platform::Linux | Platform::Windows | Platform::Unknown
));
}
#[test]
fn test_platform_config_macos() {
let config = PlatformConfig::macos();
assert_eq!(config.platform, Platform::MacOS);
assert!(!config.exposes_mac_address);
assert!(config.recommended_scan_duration >= Duration::from_secs(5));
}
#[test]
fn test_platform_config_linux() {
let config = PlatformConfig::linux();
assert_eq!(config.platform, Platform::Linux);
assert!(config.exposes_mac_address);
}
#[test]
fn test_platform_config_windows() {
let config = PlatformConfig::windows();
assert_eq!(config.platform, Platform::Windows);
assert!(config.exposes_mac_address);
}
#[test]
fn test_current_platform_config() {
let config = PlatformConfig::for_current_platform();
assert!(config.recommended_scan_duration > Duration::ZERO);
assert!(config.recommended_connection_timeout > Duration::ZERO);
assert!(config.max_concurrent_connections > 0);
}
}