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