Skip to main content

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}