use crate::error::{LicenseError, LicenseResult};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HardwareBinding {
#[serde(default)]
pub mac_addresses: Vec<String>,
#[serde(default)]
pub disk_ids: Vec<String>,
#[serde(default)]
pub host_names: Vec<String>,
#[serde(default)]
pub custom_ids: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct HardwareInfo {
pub mac_addresses: Vec<String>,
pub disk_ids: Vec<String>,
pub hostname: String,
}
impl HardwareInfo {
pub fn get() -> LicenseResult<Self> {
let mac_addresses = get_mac_addresses()?;
let disk_ids = get_disk_ids()?;
let hostname = get_hostname()?;
Ok(HardwareInfo {
mac_addresses,
disk_ids,
hostname,
})
}
pub fn matches_binding(&self, binding: &HardwareBinding) -> LicenseResult<()> {
if !binding.mac_addresses.is_empty()
&& !contains_any(&self.mac_addresses, &binding.mac_addresses)
{
return Err(LicenseError::HardwareBinding {
reason: "MAC address mismatch".to_string(),
});
}
if !binding.disk_ids.is_empty() && !contains_any(&self.disk_ids, &binding.disk_ids) {
return Err(LicenseError::HardwareBinding {
reason: "Disk ID mismatch".to_string(),
});
}
if !binding.host_names.is_empty() && !binding.host_names.contains(&self.hostname) {
return Err(LicenseError::HardwareBinding {
reason: "Hostname mismatch".to_string(),
});
}
Ok(())
}
}
fn contains_any(list1: &[String], list2: &[String]) -> bool {
let set2: HashSet<&String> = list2.iter().collect();
list1.iter().any(|item| set2.contains(item))
}
fn get_mac_addresses() -> LicenseResult<Vec<String>> {
#[cfg(target_os = "linux")]
{
get_linux_mac_addresses()
}
#[cfg(target_os = "windows")]
{
get_windows_mac_addresses()
}
#[cfg(target_os = "macos")]
{
get_macos_mac_addresses()
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
{
Ok(vec!["unknown-mac".to_string()])
}
}
fn get_disk_ids() -> LicenseResult<Vec<String>> {
#[cfg(target_os = "linux")]
{
get_linux_disk_ids()
}
#[cfg(target_os = "windows")]
{
get_windows_disk_ids()
}
#[cfg(target_os = "macos")]
{
get_macos_disk_ids()
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
{
Ok(vec!["unknown-disk".to_string()])
}
}
fn get_hostname() -> LicenseResult<String> {
std::env::var("HOSTNAME")
.or_else(|_| std::env::var("COMPUTERNAME"))
.or_else(|_| {
std::process::Command::new("hostname")
.output()
.map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string())
.map_err(|e| LicenseError::Hardware(format!("Failed to get hostname: {}", e)))
})
.map_err(|e| LicenseError::Hardware(format!("Failed to get hostname: {}", e)))
}
#[cfg(target_os = "linux")]
fn get_linux_mac_addresses() -> LicenseResult<Vec<String>> {
use std::fs;
let mut mac_addresses = Vec::new();
if let Ok(entries) = fs::read_dir("/sys/class/net") {
for entry in entries.flatten() {
let interface_name = entry.file_name();
let interface_str = interface_name.to_string_lossy();
if interface_str == "lo" {
continue;
}
let address_path = format!("/sys/class/net/{}/address", interface_str);
if let Ok(address) = fs::read_to_string(&address_path) {
let mac = address.trim().to_string();
if !mac.is_empty() && mac != "00:00:00:00:00:00" {
mac_addresses.push(mac);
}
}
}
}
if mac_addresses.is_empty() {
mac_addresses.push("linux-mac-fallback".to_string());
}
Ok(mac_addresses)
}
#[cfg(target_os = "linux")]
fn get_linux_disk_ids() -> LicenseResult<Vec<String>> {
use std::process::Command;
if let Ok(output) = Command::new("lsblk")
.args(["-ndo", "SERIAL", "-d"])
.output()
{
let serials: Vec<String> = String::from_utf8_lossy(&output.stdout)
.lines()
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty())
.collect();
if !serials.is_empty() {
return Ok(serials);
}
}
if let Ok(entries) = std::fs::read_dir("/dev/disk/by-id") {
let disk_ids: Vec<String> = entries
.flatten()
.map(|entry| entry.file_name().to_string_lossy().to_string())
.collect();
if !disk_ids.is_empty() {
return Ok(disk_ids);
}
}
Ok(vec!["linux-disk-fallback".to_string()])
}
#[cfg(target_os = "windows")]
fn get_windows_mac_addresses() -> LicenseResult<Vec<String>> {
use std::process::Command;
if let Ok(output) = Command::new("wmic")
.args([
"path",
"Win32_NetworkAdapter",
"where",
"NetConnectionStatus=2",
"get",
"MACAddress",
])
.output()
{
let mac_addresses: Vec<String> = String::from_utf8_lossy(&output.stdout)
.lines()
.skip(1) .map(|line| line.trim().to_string())
.filter(|line| !line.is_empty())
.collect();
if !mac_addresses.is_empty() {
return Ok(mac_addresses);
}
}
Ok(vec!["windows-mac-fallback".to_string()])
}
#[cfg(target_os = "windows")]
fn get_windows_disk_ids() -> LicenseResult<Vec<String>> {
use std::process::Command;
if let Ok(output) = Command::new("wmic")
.args(["diskdrive", "get", "SerialNumber"])
.output()
{
let disk_ids: Vec<String> = String::from_utf8_lossy(&output.stdout)
.lines()
.skip(1) .map(|line| line.trim().to_string())
.filter(|line| !line.is_empty())
.collect();
if !disk_ids.is_empty() {
return Ok(disk_ids);
}
}
Ok(vec!["windows-disk-fallback".to_string()])
}
#[cfg(target_os = "macos")]
fn get_macos_mac_addresses() -> LicenseResult<Vec<String>> {
use std::process::Command;
if let Ok(output) = Command::new("ifconfig").output() {
let mut mac_addresses = Vec::new();
let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines() {
if line.contains("ether ") {
if let Some(mac_start) = line.find("ether ") {
let mac_part = &line[mac_start + 6..];
if let Some(mac_end) = mac_part.find(' ') {
let mac = mac_part[..mac_end].trim().to_string();
if !mac.is_empty() && mac != "00:00:00:00:00:00" {
mac_addresses.push(mac);
}
}
}
}
}
if !mac_addresses.is_empty() {
return Ok(mac_addresses);
}
}
Ok(vec!["macos-mac-fallback".to_string()])
}
#[cfg(target_os = "macos")]
fn get_macos_disk_ids() -> LicenseResult<Vec<String>> {
use std::process::Command;
if let Ok(output) = Command::new("diskutil")
.args(["info", "/dev/disk0"])
.output()
{
let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines() {
if line.contains("Serial Number") {
if let Some(colon_pos) = line.find(':') {
let serial = line[colon_pos + 1..].trim().to_string();
if !serial.is_empty() {
return Ok(vec![serial]);
}
}
}
}
}
Ok(vec!["macos-disk-fallback".to_string()])
}