use std::error::Error;
use std::fmt;
use std::path::{Path, PathBuf};
use crate::daemon_registration_v2 as backend;
pub mod canonical {
pub use crate::daemon_registration_v2::{
read_service_definition_v2, service_definition_dir_v2, service_definition_path_v2,
write_service_definition_v2, LoadedServiceDefinitionV2, ServiceDefinition,
ServiceDefinitionBuilder, ServiceDefinitionError,
};
}
#[derive(Debug)]
pub enum DaemonRegistrationV2Error {
Io(std::io::Error),
InvalidName {
detail: String,
},
InsecureDirectory {
path: PathBuf,
},
InvalidDefinition {
detail: String,
},
}
impl fmt::Display for DaemonRegistrationV2Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(error) => write!(formatter, "daemon registration v2 I/O failed: {error}"),
Self::InvalidName { detail } => {
write!(formatter, "invalid frozen v2 service name: {detail}")
}
Self::InsecureDirectory { path } => write!(
formatter,
"v2 service-definition directory is not owner-private: {}",
path.display()
),
Self::InvalidDefinition { detail } => {
write!(formatter, "invalid frozen v2 service definition: {detail}")
}
}
}
}
impl Error for DaemonRegistrationV2Error {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Io(error) => Some(error),
_ => None,
}
}
}
fn service_error(error: backend::ServiceDefinitionError) -> DaemonRegistrationV2Error {
match error {
backend::ServiceDefinitionError::Io(error) => DaemonRegistrationV2Error::Io(error),
backend::ServiceDefinitionError::InvalidName(error) => {
DaemonRegistrationV2Error::InvalidName {
detail: error.to_string(),
}
}
backend::ServiceDefinitionError::InsecureDirectory(path) => {
DaemonRegistrationV2Error::InsecureDirectory { path }
}
other => DaemonRegistrationV2Error::InvalidDefinition {
detail: other.to_string(),
},
}
}
#[must_use]
pub fn service_definition_directory() -> PathBuf {
backend::service_definition_dir_v2()
}
pub fn service_definition_path(
root: impl AsRef<Path>,
service_name: impl AsRef<str>,
) -> Result<PathBuf, DaemonRegistrationV2Error> {
backend::service_definition_path_v2(root.as_ref(), service_name.as_ref()).map_err(service_error)
}
pub fn write_service_definition(
root: impl AsRef<Path>,
definition: &ServiceDefinition,
) -> Result<PathBuf, DaemonRegistrationV2Error> {
backend::write_service_definition_v2(root.as_ref(), &definition.inner).map_err(service_error)
}
#[derive(Clone, Debug)]
pub struct ServiceDefinitionBuilder {
service_name: String,
binary_path: String,
per_version_binary_dir: Option<String>,
min_version: Option<String>,
allowed_versions: Vec<String>,
labels: Vec<(String, String)>,
}
impl ServiceDefinitionBuilder {
#[must_use]
pub fn shared_broker(service_name: impl Into<String>, binary_path: impl Into<String>) -> Self {
Self {
service_name: service_name.into(),
binary_path: binary_path.into(),
per_version_binary_dir: None,
min_version: None,
allowed_versions: Vec::new(),
labels: Vec::new(),
}
}
#[must_use]
pub fn per_version_binary_dir(mut self, directory: impl Into<String>) -> Self {
self.per_version_binary_dir = Some(directory.into());
self
}
#[must_use]
pub fn min_version(mut self, version: impl Into<String>) -> Self {
self.min_version = Some(version.into());
self
}
#[must_use]
pub fn allow_version(mut self, version: impl Into<String>) -> Self {
self.allowed_versions.push(version.into());
self
}
#[must_use]
pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.labels.push((key.into(), value.into()));
self
}
#[must_use]
pub fn build(self) -> ServiceDefinition {
let mut inner =
backend::ServiceDefinitionBuilder::shared_broker(self.service_name, self.binary_path);
if let Some(directory) = self.per_version_binary_dir {
inner = inner.per_version_binary_dir(directory);
}
if let Some(version) = self.min_version {
inner = inner.min_version(version);
}
inner = inner.version_allow_list(self.allowed_versions);
for (key, value) in self.labels {
inner = inner.label(key, value);
}
ServiceDefinition {
inner: inner.build(),
}
}
pub fn install(self) -> Result<PathBuf, DaemonRegistrationV2Error> {
let root = service_definition_directory();
self.install_in(root)
}
pub fn install_in(self, root: impl AsRef<Path>) -> Result<PathBuf, DaemonRegistrationV2Error> {
write_service_definition(root, &self.build())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ServiceDefinition {
inner: backend::ServiceDefinition,
}
impl ServiceDefinition {
#[must_use]
pub fn service_name(&self) -> &str {
&self.inner.service_name
}
#[must_use]
pub fn binary_path(&self) -> &str {
&self.inner.binary_path
}
#[must_use]
pub fn is_shared_broker(&self) -> bool {
self.inner.isolation == backend::BrokerIsolation::SharedBroker as i32
}
#[must_use]
pub fn per_version_binary_dir(&self) -> &str {
&self.inner.per_version_binary_dir
}
#[must_use]
pub fn min_version(&self) -> &str {
&self.inner.min_version
}
pub fn allowed_versions(&self) -> impl ExactSizeIterator<Item = &str> {
self.inner.version_allow_list.iter().map(String::as_str)
}
#[must_use]
pub fn label(&self, key: &str) -> Option<&str> {
self.inner.labels.get(key).map(String::as_str)
}
pub fn labels(&self) -> impl ExactSizeIterator<Item = (&str, &str)> {
self.inner
.labels
.iter()
.map(|(key, value)| (key.as_str(), value.as_str()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn compatibility_builder_retains_order_and_shared_isolation() {
let definition = ServiceDefinitionBuilder::shared_broker("service", "/bin/service")
.allow_version("2.0.0")
.allow_version("1.0.0")
.label("vendor", "first")
.label("vendor", "last")
.build();
assert!(definition.is_shared_broker());
assert_eq!(
definition.allowed_versions().collect::<Vec<_>>(),
["2.0.0", "1.0.0"]
);
assert_eq!(definition.label("vendor"), Some("last"));
}
#[test]
fn compatibility_io_error_keeps_original_os_error_and_source() {
let error = service_error(backend::ServiceDefinitionError::Io(
std::io::Error::from_raw_os_error(13),
));
assert!(error.source().is_some());
let DaemonRegistrationV2Error::Io(source) = error else {
panic!("I/O classification must remain intact");
};
assert_eq!(source.raw_os_error(), Some(13));
}
#[test]
fn facade_and_backend_keep_same_object_write_bytes_in_one_process() {
let definition = ServiceDefinitionBuilder::shared_broker("service", "/bin/service")
.per_version_binary_dir("/bin")
.min_version("1.2.3")
.allow_version("1.2.3")
.allow_version("1.2.4")
.label("vendor", "zackees")
.label("package", "fixture")
.build();
let facade_root = tempdir().expect("facade root");
let backend_root = tempdir().expect("backend root");
let facade_path =
write_service_definition(facade_root.path(), &definition).expect("facade write");
let backend_path =
backend::write_service_definition_v2(backend_root.path(), &definition.inner)
.expect("backend write");
assert_eq!(
std::fs::read(facade_path).expect("facade bytes"),
std::fs::read(backend_path).expect("backend bytes"),
"the comparison intentionally uses the same object in one process; labels remain a HashMap and are not a canonical byte promise"
);
}
}