Skip to main content

atomcode_telemetry/
identity.rs

1//! Device identity. `device_id` persists in `$ATOMCODE_HOME/device_id`.
2
3use anyhow::{Context, Result};
4use std::env;
5use std::fs;
6use std::path::{Path, PathBuf};
7use uuid::Uuid;
8
9pub fn load_or_create(atomcode_dir: &Path) -> Result<Uuid> {
10    let path = atomcode_dir.join("device_id");
11    match fs::read_to_string(&path) {
12        Ok(s) => Uuid::parse_str(s.trim())
13            .with_context(|| format!("device_id file corrupt at {}", path.display())),
14        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
15            fs::create_dir_all(atomcode_dir)
16                .with_context(|| format!("creating {}", atomcode_dir.display()))?;
17            let id = Uuid::new_v4();
18            fs::write(&path, id.to_string())
19                .with_context(|| format!("writing {}", path.display()))?;
20            Ok(id)
21        }
22        Err(e) => Err(e).with_context(|| format!("reading {}", path.display())),
23    }
24}
25
26/// Get the real user's home directory, accounting for sudo scenarios.
27/// 
28/// When running under sudo, `dirs::home_dir()` returns root's home directory
29/// because $HOME is set to /root. This function checks for SUDO_USER and
30/// attempts to use that information.
31/// 
32/// Note: This crate forbids unsafe code, so we cannot use getpwnam_r.
33/// Instead, we use environment variables and construct the path.
34pub fn real_home_dir() -> Option<PathBuf> {
35    // Check if we're running under sudo
36    if let Ok(sudo_user) = env::var("SUDO_USER") {
37        // Try to construct the home path from SUDO_USER
38        // On Linux: /home/{user} or /root for root
39        // On macOS: /Users/{user}
40        if cfg!(target_os = "macos") {
41            return Some(PathBuf::from("/Users").join(sudo_user));
42        } else if cfg!(target_os = "linux") {
43            if sudo_user == "root" {
44                return Some(PathBuf::from("/root"));
45            }
46            return Some(PathBuf::from("/home").join(sudo_user));
47        }
48    }
49    
50    // Fall back to the standard home directory
51    dirs::home_dir()
52}
53
54/// Return the AtomCode data directory, respecting the `ATOMCODE_HOME`
55/// environment variable. When `ATOMCODE_HOME` is set, it IS the data root
56/// (no `.atomcode` suffix is appended). Otherwise falls back to
57/// `$HOME/.atomcode`, or `./.atomcode` when `$HOME` cannot be resolved.
58///
59/// This mirrors the logic in `atomcode_core::config::Config::config_dir()`
60/// but is implemented independently to avoid a circular dependency between
61/// the `atomcode-telemetry` and `atomcode-core` crates.
62pub fn default_atomcode_dir() -> PathBuf {
63    if let Some(p) = env::var("ATOMCODE_HOME").ok().filter(|s| !s.is_empty()) {
64        PathBuf::from(p)
65    } else {
66        real_home_dir().unwrap_or_else(|| PathBuf::from(".")).join(".atomcode")
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use tempfile::TempDir;
74
75    #[test]
76    fn creates_on_first_call_then_reads_same_id() {
77        let dir = TempDir::new().unwrap();
78        let id1 = load_or_create(dir.path()).unwrap();
79        let id2 = load_or_create(dir.path()).unwrap();
80        assert_eq!(id1, id2);
81        assert!(dir.path().join("device_id").exists());
82    }
83
84    #[test]
85    fn rejects_corrupt_file() {
86        let dir = TempDir::new().unwrap();
87        std::fs::write(dir.path().join("device_id"), "not-a-uuid").unwrap();
88        assert!(load_or_create(dir.path()).is_err());
89    }
90
91    #[test]
92    fn trims_whitespace_in_file() {
93        let dir = TempDir::new().unwrap();
94        let id = Uuid::new_v4();
95        std::fs::write(dir.path().join("device_id"), format!("{}\n\n", id)).unwrap();
96        let got = load_or_create(dir.path()).unwrap();
97        assert_eq!(id, got);
98    }
99}