Skip to main content

running_process/broker/server/
service_def_loader.rs

1//! Service-definition file loading for the v1 broker.
2//!
3//! The loader intentionally re-reads from disk for each `lookup_or_reload`
4//! call. That gives Phase 4's Hello path reload-on-Hello semantics without
5//! coupling the validation rules to the later async accept loop.
6
7use std::fs;
8use std::path::{Path, PathBuf};
9
10use prost::Message;
11
12use crate::daemon_registration::protocol::{BrokerIsolation, ServiceDefinition};
13use crate::daemon_registration::validation::{validate_service_name, validate_version};
14pub use crate::daemon_registration_common::service_definition::{
15    ensure_service_definition_dir, service_definition_dir, ServiceDefinitionError,
16    SERVICE_DEF_DIR_ENV,
17};
18
19/// Service-definition file extension.
20pub const SERVICE_DEF_EXTENSION: &str = "servicedef";
21
22/// Loader rooted at one service-definition directory.
23#[derive(Clone, Debug)]
24pub struct ServiceDefinitionLoader {
25    root: PathBuf,
26}
27
28impl ServiceDefinitionLoader {
29    /// Create a loader for `root`.
30    pub fn new(root: impl Into<PathBuf>) -> Self {
31        Self { root: root.into() }
32    }
33
34    /// Create a loader for the platform default service-definition directory.
35    pub fn default_root() -> Self {
36        Self::new(service_definition_dir())
37    }
38
39    /// Directory this loader reads from.
40    pub fn root(&self) -> &Path {
41        &self.root
42    }
43
44    /// Load and validate one service definition from disk.
45    pub fn load(&self, service_name: &str) -> Result<ServiceDefinition, ServiceDefinitionError> {
46        ensure_loadable_service_definition_dir(&self.root)?;
47        let path = service_definition_path(&self.root, service_name)?;
48        let bytes = fs::read(&path)?;
49        let definition = ServiceDefinition::decode(bytes.as_slice())?;
50        validate_service_definition_for_service(&definition, service_name)?;
51        Ok(definition)
52    }
53
54    /// Reload one service definition from disk.
55    pub fn reload(&self, service_name: &str) -> Result<ServiceDefinition, ServiceDefinitionError> {
56        self.load(service_name)
57    }
58
59    /// Lookup that always re-reads the service-definition file.
60    pub fn lookup_or_reload(
61        &self,
62        service_name: &str,
63    ) -> Result<ServiceDefinition, ServiceDefinitionError> {
64        self.load(service_name)
65    }
66}
67
68/// Validate and write one `.servicedef` file into `root`.
69///
70/// Consumer installers and development tools should use this helper instead of
71/// duplicating protobuf serialization and service-definition path logic.
72pub fn write_service_definition(
73    root: &Path,
74    definition: &ServiceDefinition,
75) -> Result<PathBuf, ServiceDefinitionError> {
76    ensure_service_definition_dir(root)?;
77    validate_service_definition_for_service(definition, &definition.service_name)?;
78    let path = service_definition_path(root, &definition.service_name)?;
79    fs::write(&path, definition.encode_to_vec())?;
80    Ok(path)
81}
82
83/// Compute the file path for one service definition.
84pub fn service_definition_path(
85    root: &Path,
86    service_name: &str,
87) -> Result<PathBuf, ServiceDefinitionError> {
88    validate_service_name(service_name)?;
89    Ok(root.join(format!("{service_name}.{SERVICE_DEF_EXTENSION}")))
90}
91
92/// Validate one decoded service definition against the requested service.
93pub fn validate_service_definition_for_service(
94    definition: &ServiceDefinition,
95    expected_service: &str,
96) -> Result<(), ServiceDefinitionError> {
97    validate_service_name(expected_service)?;
98    validate_service_name(&definition.service_name)?;
99    if definition.service_name != expected_service {
100        return Err(ServiceDefinitionError::ServiceNameMismatch {
101            requested: expected_service.into(),
102            actual: definition.service_name.clone(),
103        });
104    }
105    validate_absolute_path("binary_path", &definition.binary_path)?;
106    if !definition.per_version_binary_dir.is_empty() {
107        validate_absolute_path("per_version_binary_dir", &definition.per_version_binary_dir)?;
108    }
109    if !definition.min_version.is_empty() {
110        validate_version(&definition.min_version)?;
111    }
112    for version in &definition.version_allow_list {
113        validate_version(version)?;
114    }
115
116    match BrokerIsolation::try_from(definition.isolation) {
117        Ok(BrokerIsolation::PrivateBroker) | Ok(BrokerIsolation::SharedBroker) => {
118            if !definition.explicit_instance.is_empty() {
119                return Err(ServiceDefinitionError::InvalidIsolation {
120                    reason: "explicit_instance must be empty unless isolation is EXPLICIT_INSTANCE",
121                });
122            }
123        }
124        Ok(BrokerIsolation::ExplicitInstance) => {
125            if definition.explicit_instance.is_empty() {
126                return Err(ServiceDefinitionError::InvalidIsolation {
127                    reason: "EXPLICIT_INSTANCE requires explicit_instance",
128                });
129            }
130            validate_service_name(&definition.explicit_instance)?;
131        }
132        Err(_) => {
133            return Err(ServiceDefinitionError::InvalidIsolation {
134                reason: "unknown BrokerIsolation value",
135            });
136        }
137    }
138
139    Ok(())
140}
141
142fn ensure_loadable_service_definition_dir(path: &Path) -> Result<(), ServiceDefinitionError> {
143    if !crate::daemon_registration_common::secure_dir::private_dir_permissions_are_private(path)? {
144        return Err(ServiceDefinitionError::InsecureDirectory(
145            path.to_path_buf(),
146        ));
147    }
148    Ok(())
149}
150
151fn validate_absolute_path(field: &'static str, value: &str) -> Result<(), ServiceDefinitionError> {
152    if value.is_empty() {
153        return Err(ServiceDefinitionError::InvalidPath {
154            field,
155            path: value.into(),
156            reason: "must not be empty",
157        });
158    }
159    if !Path::new(value).is_absolute() {
160        return Err(ServiceDefinitionError::InvalidPath {
161            field,
162            path: value.into(),
163            reason: "must be absolute",
164        });
165    }
166    Ok(())
167}