use std::fs;
use std::path::Path;
use rand::RngCore;
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use crate::error::Result;
use crate::paths::write_atomic;
#[derive(Clone, Debug)]
pub struct Device {
pub id: String,
pub regenerated: bool,
pub unbound: bool,
}
pub fn load_or_create(data_dir: &Path, identifier: &str, machine_id: Option<&str>) -> Result<Device> {
let file = data_dir.join("device").join("device_id");
let machine = machine_id.map(|m| machine_hash(identifier, m));
let stored: Option<Value> = fs::read_to_string(&file).ok().and_then(|t| serde_json::from_str(&t).ok());
if let Some(stored) = &stored {
let id = stored.get("device_id").and_then(Value::as_str).filter(|id| valid_id(id));
let hash = stored.get("machine").and_then(Value::as_str);
if let Some(id) = id {
match (&machine, hash) {
(Some(current), Some(saved)) if current != saved => {
log::warn!("这台电脑的标识与设备标识中记下的不同(数据目录被复制到了另一台电脑),重新生成设备标识");
}
(Some(current), None) => {
write_atomic(&file, json!({ "v": 1, "device_id": id, "machine": current }).to_string().as_bytes())?;
return Ok(Device { id: id.to_string(), regenerated: false, unbound: false });
}
_ => return Ok(Device { id: id.to_string(), regenerated: false, unbound: machine.is_none() }),
}
}
}
if machine.is_none() {
log::info!("取不到这台电脑的标识,设备标识不与电脑绑定");
}
let mut bytes = [0u8; 16];
rand::rng().fill_bytes(&mut bytes);
let id = format!("tauri_{}", hex::encode(bytes));
let mut value = json!({ "v": 1, "device_id": id });
if let Some(m) = &machine {
value["machine"] = Value::String(m.clone());
}
write_atomic(&file, value.to_string().as_bytes())?;
Ok(Device { id, regenerated: true, unbound: machine.is_none() })
}
fn machine_hash(identifier: &str, machine_id: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(identifier.as_bytes());
hasher.update(b":");
hasher.update(machine_id.trim().as_bytes());
hex::encode(hasher.finalize())
}
fn valid_id(id: &str) -> bool {
!id.is_empty() && id.len() <= 64 && id.bytes().all(|b| (0x21..=0x7e).contains(&b))
}
pub fn machine_id() -> Option<String> {
machine_uid::get().ok().map(|s| s.trim().to_string()).filter(|s| !s.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stable_then_regenerated_on_other_machine() {
let dir = tempfile::tempdir().unwrap();
let first = load_or_create(dir.path(), "com.example.app", Some("machine-a")).unwrap();
assert!(first.regenerated);
assert!(first.id.starts_with("tauri_") && first.id.len() == 38);
let again = load_or_create(dir.path(), "com.example.app", Some("machine-a")).unwrap();
assert_eq!(again.id, first.id);
assert!(!again.regenerated);
let moved = load_or_create(dir.path(), "com.example.app", Some("machine-b")).unwrap();
assert_ne!(moved.id, first.id);
assert!(moved.regenerated);
let text = fs::read_to_string(dir.path().join("device/device_id")).unwrap();
assert!(!text.contains("machine-b"));
}
#[test]
fn unbound_when_machine_id_unavailable() {
let dir = tempfile::tempdir().unwrap();
let first = load_or_create(dir.path(), "x", None).unwrap();
assert!(first.unbound);
let again = load_or_create(dir.path(), "x", None).unwrap();
assert_eq!(again.id, first.id);
let bound = load_or_create(dir.path(), "x", Some("m")).unwrap();
assert_eq!(bound.id, first.id);
assert!(!bound.unbound);
}
}