Skip to main content

kunobi_daemon/
identity.rs

1//! Stable identity and resource paths for independent daemon installations.
2use std::{
3    io,
4    path::{Path, PathBuf},
5};
6
7/// A service installation, independent of its binary version or process ID.
8///
9/// Generate a UUID once per service and store its bytes in the consumer package.
10/// Never generate a new UUID at startup or per release. Also use a reverse-DNS name, for example `ninja.kunobi.kache`.
11/// Profiles separate installations such as `stable` and `dev`; instances allow
12/// multiple services within one profile. Each component is canonical lowercase
13/// ASCII to avoid aliases on case-insensitive filesystems.
14#[derive(Clone, Debug, PartialEq, Eq, Hash)]
15pub struct ServiceIdentity {
16    id: [u8; 16],
17    application: String,
18    profile: String,
19    instance: String,
20}
21
22impl ServiceIdentity {
23    /// Validate an identity without creating directories or changing permissions.
24    pub fn new(
25        id: [u8; 16],
26        application: impl Into<String>,
27        profile: impl Into<String>,
28        instance: impl Into<String>,
29    ) -> io::Result<Self> {
30        if id == [0; 16] {
31            return Err(io::Error::new(
32                io::ErrorKind::InvalidInput,
33                "service UUID must not be nil",
34            ));
35        }
36        let identity = Self {
37            id,
38            application: application.into(),
39            profile: profile.into(),
40            instance: instance.into(),
41        };
42        for component in [&identity.application, &identity.profile, &identity.instance] {
43            if component.is_empty()
44                || component.len() > 64
45                || !component
46                    .bytes()
47                    .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-' || b == b'.')
48                || component.split('.').any(|part| {
49                    part.is_empty()
50                        || !part.as_bytes()[0].is_ascii_alphanumeric()
51                        || !part.as_bytes()[part.len() - 1].is_ascii_alphanumeric()
52                })
53            {
54                return Err(io::Error::new(
55                    io::ErrorKind::InvalidInput,
56                    "invalid service identity component",
57                ));
58            }
59        }
60        Ok(identity)
61    }
62
63    /// Package-owned UUID bytes, fixed across all versions of this service.
64    pub fn id(&self) -> &[u8; 16] {
65        &self.id
66    }
67
68    /// Application-owned name. It must remain stable across binary upgrades.
69    pub fn application(&self) -> &str {
70        &self.application
71    }
72    /// Installation profile, such as stable or dev.
73    pub fn profile(&self) -> &str {
74        &self.profile
75    }
76    /// Named service instance.
77    pub fn instance(&self) -> &str {
78        &self.instance
79    }
80
81    /// Derive disjoint paths below a private per-user runtime directory.
82    ///
83    /// The caller secures the root and creates these directories. Prefixes avoid
84    /// Windows device names; distinct components cannot alias through separators.
85    /// No version is included, so an upgrader finds its predecessor's resources.
86    /// Keep the root short enough for the OS socket-path limit.
87    pub fn paths(&self, user_root: impl AsRef<Path>) -> ServicePaths {
88        ServicePaths {
89            directory: user_root
90                .as_ref()
91                .join(format!("service-{:032x}", u128::from_be_bytes(self.id)))
92                .join(format!("profile-{}", self.profile))
93                .join(format!("instance-{}", self.instance)),
94        }
95    }
96}
97
98/// Conventional resource paths for one service identity under a per-user root.
99///
100/// This is a naming helper, not peer authentication or a directory-security API.
101/// Windows named-pipe users must scope their pipe name by both this identity and
102/// the user's SID, and enforce a same-user ACL; filesystem paths do not do that.
103#[derive(Clone, Debug, PartialEq, Eq)]
104pub struct ServicePaths {
105    directory: PathBuf,
106}
107
108impl ServicePaths {
109    /// Parent directory for the service's filesystem resources.
110    pub fn directory(&self) -> &Path {
111        &self.directory
112    }
113    /// Persistent daemon ownership lock; never unlink it during replacement.
114    pub fn ownership_lock(&self) -> PathBuf {
115        self.directory.join("owner.lock")
116    }
117    /// Persistent upgrade lock, distinct from daemon ownership.
118    pub fn upgrade_lock(&self) -> PathBuf {
119        self.directory.join("upgrade.lock")
120    }
121    /// Application-defined discovery record.
122    pub fn discovery(&self) -> PathBuf {
123        self.directory.join("discovery")
124    }
125    /// Protobuf control endpoint for Unix-domain socket consumers.
126    pub fn control_socket(&self) -> PathBuf {
127        self.directory.join("control.sock")
128    }
129}