Skip to main content

lighty_core/
app_state.rs

1//! Process-wide launcher paths resolved via the `dirs` crate.
2
3use std::path::{Path, PathBuf};
4
5use once_cell::sync::OnceCell;
6
7use crate::errors::{AppStateError, AppStateResult};
8
9const CLIENT_ID_FILE: &str = "client_id";
10
11/// Resolved per-launcher paths.
12#[derive(Debug, Clone)]
13pub struct LauncherPaths {
14    pub name: String,
15    pub data_dir: PathBuf,
16    pub config_dir: PathBuf,
17    pub cache_dir: PathBuf,
18}
19
20static PATHS: OnceCell<LauncherPaths> = OnceCell::new();
21static CLIENT_ID: OnceCell<String> = OnceCell::new();
22
23/// Zero-sized handle for the global launcher paths.
24pub struct AppState;
25
26impl AppState {
27    /// Initialises the global launcher paths. Call once at startup.
28    ///
29    /// `name` becomes the per-launcher subdirectory under the OS-
30    /// standard data/config/cache bases. Returns
31    /// [`AppStateError::AlreadyInitialized`] on a second call.
32    pub fn init(name: impl Into<String>) -> AppStateResult<()> {
33        let name = name.into();
34        let data_dir = dirs::data_dir()
35            .ok_or(AppStateError::MissingPlatformDir("data"))?
36            .join(&name);
37        let config_dir = dirs::config_dir()
38            .ok_or(AppStateError::MissingPlatformDir("config"))?
39            .join(&name);
40        let cache_dir = dirs::cache_dir()
41            .ok_or(AppStateError::MissingPlatformDir("cache"))?
42            .join(&name);
43        PATHS
44            .set(LauncherPaths { name, data_dir, config_dir, cache_dir })
45            .map_err(|_| AppStateError::AlreadyInitialized)?;
46
47        // Startup diagnostic: an antivirus or a cracked-launcher installer can
48        // blackhole login in the hosts file long before the first request.
49        match crate::hosts::blocked_launcher_domains(&[]) {
50            Ok(entries) if !entries.is_empty() => crate::trace_warn!(
51                entries = %entries.join(", "),
52                "Hosts file intercepts domains the launcher needs"
53            ),
54            Err(err) => crate::trace_debug!(error = %err, "Could not read the hosts file"),
55            _ => {}
56        }
57
58        Ok(())
59    }
60
61    /// Returns the resolved launcher paths.
62    ///
63    /// Panics with a clear message if [`Self::init`] hasn't been
64    /// called — that's a programmer error, not a runtime condition.
65    pub fn paths() -> &'static LauncherPaths {
66        PATHS.get().expect(
67            "AppState::init(\"<launcher-name>\") must be called once at startup",
68        )
69    }
70
71    /// Launcher name as supplied to [`Self::init`].
72    pub fn name() -> &'static str {
73        &Self::paths().name
74    }
75
76    /// Persistent data directory (instances live here).
77    pub fn data_dir() -> &'static Path {
78        &Self::paths().data_dir
79    }
80
81    /// User configuration directory (the bundled JRE lives here).
82    pub fn config_dir() -> &'static Path {
83        &Self::paths().config_dir
84    }
85
86    /// Disposable cache directory.
87    pub fn cache_dir() -> &'static Path {
88        &Self::paths().cache_dir
89    }
90
91    /// Application version derived from `CARGO_PKG_VERSION`.
92    pub fn app_version() -> &'static str {
93        env!("CARGO_PKG_VERSION")
94    }
95
96    /// Per-install launcher client id, surfaced to the JVM as `${clientid}`.
97    ///
98    /// Persisted at `<config_dir>/client_id` so crash reports and Mojang
99    /// telemetry stay correlated across sessions. On first call we read it
100    /// from disk; on a missing/unreadable file we mint a fresh UUID v4
101    /// (RFC 4122) and write it back. Subsequent calls in the same process
102    /// hit the in-memory cache.
103    pub fn client_id() -> &'static str {
104        CLIENT_ID.get_or_init(|| {
105            let path = Self::config_dir().join(CLIENT_ID_FILE);
106
107            // Trim avoids trailing \n from manual edits or POSIX conventions.
108            if let Ok(raw) = std::fs::read_to_string(&path) {
109                let trimmed = raw.trim();
110                if !trimmed.is_empty() {
111                    return trimmed.to_string();
112                }
113            }
114
115            let fresh = generate_uuid_v4();
116
117            // Best-effort write: if config dir is unwritable we still return
118            // a valid id so launches go through; next run will regenerate.
119            if let Some(parent) = path.parent() {
120                let _ = std::fs::create_dir_all(parent);
121            }
122            if let Err(e) = std::fs::write(&path, &fresh) {
123                crate::trace_debug!(
124                    error = %e,
125                    path = %path.display(),
126                    "Could not persist client_id; continuing with in-memory value"
127                );
128            }
129
130            fresh
131        })
132    }
133}
134
135/// Generates a RFC 4122 v4 UUID string from `fastrand`.
136fn generate_uuid_v4() -> String {
137    let mut bytes = [0u8; 16];
138    for b in bytes.iter_mut() {
139        *b = fastrand::u8(..);
140    }
141    // Version 4 (random): high nibble of byte 6 = 0b0100
142    bytes[6] = (bytes[6] & 0x0f) | 0x40;
143    // Variant RFC 4122: top two bits of byte 8 = 0b10
144    bytes[8] = (bytes[8] & 0x3f) | 0x80;
145
146    format!(
147        "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
148        bytes[0], bytes[1], bytes[2], bytes[3],
149        bytes[4], bytes[5],
150        bytes[6], bytes[7],
151        bytes[8], bytes[9],
152        bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
153    )
154}