tauri-plugin-deeprespond-im 1.0.0-alpha

深应智能 IM 的 Tauri 插件:本地存储、安全存储、窗口间的锁与消息、系统事件、通知、角标、文件
//! 设备标识(《2.功能概述文档》第 6 节,《1.本地存储》第 2.3 节):`tauri_` 加 32 位十六进制,保存在 `device/device_id`,
//! 同时记下这台电脑标识的哈希(SHA-256,加上应用的 identifier,不保存原值)。数据目录被复制到另一台电脑时
//! (迁移助理、整机克隆)哈希对不上,重新生成设备标识;会话随之因设备标识不符被丢弃。

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,
}

/// 读出或生成设备标识。machine_id 为 None 表示取不到(不做检查)。
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))
}

/// 这台电脑的标识:Windows 的 MachineGuid、macOS 的 IOPlatformUUID、Linux 的 /etc/machine-id。
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);
        // 数据目录复制到另一台电脑(TA-66)
        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);
    }
}