use std::fmt;
use std::path::PathBuf;
use std::sync::OnceLock;
use crate::origin::{HostId, LocalOrigin};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AppName(&'static str);
impl AppName {
pub const fn new(name: &'static str) -> Self {
assert!(
crate::grammar::is_valid_plain_chunk(name),
"an application name must be a valid plain chunk (RFC 03 §2) — it becomes a directory \
name in the host-id fallback path"
);
AppName(name)
}
pub const fn as_str(&self) -> &'static str {
self.0
}
}
impl fmt::Display for AppName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OriginSalt(&'static str);
impl OriginSalt {
pub const fn new(salt: &'static str) -> Self {
assert!(
!salt.is_empty(),
"an origin salt must not be empty (RFC 06 §1)"
);
OriginSalt(salt)
}
pub const fn as_str(&self) -> &'static str {
self.0
}
}
impl fmt::Display for OriginSalt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0)
}
}
#[derive(Debug)]
pub struct AppProfile {
app: AppName,
salt: OriginSalt,
host_id: OnceLock<HostId>,
}
impl AppProfile {
pub const fn new(app: AppName, salt: OriginSalt) -> Self {
AppProfile {
app,
salt,
host_id: OnceLock::new(),
}
}
pub fn app(&self) -> AppName {
self.app
}
pub fn salt(&self) -> OriginSalt {
self.salt
}
pub fn host_id_fallback_path(&self) -> PathBuf {
dirs::state_dir()
.map(|d| d.join(self.app.as_str()).join("host-id"))
.unwrap_or_else(|| PathBuf::from(format!("/var/lib/{}/host-id", self.app)))
}
pub fn host_id(&self) -> &HostId {
self.host_id.get_or_init(|| {
let id = HostId::mint(
std::path::Path::new("/etc/machine-id"),
&self.host_id_fallback_path(),
self.salt,
);
tracing::info!(origin = %id, app = %self.app, "host origin minted");
id
})
}
pub fn local_origin(&'static self) -> LocalOrigin {
LocalOrigin::from_host_id(self.host_id().clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_profile_constants_are_checked_at_compile_time() {
const APP: AppName = AppName::new("acme-fleet");
const SALT: OriginSalt = OriginSalt::new("acme-fleet-host-id-v1");
static PROFILE: AppProfile = AppProfile::new(APP, SALT);
assert_eq!(APP.as_str(), "acme-fleet");
assert_eq!(SALT.as_str(), "acme-fleet-host-id-v1");
assert_eq!(PROFILE.app(), APP);
assert_eq!(PROFILE.salt(), SALT);
}
#[test]
fn fallback_path_is_app_derived() {
let p = AppProfile::new(AppName::new("acme-fleet"), OriginSalt::new("s"));
let path = p.host_id_fallback_path();
assert!(
path.ends_with("acme-fleet/host-id"),
"unexpected fallback path: {path:?}"
);
}
#[test]
fn host_id_is_minted_once() {
let p = AppProfile::new(
AppName::new("zenkey-profile-test"),
OriginSalt::new("test-salt-v1"),
);
let a = p.host_id().clone();
let b = p.host_id().clone();
assert_eq!(a, b);
}
}