use sha2::{Digest, Sha256};
use std::env;
use std::fs;
use std::process::Command;
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct MachineFingerprint(String);
impl MachineFingerprint {
pub fn generate() -> Self {
let mut hasher = Sha256::new();
let hostname = hostname().unwrap_or_else(|| "unknown".to_string());
hasher.update(hostname.as_bytes());
let username = username().unwrap_or_else(|| "unknown".to_string());
hasher.update(username.as_bytes());
let arch = env::consts::ARCH;
hasher.update(arch.as_bytes());
let os = env::consts::OS;
hasher.update(os.as_bytes());
let cpus = num_cpus().min(256);
hasher.update([cpus as u8]);
let page_size = page_size_log();
hasher.update([page_size as u8]);
if let Some(machine_id) = machine_id() {
hasher.update(machine_id.as_bytes());
}
let result = hasher.finalize();
let hex = encode_hex(&result);
MachineFingerprint(hex[..64].to_string())
}
#[cfg(test)]
pub fn from_hex(hex: &str) -> Self {
MachineFingerprint(hex[..64].to_string())
}
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for MachineFingerprint {
fn default() -> Self {
Self::generate()
}
}
impl std::fmt::Display for MachineFingerprint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
fn hostname() -> Option<String> {
#[cfg(unix)]
{
Command::new("hostname")
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
}
#[cfg(not(unix))]
{
None
}
}
fn username() -> Option<String> {
env::var("USER").or_else(|_| env::var("USERNAME")).ok()
}
fn num_cpus() -> usize {
std::thread::available_parallelism()
.map(|p| p.get())
.unwrap_or(1)
}
fn page_size_log() -> usize {
#[cfg(unix)]
{
use std::fs;
if let Ok(content) = fs::read_to_string("/proc/sys/kernel/osureset_page_size") {
if let Ok(size) = content.trim().parse::<usize>() {
let mut log = 0usize;
let mut s = size;
while s > 1 {
s /= 2;
log += 1;
}
return log.min(16);
}
}
12
}
#[cfg(not(unix))]
{
12
}
}
fn machine_id() -> Option<String> {
#[cfg(unix)]
{
let paths = ["/etc/machine-id", "/var/lib/dbus/machine-id"];
for path in &paths {
if let Ok(content) = fs::read_to_string(path) {
let trimmed = content.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
}
None
}
#[cfg(not(unix))]
{
None
}
}
fn encode_hex(data: &[u8]) -> String {
const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
let mut s = String::with_capacity(data.len() * 2);
for &b in data {
s.push(HEX_CHARS[(b >> 4) as usize] as char);
s.push(HEX_CHARS[(b & 0xf) as usize] as char);
}
s
}