use objc2::AnyThread;
use objc2::rc::Retained;
use objc2_foundation::NSString;
use objc2_virtualization::{VZMACAddress, VZNATNetworkDeviceAttachment, VZVirtioNetworkDeviceConfiguration};
use crate::KasouError;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct NetworkConfig {
pub mac_address: Option<String>,
}
pub(crate) fn create_network_device(
config: &NetworkConfig,
) -> Result<Retained<VZVirtioNetworkDeviceConfiguration>, KasouError> {
let device = unsafe { VZVirtioNetworkDeviceConfiguration::new() };
let attachment = unsafe { VZNATNetworkDeviceAttachment::new() };
unsafe { device.setAttachment(Some(&attachment)) };
if let Some(mac) = &config.mac_address {
let vz_mac = parse_mac_address(mac)?;
unsafe { device.setMACAddress(&vz_mac) };
}
Ok(device)
}
fn parse_mac_address(mac: &str) -> Result<Retained<VZMACAddress>, KasouError> {
let ns_mac = NSString::from_str(mac);
let vz_mac = unsafe { VZMACAddress::initWithString(VZMACAddress::alloc(), &ns_mac) };
vz_mac.ok_or_else(|| KasouError::InvalidMac(mac.to_string()))
}
pub fn deterministic_mac(seed: &str, unique_id: &str) -> String {
crate::types::MacAddress::deterministic(seed, unique_id).to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_mac_address_parses() {
let result = parse_mac_address("5a:94:ef:ab:cd:12");
assert!(result.is_ok());
}
#[test]
fn invalid_mac_address_rejected() {
let result = parse_mac_address("not-a-mac");
assert!(result.is_err());
match result {
Err(KasouError::InvalidMac(s)) => assert_eq!(s, "not-a-mac"),
other => panic!("expected InvalidMac, got {other:?}"),
}
}
#[test]
fn deterministic_mac_is_stable() {
let mac1 = deterministic_mac("host", "cid-k3s");
let mac2 = deterministic_mac("host", "cid-k3s");
assert_eq!(mac1, mac2, "same input should produce same MAC");
assert!(mac1.starts_with("52:55:55:"), "should use locally-administered prefix");
}
#[test]
fn deterministic_mac_differs_per_cluster() {
let mac1 = deterministic_mac("host", "cid-k3s");
let mac2 = deterministic_mac("host", "ryn-k3s");
assert_ne!(mac1, mac2, "different clusters should get different MACs");
}
#[test]
fn no_mac_address_uses_random() {
let config = NetworkConfig { mac_address: None };
let result = create_network_device(&config);
assert!(result.is_ok());
}
}