// file: crates/ksp-config-lib/src/registry.rs // version: 5 /// Bootstrap argument used to replace a known Config filename mapping. pub const ARG_FILE_MAP: &str = "--filemap"; /// Logical file identifier for the standard Logging configuration document. pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging"; /// Logical file identifier for the standard Logging JSON Schema document. pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging"; /// Logical file identifier for the standard HTTP Transport configuration document. pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport"; /// Logical file identifier for the standard HTTP Transport JSON Schema document. pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport"; /// Logical file identifier for the generic composite JSON Schema document. pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite"; /// Default physical filename for the standard Logging configuration document. pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json"; /// Default physical filename for the standard Logging JSON Schema document. pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json"; /// Default physical filename for the standard HTTP Transport configuration document. pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json"; /// Default physical filename for the standard HTTP Transport JSON Schema document. pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.json"; /// Default physical filename for the generic composite JSON Schema document. pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json"; /// Stable logical identifier for a Config-managed file. #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] pub struct ConfigFileId(String); impl ConfigFileId { /// Creates and validates a logical Config file identifier. pub fn new(value: impl std::convert::Into) -> ksp_core_lib::Result { let value = value.into(); let validated = validate_file_id(value.as_str()); return match validated { std::result::Result::Ok(()) => std::result::Result::Ok(Self(value)), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Returns the logical identifier as text. #[must_use] pub fn as_str(&self) -> &str { return self.0.as_str(); } } /// Physical root category used to resolve a Config-managed file. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ConfigFileKind { /// Runtime configuration document resolved below `cfgpath`. Config, /// JSON Schema document resolved below `schemapath`. Schema, } /// Logical descriptor associating a stable file identifier with its physical filename, root category and optional validation schema. #[derive(Clone, Debug, Eq, PartialEq)] pub struct ConfigFileDescriptor { file_id: ConfigFileId, kind: ConfigFileKind, filename: std::path::PathBuf, schema_file_id: std::option::Option, } impl ConfigFileDescriptor { /// Returns the stable logical identifier. #[must_use] pub fn file_id(&self) -> &ConfigFileId { return &self.file_id; } /// Returns the root category used when resolving the file. #[must_use] pub fn kind(&self) -> ConfigFileKind { return self.kind; } /// Returns the relative physical filename currently mapped to the identifier. #[must_use] pub fn filename(&self) -> &std::path::Path { return self.filename.as_path(); } /// Returns the logical schema identifier associated with this Config document when one is declared. #[must_use] pub fn schema_file_id(&self) -> std::option::Option<&ConfigFileId> { return self.schema_file_id.as_ref(); } pub(crate) fn new( file_id: &'static str, kind: ConfigFileKind, filename: &'static str, schema_file_id: std::option::Option<&'static str>, ) -> ksp_core_lib::Result { let file_id = ConfigFileId::new(file_id); let file_id = match file_id { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let kind_validation = validate_kind_prefix(&file_id, kind); match kind_validation { std::result::Result::Ok(()) => {}, std::result::Result::Err(error) => return std::result::Result::Err(error), } let filename = validate_relative_filename(&file_id, std::path::PathBuf::from(filename)); let filename = match filename { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let schema_file_id = parse_schema_file_id(&file_id, kind, schema_file_id); return match schema_file_id { std::result::Result::Ok(value) => std::result::Result::Ok(Self { file_id, kind, filename, schema_file_id: value }), std::result::Result::Err(error) => std::result::Result::Err(error), }; } } /// Registry of KSP-known logical Config files and their replaceable physical filenames. #[derive(Clone, Debug, Eq, PartialEq)] pub struct ConfigFileRegistry { descriptors: std::collections::BTreeMap, } impl ConfigFileRegistry { /// Creates the registry containing the KSP default file mappings known in the current release. pub fn defaults() -> ksp_core_lib::Result { let logging = ConfigFileDescriptor::new( FILE_ID_STD_LOGGING, ConfigFileKind::Config, DEFAULT_STD_LOGGING_FILENAME, std::option::Option::Some(FILE_ID_SCHEMA_STD_LOGGING), ); let logging = match logging { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let logging_schema = ConfigFileDescriptor::new(FILE_ID_SCHEMA_STD_LOGGING, ConfigFileKind::Schema, DEFAULT_STD_LOGGING_SCHEMA_FILENAME, std::option::Option::None); let logging_schema = match logging_schema { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let transport = ConfigFileDescriptor::new( FILE_ID_STD_TRANSPORT, ConfigFileKind::Config, DEFAULT_STD_TRANSPORT_FILENAME, std::option::Option::Some(FILE_ID_SCHEMA_STD_TRANSPORT), ); let transport = match transport { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let transport_schema = ConfigFileDescriptor::new(FILE_ID_SCHEMA_STD_TRANSPORT, ConfigFileKind::Schema, DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME, std::option::Option::None); let transport_schema = match transport_schema { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let composite_schema = ConfigFileDescriptor::new(FILE_ID_SCHEMA_COMPOSITE, ConfigFileKind::Schema, DEFAULT_COMPOSITE_SCHEMA_FILENAME, std::option::Option::None); let composite_schema = match composite_schema { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return build_registry([logging, logging_schema, transport, transport_schema, composite_schema]); } /// Creates the default registry and applies repeatable `--filemap==` overrides from raw process arguments. /// /// Unrelated arguments are ignored. A repeated mapping for the same known `file_id` is accepted and the last mapping wins. pub fn from_args(args: &[std::ffi::OsString]) -> ksp_core_lib::Result { let registry = Self::defaults(); let mut registry = match registry { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mut index: usize = 0; while index < args.len() { let application = apply_file_map_argument(&mut registry, &args[index]); match application { std::result::Result::Ok(()) => {}, std::result::Result::Err(error) => return std::result::Result::Err(error), } index += 1; } return std::result::Result::Ok(registry); } /// Iterates over all registered descriptors in deterministic logical `file_id` order. /// /// The returned view is read-only and reflects filename overrides already applied to this registry. pub fn descriptors(&self) -> impl std::iter::Iterator + '_ { return self.descriptors.values(); } /// Returns the descriptor associated with a known logical file identifier. pub fn descriptor(&self, file_id: &ConfigFileId) -> ksp_core_lib::Result<&ConfigFileDescriptor> { return match self.descriptors.get(file_id) { std::option::Option::Some(descriptor) => std::result::Result::Ok(descriptor), std::option::Option::None => std::result::Result::Err(unknown_file_id_error(file_id.as_str())), }; } /// Resolves a known logical file identifier below the bootstrap root selected by its descriptor kind. pub fn resolve_path(&self, bootstrap: &crate::ConfigBootstrapOptions, file_id: &ConfigFileId) -> ksp_core_lib::Result { let descriptor = self.descriptor(file_id); let descriptor = match descriptor { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let root = match descriptor.kind() { ConfigFileKind::Config => bootstrap.cfg_path(), ConfigFileKind::Schema => bootstrap.schema_path(), }; return std::result::Result::Ok(root.join(descriptor.filename())); } /// Replaces the physical filename of one known logical identifier while preserving its kind, schema association and logical identity. pub fn with_filename_override(mut self, file_id: &ConfigFileId, filename: impl std::convert::Into) -> ksp_core_lib::Result { let update = self.set_filename_override(file_id, filename.into()); return match update { std::result::Result::Ok(()) => std::result::Result::Ok(self), std::result::Result::Err(error) => std::result::Result::Err(error), }; } fn set_filename_override(&mut self, file_id: &ConfigFileId, filename: std::path::PathBuf) -> ksp_core_lib::Result<()> { let descriptor = self.descriptors.get(file_id); let descriptor = match descriptor { std::option::Option::Some(value) => value.clone(), std::option::Option::None => return std::result::Result::Err(unknown_file_id_error(file_id.as_str())), }; let filename = validate_relative_filename(file_id, filename); let filename = match filename { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let updated = ConfigFileDescriptor { file_id: descriptor.file_id.clone(), kind: descriptor.kind, filename, schema_file_id: descriptor.schema_file_id.clone(), }; self.descriptors.insert(file_id.clone(), updated); return std::result::Result::Ok(()); } } pub(crate) fn build_registry(descriptors: [ConfigFileDescriptor; N]) -> ksp_core_lib::Result { let mut registry = ConfigFileRegistry { descriptors: std::collections::BTreeMap::new() }; for descriptor in descriptors { let file_id = descriptor.file_id.clone(); let duplicate_id = file_id.clone(); let previous = registry.descriptors.insert(file_id, descriptor); if previous.is_some() { return std::result::Result::Err(duplicate_file_id_error(duplicate_id.as_str())); } } let associations = validate_schema_associations(®istry); return match associations { std::result::Result::Ok(()) => std::result::Result::Ok(registry), std::result::Result::Err(error) => std::result::Result::Err(error), }; } fn parse_schema_file_id( file_id: &ConfigFileId, kind: ConfigFileKind, schema_file_id: std::option::Option<&'static str>, ) -> ksp_core_lib::Result> { return match (kind, schema_file_id) { (ConfigFileKind::Schema, std::option::Option::Some(_)) => { std::result::Result::Err(invalid_file_mapping_with_id_error(file_id.as_str(), "schema descriptors cannot declare another validation schema")) }, (ConfigFileKind::Schema, std::option::Option::None) | (ConfigFileKind::Config, std::option::Option::None) => { std::result::Result::Ok(std::option::Option::None) }, (ConfigFileKind::Config, std::option::Option::Some(value)) => { let schema_id = ConfigFileId::new(value); match schema_id { std::result::Result::Ok(schema_id) => { if schema_id.as_str().starts_with("schema.") { std::result::Result::Ok(std::option::Option::Some(schema_id)) } else { std::result::Result::Err(invalid_file_mapping_with_id_error( file_id.as_str(), "validation schema file_id must use the schema namespace", )) } }, std::result::Result::Err(error) => std::result::Result::Err(error), } }, }; } fn validate_schema_associations(registry: &ConfigFileRegistry) -> ksp_core_lib::Result<()> { for descriptor in registry.descriptors.values() { let schema_file_id = match descriptor.schema_file_id() { std::option::Option::Some(value) => value, std::option::Option::None => continue, }; let schema = registry.descriptors.get(schema_file_id); let schema = match schema { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(invalid_file_mapping_with_id_error( descriptor.file_id().as_str(), "validation schema file_id is not registered", )); }, }; if schema.kind() != ConfigFileKind::Schema { return std::result::Result::Err(invalid_file_mapping_with_id_error( descriptor.file_id().as_str(), "validation schema descriptor must have schema kind", )); } } return std::result::Result::Ok(()); } fn apply_file_map_argument(registry: &mut ConfigFileRegistry, argument: &std::ffi::OsStr) -> ksp_core_lib::Result<()> { let text = match argument.to_str() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Ok(()), }; if text == ARG_FILE_MAP { return std::result::Result::Err(invalid_file_mapping_error("--filemap requires the inline form --filemap==")); } let prefix = "--filemap="; let mapping = match text.strip_prefix(prefix) { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Ok(()), }; let separator = mapping.find('='); let separator = match separator { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(invalid_file_mapping_error("file mapping must contain a file_id and filename separated by '='")); }, }; let file_id_text = &mapping[..separator]; let filename_text = &mapping[separator + 1..]; if file_id_text.is_empty() || filename_text.is_empty() { return std::result::Result::Err(invalid_file_mapping_error("file mapping requires non-empty file_id and filename values")); } let file_id = ConfigFileId::new(file_id_text); let file_id = match file_id { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return registry.set_filename_override(&file_id, std::path::PathBuf::from(filename_text)); } fn validate_file_id(value: &str) -> ksp_core_lib::Result<()> { if value.is_empty() || value.starts_with('.') || value.ends_with('.') || value.contains("..") { return std::result::Result::Err(invalid_file_id_error(value)); } let mut valid = true; for byte in value.bytes() { let allowed = byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'.' || byte == b'_' || byte == b'-'; if !allowed { valid = false; } } if !valid { return std::result::Result::Err(invalid_file_id_error(value)); } return std::result::Result::Ok(()); } fn validate_kind_prefix(file_id: &ConfigFileId, kind: ConfigFileKind) -> ksp_core_lib::Result<()> { let valid = match kind { ConfigFileKind::Config => file_id.as_str().starts_with("cfg."), ConfigFileKind::Schema => file_id.as_str().starts_with("schema."), }; if !valid { return std::result::Result::Err(invalid_file_mapping_with_id_error(file_id.as_str(), "file_id prefix does not match descriptor kind")); } return std::result::Result::Ok(()); } fn validate_relative_filename(file_id: &ConfigFileId, filename: std::path::PathBuf) -> ksp_core_lib::Result { if filename.as_os_str().is_empty() || filename.is_absolute() { return std::result::Result::Err(invalid_filename_error(file_id.as_str(), &filename)); } let mut has_normal_component = false; for component in filename.components() { match component { std::path::Component::Normal(_) => has_normal_component = true, std::path::Component::CurDir | std::path::Component::ParentDir | std::path::Component::RootDir | std::path::Component::Prefix(_) => { return std::result::Result::Err(invalid_filename_error(file_id.as_str(), &filename)); }, } } if !has_normal_component { return std::result::Result::Err(invalid_filename_error(file_id.as_str(), &filename)); } return std::result::Result::Ok(filename); } fn invalid_file_id_error(file_id: &str) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_FILE_ID_INVALID, "Config file_id is invalid") .with_context("file_id", file_id) .with_context("reason", "expected lowercase ASCII segments separated by single dots"); } fn unknown_file_id_error(file_id: &str) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_FILE_ID_UNKNOWN, "Config file_id is not registered").with_context("file_id", file_id); } fn duplicate_file_id_error(file_id: &str) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_FILE_ID_DUPLICATE, "Config file_id is registered more than once").with_context("file_id", file_id); } fn invalid_file_mapping_error(reason: &'static str) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_FILE_MAPPING_INVALID, "Config file mapping is invalid").with_context("reason", reason); } fn invalid_file_mapping_with_id_error(file_id: &str, reason: &'static str) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_FILE_MAPPING_INVALID, "Config file mapping is invalid") .with_context("file_id", file_id) .with_context("reason", reason); } fn invalid_filename_error(file_id: &str, filename: &std::path::Path) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_FILE_MAPPING_INVALID, "Config mapped filename is invalid") .with_context("file_id", file_id) .with_context("filename", filename.to_string_lossy().into_owned()) .with_context("reason", "filename must stay relative to its Config-owned root without traversal components"); } #[cfg(test)] #[path = "../unit_tests/registry.rs"] mod tests;