1use std::error::Error;
9use std::fmt;
10use std::path::{Path, PathBuf};
11
12use crate::daemon_registration_v2 as backend;
13
14pub mod canonical {
19 pub use crate::daemon_registration_v2::{
20 read_service_definition_v2, service_definition_dir_v2, service_definition_path_v2,
21 write_service_definition_v2, LoadedServiceDefinitionV2, ServiceDefinition,
22 ServiceDefinitionBuilder, ServiceDefinitionError,
23 };
24}
25
26#[derive(Debug)]
28pub enum DaemonRegistrationV2Error {
29 Io(std::io::Error),
31 InvalidName {
33 detail: String,
35 },
36 InsecureDirectory {
38 path: PathBuf,
40 },
41 InvalidDefinition {
43 detail: String,
45 },
46}
47
48impl fmt::Display for DaemonRegistrationV2Error {
49 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50 match self {
51 Self::Io(error) => write!(formatter, "daemon registration v2 I/O failed: {error}"),
52 Self::InvalidName { detail } => {
53 write!(formatter, "invalid frozen v2 service name: {detail}")
54 }
55 Self::InsecureDirectory { path } => write!(
56 formatter,
57 "v2 service-definition directory is not owner-private: {}",
58 path.display()
59 ),
60 Self::InvalidDefinition { detail } => {
61 write!(formatter, "invalid frozen v2 service definition: {detail}")
62 }
63 }
64 }
65}
66
67impl Error for DaemonRegistrationV2Error {
68 fn source(&self) -> Option<&(dyn Error + 'static)> {
69 match self {
70 Self::Io(error) => Some(error),
71 _ => None,
72 }
73 }
74}
75
76fn service_error(error: backend::ServiceDefinitionError) -> DaemonRegistrationV2Error {
77 match error {
78 backend::ServiceDefinitionError::Io(error) => DaemonRegistrationV2Error::Io(error),
79 backend::ServiceDefinitionError::InvalidName(error) => {
80 DaemonRegistrationV2Error::InvalidName {
81 detail: error.to_string(),
82 }
83 }
84 backend::ServiceDefinitionError::InsecureDirectory(path) => {
85 DaemonRegistrationV2Error::InsecureDirectory { path }
86 }
87 other => DaemonRegistrationV2Error::InvalidDefinition {
88 detail: other.to_string(),
89 },
90 }
91}
92
93#[must_use]
98pub fn service_definition_directory() -> PathBuf {
99 backend::service_definition_dir_v2()
100}
101
102pub fn service_definition_path(
109 root: impl AsRef<Path>,
110 service_name: impl AsRef<str>,
111) -> Result<PathBuf, DaemonRegistrationV2Error> {
112 backend::service_definition_path_v2(root.as_ref(), service_name.as_ref()).map_err(service_error)
113}
114
115pub fn write_service_definition(
120 root: impl AsRef<Path>,
121 definition: &ServiceDefinition,
122) -> Result<PathBuf, DaemonRegistrationV2Error> {
123 backend::write_service_definition_v2(root.as_ref(), &definition.inner).map_err(service_error)
124}
125
126#[derive(Clone, Debug)]
128pub struct ServiceDefinitionBuilder {
129 service_name: String,
130 binary_path: String,
131 per_version_binary_dir: Option<String>,
132 min_version: Option<String>,
133 allowed_versions: Vec<String>,
134 labels: Vec<(String, String)>,
135}
136
137impl ServiceDefinitionBuilder {
138 #[must_use]
140 pub fn shared_broker(service_name: impl Into<String>, binary_path: impl Into<String>) -> Self {
141 Self {
142 service_name: service_name.into(),
143 binary_path: binary_path.into(),
144 per_version_binary_dir: None,
145 min_version: None,
146 allowed_versions: Vec::new(),
147 labels: Vec::new(),
148 }
149 }
150
151 #[must_use]
153 pub fn per_version_binary_dir(mut self, directory: impl Into<String>) -> Self {
154 self.per_version_binary_dir = Some(directory.into());
155 self
156 }
157
158 #[must_use]
160 pub fn min_version(mut self, version: impl Into<String>) -> Self {
161 self.min_version = Some(version.into());
162 self
163 }
164
165 #[must_use]
167 pub fn allow_version(mut self, version: impl Into<String>) -> Self {
168 self.allowed_versions.push(version.into());
169 self
170 }
171
172 #[must_use]
174 pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
175 self.labels.push((key.into(), value.into()));
176 self
177 }
178
179 #[must_use]
181 pub fn build(self) -> ServiceDefinition {
182 let mut inner =
183 backend::ServiceDefinitionBuilder::shared_broker(self.service_name, self.binary_path);
184 if let Some(directory) = self.per_version_binary_dir {
185 inner = inner.per_version_binary_dir(directory);
186 }
187 if let Some(version) = self.min_version {
188 inner = inner.min_version(version);
189 }
190 inner = inner.version_allow_list(self.allowed_versions);
191 for (key, value) in self.labels {
192 inner = inner.label(key, value);
193 }
194 ServiceDefinition {
195 inner: inner.build(),
196 }
197 }
198
199 pub fn install(self) -> Result<PathBuf, DaemonRegistrationV2Error> {
201 let root = service_definition_directory();
202 self.install_in(root)
203 }
204
205 pub fn install_in(self, root: impl AsRef<Path>) -> Result<PathBuf, DaemonRegistrationV2Error> {
207 write_service_definition(root, &self.build())
208 }
209}
210
211#[derive(Clone, Debug, PartialEq)]
213pub struct ServiceDefinition {
214 inner: backend::ServiceDefinition,
215}
216
217impl ServiceDefinition {
218 #[must_use]
220 pub fn service_name(&self) -> &str {
221 &self.inner.service_name
222 }
223
224 #[must_use]
226 pub fn binary_path(&self) -> &str {
227 &self.inner.binary_path
228 }
229
230 #[must_use]
232 pub fn is_shared_broker(&self) -> bool {
233 self.inner.isolation == backend::BrokerIsolation::SharedBroker as i32
234 }
235
236 #[must_use]
238 pub fn per_version_binary_dir(&self) -> &str {
239 &self.inner.per_version_binary_dir
240 }
241
242 #[must_use]
244 pub fn min_version(&self) -> &str {
245 &self.inner.min_version
246 }
247
248 pub fn allowed_versions(&self) -> impl ExactSizeIterator<Item = &str> {
250 self.inner.version_allow_list.iter().map(String::as_str)
251 }
252
253 #[must_use]
255 pub fn label(&self, key: &str) -> Option<&str> {
256 self.inner.labels.get(key).map(String::as_str)
257 }
258
259 pub fn labels(&self) -> impl ExactSizeIterator<Item = (&str, &str)> {
261 self.inner
262 .labels
263 .iter()
264 .map(|(key, value)| (key.as_str(), value.as_str()))
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use tempfile::tempdir;
272
273 #[test]
274 fn compatibility_builder_retains_order_and_shared_isolation() {
275 let definition = ServiceDefinitionBuilder::shared_broker("service", "/bin/service")
276 .allow_version("2.0.0")
277 .allow_version("1.0.0")
278 .label("vendor", "first")
279 .label("vendor", "last")
280 .build();
281 assert!(definition.is_shared_broker());
282 assert_eq!(
283 definition.allowed_versions().collect::<Vec<_>>(),
284 ["2.0.0", "1.0.0"]
285 );
286 assert_eq!(definition.label("vendor"), Some("last"));
287 }
288
289 #[test]
290 fn compatibility_io_error_keeps_original_os_error_and_source() {
291 let error = service_error(backend::ServiceDefinitionError::Io(
292 std::io::Error::from_raw_os_error(13),
293 ));
294 assert!(error.source().is_some());
295 let DaemonRegistrationV2Error::Io(source) = error else {
296 panic!("I/O classification must remain intact");
297 };
298 assert_eq!(source.raw_os_error(), Some(13));
299 }
300
301 #[test]
302 fn facade_and_backend_keep_same_object_write_bytes_in_one_process() {
303 let definition = ServiceDefinitionBuilder::shared_broker("service", "/bin/service")
304 .per_version_binary_dir("/bin")
305 .min_version("1.2.3")
306 .allow_version("1.2.3")
307 .allow_version("1.2.4")
308 .label("vendor", "zackees")
309 .label("package", "fixture")
310 .build();
311 let facade_root = tempdir().expect("facade root");
312 let backend_root = tempdir().expect("backend root");
313
314 let facade_path =
315 write_service_definition(facade_root.path(), &definition).expect("facade write");
316 let backend_path =
317 backend::write_service_definition_v2(backend_root.path(), &definition.inner)
318 .expect("backend write");
319
320 assert_eq!(
321 std::fs::read(facade_path).expect("facade bytes"),
322 std::fs::read(backend_path).expect("backend bytes"),
323 "the comparison intentionally uses the same object in one process; labels remain a HashMap and are not a canonical byte promise"
324 );
325 }
326}