use sha2::{Digest, Sha256};
use std::ffi::OsString;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::Duration;
const HASH_PREFIX: &[u8] = b"velesdb-install-v2:";
const FILE_NAME: &str = "install-id";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Platform {
Linux,
MacOs,
Windows,
}
impl Platform {
pub(super) const CURRENT: Self = if cfg!(target_os = "windows") {
Self::Windows
} else if cfg!(target_os = "macos") {
Self::MacOs
} else {
Self::Linux
};
}
pub(super) fn user_config_dir(
platform: Platform,
lookup: impl Fn(&str) -> Option<OsString>,
) -> Option<PathBuf> {
let base = match platform {
Platform::Linux => lookup("XDG_CONFIG_HOME")
.map(PathBuf::from)
.filter(|path| path.is_absolute())
.or_else(|| non_empty(lookup("HOME")).map(|home| home.join(".config"))),
Platform::MacOs => {
non_empty(lookup("HOME")).map(|home| home.join("Library").join("Application Support"))
}
Platform::Windows => non_empty(lookup("APPDATA")),
};
base.map(|dir| dir.join("velesdb"))
}
fn non_empty(value: Option<OsString>) -> Option<PathBuf> {
value.filter(|v| !v.is_empty()).map(PathBuf::from)
}
pub(super) fn candidate_dirs(
platform: Platform,
lookup: impl Fn(&str) -> Option<OsString>,
data_dir: Option<&Path>,
) -> Vec<PathBuf> {
user_config_dir(platform, lookup)
.into_iter()
.chain(data_dir.map(Path::to_path_buf))
.collect()
}
fn new_id() -> String {
format!("{:032x}", rand::random::<u128>())
}
fn is_valid_id(candidate: &str) -> bool {
candidate.len() == 32
&& candidate
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}
fn read_id(path: &Path) -> Option<String> {
let content = std::fs::read_to_string(path).ok()?;
let id = content.trim();
is_valid_id(id).then(|| id.to_string())
}
const LOCK_WAIT: Duration = Duration::from_secs(2);
const LOCK_POLL: Duration = Duration::from_millis(5);
const LOCK_NAME: &str = ".install-id.lock";
pub(super) fn load_or_create(dir: &Path) -> Option<String> {
load_or_create_within(dir, LOCK_WAIT)
}
fn load_or_create_within(dir: &Path, wait: Duration) -> Option<String> {
let path = dir.join(FILE_NAME);
if let Some(id) = read_id(&path) {
return Some(id);
}
std::fs::create_dir_all(dir).ok()?;
let _lock = lock_exclusive(&dir.join(LOCK_NAME), wait)?;
read_id(&path).or_else(|| write_new(dir, &path))
}
#[derive(Debug, PartialEq, Eq)]
enum LockStep {
Acquired,
Retry,
GiveUp,
}
fn lock_step(
attempt: &Result<(), std::fs::TryLockError>,
waited: Duration,
wait: Duration,
) -> LockStep {
match attempt {
Ok(()) => LockStep::Acquired,
Err(std::fs::TryLockError::WouldBlock) if waited < wait => LockStep::Retry,
Err(_) => LockStep::GiveUp,
}
}
fn lock_exclusive(path: &Path, wait: Duration) -> Option<std::fs::File> {
let file = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(path)
.ok()?;
let started = std::time::Instant::now();
loop {
match lock_step(&file.try_lock(), started.elapsed(), wait) {
LockStep::Acquired => return Some(file),
LockStep::Retry => std::thread::sleep(LOCK_POLL),
LockStep::GiveUp => return None,
}
}
}
fn write_new(dir: &Path, path: &Path) -> Option<String> {
let id = new_id();
let temporary = dir.join(format!(".{FILE_NAME}.{id}.tmp"));
write_synced(&temporary, &format!("{id}\n")).ok()?;
if std::fs::rename(&temporary, path).is_err() {
let _ = std::fs::remove_file(&temporary);
return None;
}
Some(id)
}
fn write_synced(path: &Path, content: &str) -> std::io::Result<()> {
let mut file = std::fs::File::create(path)?;
file.write_all(content.as_bytes())?;
file.sync_all()
}
pub(super) fn resolve_install_id(dirs: &[PathBuf]) -> String {
dirs.iter()
.find_map(|dir| load_or_create(dir))
.unwrap_or_else(new_id)
}
pub(super) fn peek_install_id(dirs: &[PathBuf]) -> String {
dirs.iter()
.find_map(|dir| read_id(&dir.join(FILE_NAME)))
.unwrap_or_else(new_id)
}
pub(super) fn hash_install_id(id: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(HASH_PREFIX);
hasher.update(id.as_bytes());
hex::encode(hasher.finalize())
}
pub(super) fn install_hash(data_dir: Option<&Path>) -> String {
let dirs = candidate_dirs(Platform::CURRENT, |key| std::env::var_os(key), data_dir);
hash_install_id(&resolve_install_id(&dirs))
}
#[deprecated(
since = "7.0.0",
note = "the update check derives its install hash itself; this reads the stored id, never creates one (a fresh random hash on every call when none is stored), and is no longer machine- or data-dir-bound"
)]
#[must_use]
pub fn compute_instance_hash(data_dir: &Path) -> String {
stored_install_hash(Platform::CURRENT, |key| std::env::var_os(key), data_dir)
}
fn stored_install_hash(
platform: Platform,
lookup: impl Fn(&str) -> Option<OsString>,
data_dir: &Path,
) -> String {
let dirs = candidate_dirs(platform, lookup, Some(data_dir));
hash_install_id(&peek_install_id(&dirs))
}
#[cfg(test)]
#[path = "instance_id_tests.rs"]
mod tests;