//! Conversion from cloud-delivered config TOML fragments into config stack layers. //! //! Backend fragments arrive in backend priority order. This module parses each //! fragment, resolves relative path fields against the cloud config base //! directory, and returns layers in `ConfigLayerStack` order. use crate::ConfigLayerEntry; use crate::ConfigLayerSource; use crate::TomlValue; use crate::config_toml::ConfigToml; use crate::loader::resolve_relative_paths_in_config_toml; use crate::strict_config::config_error_from_ignored_toml_value_fields_for_source_name; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBufGuard; use serde::Deserialize; use serde::Serialize; use std::fmt; use std::io; use thiserror::Error; /// Config fragment delivered by the cloud config bundle. /// /// The bundle orders fragments from highest precedence to lowest precedence. /// This module returns config layers in stack order, so callers can append the /// result between system and user config without re-sorting. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct CloudConfigFragment { pub id: String, pub name: String, pub contents: String, } impl CloudConfigFragment { fn source_ref(&self) -> CloudConfigFragmentSource { CloudConfigFragmentSource { id: self.id.clone(), name: self.name.clone(), } } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct CloudConfigFragmentSource { pub id: String, pub name: String, } impl fmt::Display for CloudConfigFragmentSource { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} ({})", self.name, self.id) } } #[derive(Debug, Error, PartialEq, Eq)] pub enum CloudConfigLayerError { #[error("failed to parse cloud config fragment {fragment}: {message}")] Parse { fragment: CloudConfigFragmentSource, message: String, }, #[error("invalid cloud config fragment {fragment}: {message}")] Invalid { fragment: CloudConfigFragmentSource, message: String, }, } pub fn cloud_config_layers_from_fragments( fragments: impl IntoIterator, base_dir: &AbsolutePathBuf, ) -> Result, CloudConfigLayerError> { cloud_config_layers_from_fragments_impl(fragments, base_dir, /*strict_config*/ false) } pub(crate) fn cloud_config_layers_from_fragments_strict( fragments: impl IntoIterator, base_dir: &AbsolutePathBuf, ) -> Result, CloudConfigLayerError> { cloud_config_layers_from_fragments_impl(fragments, base_dir, /*strict_config*/ true) } fn cloud_config_layers_from_fragments_impl( fragments: impl IntoIterator, base_dir: &AbsolutePathBuf, strict_config: bool, ) -> Result, CloudConfigLayerError> { let mut layers = Vec::new(); for fragment in fragments { let source_ref = fragment.source_ref(); let raw_toml = fragment.contents; let value: TomlValue = toml::from_str(&raw_toml).map_err(|err| CloudConfigLayerError::Parse { fragment: source_ref.clone(), message: err.to_string(), })?; if strict_config { validate_fragment_strictly(&source_ref, &raw_toml, &value, base_dir)?; } let resolved = resolve_relative_paths_in_config_toml(value, base_dir.as_path()).map_err(|err| { CloudConfigLayerError::Invalid { fragment: source_ref.clone(), message: err.to_string(), } })?; layers.push(ConfigLayerEntry::new_with_raw_toml( ConfigLayerSource::EnterpriseManaged { id: fragment.id, name: fragment.name, }, resolved, raw_toml, base_dir.clone(), )); } // Bundle fragments arrive highest-priority first, while ConfigLayerStack // folds lowest-priority to highest-priority. layers.reverse(); Ok(layers) } fn validate_fragment_strictly( source_ref: &CloudConfigFragmentSource, raw_toml: &str, value: &TomlValue, base_dir: &AbsolutePathBuf, ) -> Result<(), CloudConfigLayerError> { let _guard = AbsolutePathBufGuard::new(base_dir.as_path()); if let Some(config_error) = config_error_from_ignored_toml_value_fields_for_source_name::< ConfigToml, >(&source_ref.to_string(), raw_toml, value.clone()) { return Err(CloudConfigLayerError::Invalid { fragment: source_ref.clone(), message: config_error.message, }); } Ok(()) } impl From for io::Error { fn from(error: CloudConfigLayerError) -> Self { io::Error::new(io::ErrorKind::InvalidData, error) } } #[cfg(test)] #[path = "cloud_config_layers_tests.rs"] mod tests;