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}