use sha2::{Digest, Sha256};
use std::path::Path;
#[must_use]
pub fn compute_instance_hash(data_dir: &Path) -> String {
let machine_id = get_machine_id().unwrap_or_else(get_fallback_id);
let path_str = data_dir.canonicalize().map_or_else(
|_| data_dir.to_string_lossy().to_string(),
|p| p.to_string_lossy().to_string(),
);
let mut hasher = Sha256::new();
hasher.update(b"velesdb-instance-v1:"); hasher.update(machine_id.as_bytes());
hasher.update(b":");
hasher.update(path_str.as_bytes());
hex::encode(hasher.finalize())
}
fn get_machine_id() -> Option<String> {
#[cfg(target_os = "linux")]
{
std::fs::read_to_string("/etc/machine-id")
.or_else(|_| std::fs::read_to_string("/var/lib/dbus/machine-id"))
.ok()
.map(|s| s.trim().to_string())
}
#[cfg(target_os = "macos")]
{
std::process::Command::new("ioreg")
.args(["-rd1", "-c", "IOPlatformExpertDevice"])
.output()
.ok()
.and_then(|output| {
let stdout = String::from_utf8_lossy(&output.stdout);
stdout
.lines()
.find(|line| line.contains("IOPlatformUUID"))
.and_then(|line| line.split('"').nth(3))
.map(std::string::ToString::to_string)
})
}
#[cfg(target_os = "windows")]
{
std::process::Command::new("reg")
.args([
"query",
"HKLM\\SOFTWARE\\Microsoft\\Cryptography",
"/v",
"MachineGuid",
])
.output()
.ok()
.and_then(|output| {
let stdout = String::from_utf8_lossy(&output.stdout);
stdout
.lines()
.find(|line| line.contains("MachineGuid"))
.and_then(|line| line.split_whitespace().last())
.map(std::string::ToString::to_string)
})
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
None
}
}
fn get_fallback_id() -> String {
let hostname = hostname::get().map_or_else(
|_| "unknown".to_string(),
|h| h.to_string_lossy().to_string(),
);
let username = whoami::username();
format!("fallback:{hostname}:{username}")
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_instance_hash_stable_across_calls() {
let dir = TempDir::new().expect("Failed to create temp dir");
let hash1 = compute_instance_hash(dir.path());
let hash2 = compute_instance_hash(dir.path());
assert_eq!(hash1, hash2, "Hash should be stable across calls");
}
#[test]
fn test_instance_hash_different_for_different_dirs() {
let dir1 = TempDir::new().expect("Failed to create temp dir 1");
let dir2 = TempDir::new().expect("Failed to create temp dir 2");
let hash1 = compute_instance_hash(dir1.path());
let hash2 = compute_instance_hash(dir2.path());
assert_ne!(hash1, hash2, "Different dirs should have different hashes");
}
#[test]
fn test_instance_hash_is_sha256_hex() {
let dir = TempDir::new().expect("Failed to create temp dir");
let hash = compute_instance_hash(dir.path());
assert_eq!(hash.len(), 64, "SHA256 hex should be 64 chars");
assert!(
hash.chars().all(|c| c.is_ascii_hexdigit()),
"Hash should be hex"
);
}
#[test]
fn test_get_machine_id_does_not_panic() {
let _ = get_machine_id();
}
#[test]
fn test_fallback_id_is_non_empty() {
let fallback = get_fallback_id();
assert!(!fallback.is_empty());
assert!(fallback.starts_with("fallback:"));
}
}