lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 63fe5ef47a3931d8b5b5b7f0ce60a95daa918338
parent 99310085626a159e66c27875399c92cc3634f1e0
Author: triesap <tyson@radroots.org>
Date:   Tue, 11 Aug 2026 05:46:16 +0000

service-host: load strict config documents

- bound selected TOML document reads to one fixed maximum
- validate exact schema identity before typed decoding
- retain safe source locations without leaking parser causes
- cover malformed, oversized, versioned, and side-effect boundaries

Diffstat:
Mcrates/service_host/Cargo.toml | 2+-
Acrates/service_host/src/config/document.rs | 704+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/service_host/src/config/mod.rs | 9+++++++++
Mcrates/service_host/src/lib.rs | 6++++++
Mcrates/service_host/tests/package_boundary.rs | 20++++++++++++++++++++
5 files changed, 740 insertions(+), 1 deletion(-)

diff --git a/crates/service_host/Cargo.toml b/crates/service_host/Cargo.toml @@ -25,11 +25,11 @@ serde = { workspace = true, features = ["derive", "std"] } serde_json = { workspace = true, features = ["raw_value", "std"] } tokio = { workspace = true, features = ["io-util", "macros", "net", "rt", "sync", "time"] } tokio-util = { workspace = true } +toml = { workspace = true } [dev-dependencies] tempfile = { workspace = true } tokio = { workspace = true, features = ["io-util", "macros", "rt", "sync", "test-util", "time"] } -toml = { workspace = true } [lints] workspace = true diff --git a/crates/service_host/src/config/document.rs b/crates/service_host/src/config/document.rs @@ -0,0 +1,704 @@ +//! Bounded loading and exact versioned TOML document admission. + +use core::fmt; +use serde::de::DeserializeOwned; +use std::error::Error; +use std::fs::File; +use std::io::{self, Read}; +use std::path::{Path, PathBuf}; + +/// Hard upper bound for one complete service configuration document. +pub const CONFIG_DOCUMENT_MAX_UTF8_BYTES: usize = 1024 * 1024; + +/// Hard upper bound for a versioned configuration schema identifier. +pub const CONFIG_SCHEMA_ID_MAX_UTF8_BYTES: usize = 128; + +/// Validated schema identity and exact version expected from one document. +#[derive(Clone, PartialEq, Eq)] +pub struct ConfigDocumentExpectation { + schema: Box<str>, + schema_version: u32, +} + +impl ConfigDocumentExpectation { + /// Creates an exact expected document identity. + pub fn new( + schema: impl Into<Box<str>>, + schema_version: u32, + ) -> Result<Self, ConfigDocumentExpectationError> { + let schema = schema.into(); + if !valid_schema_id(&schema) { + return Err(ConfigDocumentExpectationError::InvalidSchema); + } + if schema_version == 0 { + return Err(ConfigDocumentExpectationError::InvalidSchemaVersion); + } + Ok(Self { + schema, + schema_version, + }) + } + + /// Returns the exact schema identifier. + #[must_use] + pub fn schema(&self) -> &str { + &self.schema + } + + /// Returns the exact schema version. + #[must_use] + pub const fn schema_version(&self) -> u32 { + self.schema_version + } +} + +impl fmt::Debug for ConfigDocumentExpectation { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ConfigDocumentExpectation") + .field("schema", &self.schema) + .field("schema_version", &self.schema_version) + .finish() + } +} + +/// Invalid caller-provided schema expectation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ConfigDocumentExpectationError { + InvalidSchema, + InvalidSchemaVersion, +} + +impl fmt::Display for ConfigDocumentExpectationError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("configuration document expectation is invalid") + } +} + +impl Error for ConfigDocumentExpectationError {} + +/// Stable classification for a configuration document failure. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ConfigDocumentErrorKind { + NotFound, + Read, + TooLarge, + InvalidUtf8, + MalformedToml, + MissingSchema, + InvalidSchema, + SchemaMismatch, + MissingSchemaVersion, + InvalidSchemaVersion, + UnsupportedSchemaVersion, + TypedDocument, +} + +impl ConfigDocumentErrorKind { + const fn message(self) -> &'static str { + match self { + Self::NotFound => "configuration document was not found", + Self::Read => "configuration document could not be read", + Self::TooLarge => "configuration document exceeds its size limit", + Self::InvalidUtf8 => "configuration document is not valid UTF-8", + Self::MalformedToml => "configuration document is not valid TOML", + Self::MissingSchema => "configuration document schema is missing", + Self::InvalidSchema => "configuration document schema is invalid", + Self::SchemaMismatch => "configuration document schema is unsupported", + Self::MissingSchemaVersion => "configuration document schema version is missing", + Self::InvalidSchemaVersion => "configuration document schema version is invalid", + Self::UnsupportedSchemaVersion => { + "configuration document schema version is unsupported" + } + Self::TypedDocument => "configuration document fields are invalid", + } + } +} + +/// One-based location inside the selected source document. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ConfigDocumentLocation { + line: u32, + column: u32, +} + +impl ConfigDocumentLocation { + /// Returns the one-based line. + #[must_use] + pub const fn line(self) -> u32 { + self.line + } + + /// Returns the one-based UTF-8 character column. + #[must_use] + pub const fn column(self) -> u32 { + self.column + } +} + +/// Source-located failure with a redacted ordinary representation. +pub struct ConfigDocumentError { + kind: ConfigDocumentErrorKind, + source_path: PathBuf, + location: Option<ConfigDocumentLocation>, + io_kind: Option<io::ErrorKind>, +} + +impl ConfigDocumentError { + /// Returns the stable failure classification. + #[must_use] + pub const fn kind(&self) -> ConfigDocumentErrorKind { + self.kind + } + + /// Returns the selected source path for trusted local diagnostics. + #[must_use] + pub fn trusted_source_path(&self) -> &Path { + &self.source_path + } + + /// Returns a parser-provided source location when one is available. + #[must_use] + pub const fn location(&self) -> Option<ConfigDocumentLocation> { + self.location + } + + /// Returns only the safe I/O classification when the failure came from I/O. + #[must_use] + pub const fn io_kind(&self) -> Option<io::ErrorKind> { + self.io_kind + } + + fn without_source(kind: ConfigDocumentErrorKind, source_path: &Path) -> Self { + Self { + kind, + source_path: source_path.to_path_buf(), + location: None, + io_kind: None, + } + } + + fn with_io_kind( + kind: ConfigDocumentErrorKind, + source_path: &Path, + io_kind: io::ErrorKind, + ) -> Self { + Self { + kind, + source_path: source_path.to_path_buf(), + location: None, + io_kind: Some(io_kind), + } + } + + fn with_toml_source( + kind: ConfigDocumentErrorKind, + source_path: &Path, + document: &str, + source: &toml::de::Error, + ) -> Self { + let location = source + .span() + .and_then(|span| location_at(document, span.start)); + Self { + kind, + source_path: source_path.to_path_buf(), + location, + io_kind: None, + } + } +} + +impl fmt::Debug for ConfigDocumentError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ConfigDocumentError") + .field("kind", &self.kind) + .field("source_path", &"[redacted]") + .field("location", &self.location) + .field("io_kind", &self.io_kind) + .finish() + } +} + +impl fmt::Display for ConfigDocumentError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.kind.message()) + } +} + +impl Error for ConfigDocumentError {} + +/// Reads and strictly deserializes one selected, exact-version TOML document. +pub fn load_config_document<T>( + source_path: &Path, + expectation: &ConfigDocumentExpectation, +) -> Result<T, ConfigDocumentError> +where + T: DeserializeOwned, +{ + load_with_source(&FileDocumentSource, source_path, expectation) +} + +trait DocumentSource { + type Reader: Read; + + fn open_selected(&self, source_path: &Path) -> io::Result<Self::Reader>; +} + +struct FileDocumentSource; + +impl DocumentSource for FileDocumentSource { + type Reader = File; + + fn open_selected(&self, source_path: &Path) -> io::Result<Self::Reader> { + File::open(source_path) + } +} + +fn load_with_source<T, S>( + source: &S, + source_path: &Path, + expectation: &ConfigDocumentExpectation, +) -> Result<T, ConfigDocumentError> +where + T: DeserializeOwned, + S: DocumentSource, +{ + let reader = source.open_selected(source_path).map_err(|error| { + let kind = if error.kind() == io::ErrorKind::NotFound { + ConfigDocumentErrorKind::NotFound + } else { + ConfigDocumentErrorKind::Read + }; + ConfigDocumentError::with_io_kind(kind, source_path, error.kind()) + })?; + let bytes = read_bounded(reader, source_path)?; + let document = String::from_utf8(bytes).map_err(|_| { + ConfigDocumentError::without_source(ConfigDocumentErrorKind::InvalidUtf8, source_path) + })?; + let header = document.parse::<toml::Table>().map_err(|error| { + ConfigDocumentError::with_toml_source( + ConfigDocumentErrorKind::MalformedToml, + source_path, + &document, + &error, + ) + })?; + validate_header(&header, source_path, expectation)?; + toml::from_str(&document).map_err(|error| { + ConfigDocumentError::with_toml_source( + ConfigDocumentErrorKind::TypedDocument, + source_path, + &document, + &error, + ) + }) +} + +fn read_bounded(reader: impl Read, source_path: &Path) -> Result<Vec<u8>, ConfigDocumentError> { + let allocation = CONFIG_DOCUMENT_MAX_UTF8_BYTES + 1; + let mut bytes = Vec::with_capacity(allocation); + reader + .take(allocation as u64) + .read_to_end(&mut bytes) + .map_err(|error| { + ConfigDocumentError::with_io_kind( + ConfigDocumentErrorKind::Read, + source_path, + error.kind(), + ) + })?; + if bytes.len() > CONFIG_DOCUMENT_MAX_UTF8_BYTES { + return Err(ConfigDocumentError::without_source( + ConfigDocumentErrorKind::TooLarge, + source_path, + )); + } + Ok(bytes) +} + +fn validate_header( + header: &toml::Table, + source_path: &Path, + expectation: &ConfigDocumentExpectation, +) -> Result<(), ConfigDocumentError> { + let schema = header + .get("schema") + .ok_or_else(|| { + ConfigDocumentError::without_source(ConfigDocumentErrorKind::MissingSchema, source_path) + })? + .as_str() + .ok_or_else(|| { + ConfigDocumentError::without_source(ConfigDocumentErrorKind::InvalidSchema, source_path) + })?; + if !valid_schema_id(schema) { + return Err(ConfigDocumentError::without_source( + ConfigDocumentErrorKind::InvalidSchema, + source_path, + )); + } + if schema != expectation.schema() { + return Err(ConfigDocumentError::without_source( + ConfigDocumentErrorKind::SchemaMismatch, + source_path, + )); + } + + let version = header + .get("schema_version") + .ok_or_else(|| { + ConfigDocumentError::without_source( + ConfigDocumentErrorKind::MissingSchemaVersion, + source_path, + ) + })? + .as_integer() + .ok_or_else(|| { + ConfigDocumentError::without_source( + ConfigDocumentErrorKind::InvalidSchemaVersion, + source_path, + ) + })?; + let version = u32::try_from(version).map_err(|_| { + ConfigDocumentError::without_source( + ConfigDocumentErrorKind::InvalidSchemaVersion, + source_path, + ) + })?; + if version == 0 { + return Err(ConfigDocumentError::without_source( + ConfigDocumentErrorKind::InvalidSchemaVersion, + source_path, + )); + } + if version != expectation.schema_version() { + return Err(ConfigDocumentError::without_source( + ConfigDocumentErrorKind::UnsupportedSchemaVersion, + source_path, + )); + } + Ok(()) +} + +fn valid_schema_id(value: &str) -> bool { + let mut bytes = value.bytes(); + value.len() <= CONFIG_SCHEMA_ID_MAX_UTF8_BYTES + && bytes + .next() + .is_some_and(|byte| byte.is_ascii_alphanumeric()) + && bytes + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'-')) +} + +fn location_at(document: &str, byte_offset: usize) -> Option<ConfigDocumentLocation> { + let prefix = document.get(..byte_offset)?; + let line = u32::try_from(prefix.bytes().filter(|byte| *byte == b'\n').count()) + .ok()? + .checked_add(1)?; + let column = u32::try_from( + prefix + .rsplit_once('\n') + .map_or(prefix, |(_, tail)| tail) + .chars() + .count(), + ) + .ok()? + .checked_add(1)?; + Some(ConfigDocumentLocation { line, column }) +} + +#[cfg(test)] +mod tests { + use std::cell::RefCell; + use std::io::Cursor; + + use serde::Deserialize; + use tempfile::tempdir; + + use super::*; + + const SOURCE_PATH: &str = "/private/config/secret-instance.toml"; + + #[derive(Debug, Deserialize, PartialEq, Eq)] + #[serde(deny_unknown_fields)] + struct StrictDocument { + schema: String, + schema_version: u32, + service: StrictService, + } + + #[derive(Debug, Deserialize, PartialEq, Eq)] + #[serde(deny_unknown_fields)] + struct StrictService { + instance: String, + } + + #[derive(Debug, Deserialize, PartialEq, Eq)] + struct PermissiveDocument { + schema: String, + schema_version: u32, + } + + fn expectation() -> ConfigDocumentExpectation { + ConfigDocumentExpectation::new("radroots.example.config", 1).unwrap() + } + + fn valid_document() -> &'static str { + concat!( + "schema = \"radroots.example.config\"\n", + "schema_version = 1\n", + "[service]\n", + "instance = \"default\"\n", + ) + } + + #[test] + fn exact_document_is_bounded_and_strictly_deserialized() { + let directory = tempdir().unwrap(); + let path = directory.path().join("config.toml"); + std::fs::write(&path, valid_document()).unwrap(); + + let loaded = load_config_document::<StrictDocument>(&path, &expectation()).unwrap(); + assert_eq!(loaded.schema, "radroots.example.config"); + assert_eq!(loaded.schema_version, 1); + assert_eq!(loaded.service.instance, "default"); + + let exact_padding = CONFIG_DOCUMENT_MAX_UTF8_BYTES - valid_document().len() - 2; + let exact = format!("{}#{}\n", valid_document(), "x".repeat(exact_padding)); + assert_eq!(exact.len(), CONFIG_DOCUMENT_MAX_UTF8_BYTES); + std::fs::write(&path, exact).unwrap(); + assert!(load_config_document::<StrictDocument>(&path, &expectation()).is_ok()); + + std::fs::write(&path, vec![b'x'; CONFIG_DOCUMENT_MAX_UTF8_BYTES + 1]).unwrap(); + assert_eq!( + load_config_document::<StrictDocument>(&path, &expectation()) + .unwrap_err() + .kind(), + ConfigDocumentErrorKind::TooLarge + ); + } + + #[test] + fn missing_unreadable_and_invalid_utf8_documents_fail_closed() { + let directory = tempdir().unwrap(); + let missing = directory.path().join("missing.toml"); + assert_eq!( + load_config_document::<StrictDocument>(&missing, &expectation()) + .unwrap_err() + .kind(), + ConfigDocumentErrorKind::NotFound + ); + + let invalid_utf8 = directory.path().join("invalid.toml"); + std::fs::write(&invalid_utf8, [0xff, 0xfe]).unwrap(); + assert_eq!( + load_config_document::<StrictDocument>(&invalid_utf8, &expectation()) + .unwrap_err() + .kind(), + ConfigDocumentErrorKind::InvalidUtf8 + ); + + struct FailingSource; + impl DocumentSource for FailingSource { + type Reader = Cursor<Vec<u8>>; + + fn open_selected(&self, _source_path: &Path) -> io::Result<Self::Reader> { + Err(io::Error::new(io::ErrorKind::PermissionDenied, "sensitive")) + } + } + let error = load_with_source::<StrictDocument, _>( + &FailingSource, + Path::new(SOURCE_PATH), + &expectation(), + ) + .unwrap_err(); + assert_eq!(error.kind(), ConfigDocumentErrorKind::Read); + assert_eq!(error.io_kind(), Some(io::ErrorKind::PermissionDenied)); + assert!(error.source().is_none()); + } + + #[test] + fn malformed_toml_and_typed_errors_retain_safe_locations() { + let malformed = RecordingSource::new(b"token = \"secret-value\n".to_vec()); + let error = load_with_source::<StrictDocument, _>( + &malformed, + Path::new(SOURCE_PATH), + &expectation(), + ) + .unwrap_err(); + assert_eq!(error.kind(), ConfigDocumentErrorKind::MalformedToml); + assert!(error.location().is_some()); + assert_public_error_chain_is_safe(&error, &[SOURCE_PATH, "secret-value", "token"]); + + let typed = RecordingSource::new( + concat!( + "schema = \"radroots.example.config\"\n", + "schema_version = 1\n", + "credential_secret = \"secret-value\"\n", + "[service]\n", + "instance = \"default\"\n", + ) + .as_bytes() + .to_vec(), + ); + let error = + load_with_source::<StrictDocument, _>(&typed, Path::new(SOURCE_PATH), &expectation()) + .unwrap_err(); + assert_eq!(error.kind(), ConfigDocumentErrorKind::TypedDocument); + let location = error.location().unwrap(); + assert!(location.line() >= 3); + assert!(location.column() >= 1); + assert_eq!(error.trusted_source_path(), Path::new(SOURCE_PATH)); + assert_public_error_chain_is_safe( + &error, + &[SOURCE_PATH, "credential_secret", "secret-value"], + ); + assert_eq!( + error.to_string(), + "configuration document fields are invalid" + ); + } + + #[test] + fn header_identity_is_required_valid_and_checked_before_typed_fields() { + for (source, kind) in [ + ( + "schema_version = 1\n", + ConfigDocumentErrorKind::MissingSchema, + ), + ( + "schema = 7\nschema_version = 1\n", + ConfigDocumentErrorKind::InvalidSchema, + ), + ( + "schema = \"radroots.other.config\"\nschema_version = 1\nunknown = true\n", + ConfigDocumentErrorKind::SchemaMismatch, + ), + ( + "schema = \"radroots.example.config\"\n", + ConfigDocumentErrorKind::MissingSchemaVersion, + ), + ( + "schema = \"radroots.example.config\"\nschema_version = \"one\"\n", + ConfigDocumentErrorKind::InvalidSchemaVersion, + ), + ( + "schema = \"radroots.example.config\"\nschema_version = 0\n", + ConfigDocumentErrorKind::InvalidSchemaVersion, + ), + ( + "schema = \"radroots.example.config\"\nschema_version = 4294967296\n", + ConfigDocumentErrorKind::InvalidSchemaVersion, + ), + ( + "schema = \"radroots.example.config\"\nschema_version = 2\n", + ConfigDocumentErrorKind::UnsupportedSchemaVersion, + ), + ] { + let source = RecordingSource::new(source.as_bytes().to_vec()); + assert_eq!( + load_with_source::<StrictDocument, _>( + &source, + Path::new(SOURCE_PATH), + &expectation(), + ) + .unwrap_err() + .kind(), + kind + ); + } + } + + #[test] + fn unknown_field_policy_is_owned_by_the_exact_typed_document() { + let source = RecordingSource::new( + concat!( + "schema = \"radroots.example.config\"\n", + "schema_version = 1\n", + "extension = true\n", + ) + .as_bytes() + .to_vec(), + ); + let permissive = load_with_source::<PermissiveDocument, _>( + &source, + Path::new(SOURCE_PATH), + &expectation(), + ) + .unwrap(); + assert_eq!(permissive.schema, "radroots.example.config"); + assert_eq!(permissive.schema_version, 1); + } + + #[test] + fn expectation_validation_is_exact_and_bounded() { + assert_eq!( + ConfigDocumentExpectation::new("", 1).unwrap_err(), + ConfigDocumentExpectationError::InvalidSchema + ); + assert_eq!( + ConfigDocumentExpectation::new(".radroots.config", 1).unwrap_err(), + ConfigDocumentExpectationError::InvalidSchema + ); + assert_eq!( + ConfigDocumentExpectation::new("bad schema", 1).unwrap_err(), + ConfigDocumentExpectationError::InvalidSchema + ); + assert_eq!( + ConfigDocumentExpectation::new("a".repeat(CONFIG_SCHEMA_ID_MAX_UTF8_BYTES + 1), 1,) + .unwrap_err(), + ConfigDocumentExpectationError::InvalidSchema + ); + assert_eq!( + ConfigDocumentExpectation::new("radroots.example.config", 0).unwrap_err(), + ConfigDocumentExpectationError::InvalidSchemaVersion + ); + } + + struct RecordingSource { + bytes: Vec<u8>, + opened: RefCell<Vec<PathBuf>>, + } + + impl RecordingSource { + fn new(bytes: Vec<u8>) -> Self { + Self { + bytes, + opened: RefCell::new(Vec::new()), + } + } + } + + impl DocumentSource for RecordingSource { + type Reader = Cursor<Vec<u8>>; + + fn open_selected(&self, source_path: &Path) -> io::Result<Self::Reader> { + self.opened.borrow_mut().push(source_path.to_path_buf()); + Ok(Cursor::new(self.bytes.clone())) + } + } + + #[test] + fn loader_performs_exactly_one_selected_read_through_its_only_io_capability() { + let source = RecordingSource::new(valid_document().as_bytes().to_vec()); + let selected = Path::new(SOURCE_PATH); + let loaded = load_with_source::<StrictDocument, _>(&source, selected, &expectation()) + .expect("selected document"); + + assert_eq!(loaded.service.instance, "default"); + assert_eq!(source.opened.borrow().as_slice(), [selected]); + } + + fn assert_public_error_chain_is_safe(error: &ConfigDocumentError, forbidden: &[&str]) { + let mut rendered = format!("{error:?}\n{error}"); + let mut source = error.source(); + while let Some(current) = source { + rendered.push_str(&format!("\n{current:?}\n{current}")); + source = current.source(); + } + for value in forbidden { + assert!(!rendered.contains(value), "public error leaked `{value}`"); + } + } +} diff --git a/crates/service_host/src/config/mod.rs b/crates/service_host/src/config/mod.rs @@ -0,0 +1,9 @@ +//! Strict, service-neutral configuration document mechanics. + +mod document; + +pub use document::{ + CONFIG_DOCUMENT_MAX_UTF8_BYTES, CONFIG_SCHEMA_ID_MAX_UTF8_BYTES, ConfigDocumentError, + ConfigDocumentErrorKind, ConfigDocumentExpectation, ConfigDocumentExpectationError, + ConfigDocumentLocation, load_config_document, +}; diff --git a/crates/service_host/src/lib.rs b/crates/service_host/src/lib.rs @@ -4,6 +4,7 @@ pub mod admin; pub mod build_info; +pub mod config; pub mod entropy; pub mod error; pub mod lifecycle; @@ -38,6 +39,11 @@ pub use build_info::{ BuildInfo, BuildInfoEnvironment, BuildInfoError, BuildInfoField, BuildMode, ContractVersions, StatusBuildInfo, }; +pub use config::{ + CONFIG_DOCUMENT_MAX_UTF8_BYTES, CONFIG_SCHEMA_ID_MAX_UTF8_BYTES, ConfigDocumentError, + ConfigDocumentErrorKind, ConfigDocumentExpectation, ConfigDocumentExpectationError, + ConfigDocumentLocation, load_config_document, +}; pub use entropy::{EntropyError, EntropySource, SystemEntropy}; pub use error::{HostError, HostErrorCode, HostErrorKind, SafeHostError}; pub use lifecycle::{ diff --git a/crates/service_host/tests/package_boundary.rs b/crates/service_host/tests/package_boundary.rs @@ -2,6 +2,10 @@ use std::collections::BTreeSet; const MANIFEST: &str = include_str!("../Cargo.toml"); const ROOT: &str = include_str!("../src/lib.rs"); +const CONFIG_SOURCE: &str = concat!( + include_str!("../src/config/mod.rs"), + include_str!("../src/config/document.rs"), +); const ADMIN_SOURCE: &str = concat!( include_str!("../src/admin/mod.rs"), include_str!("../src/admin/client.rs"), @@ -66,6 +70,7 @@ fn service_host_is_unpublished_lint_governed_and_dependency_bounded() { "serde_json", "tokio", "tokio-util", + "toml", ]) ); assert_eq!( @@ -73,6 +78,7 @@ fn service_host_is_unpublished_lint_governed_and_dependency_bounded() { BTreeSet::from([ "admin", "build_info", + "config", "entropy", "error", "lifecycle", @@ -90,6 +96,20 @@ fn service_host_is_unpublished_lint_governed_and_dependency_bounded() { assert!(!OPERATIONS_PRIMITIVES_SOURCE.contains(forbidden)); } assert!(!OPERATIONS_SOURCE.contains("process::exit")); + let config_production = CONFIG_SOURCE.split_once("#[cfg(test)]").unwrap().0; + for forbidden in [ + "create_dir", + "read_dir", + "std::env", + "TcpStream", + "UdpSocket", + "SystemTime", + "MonotonicClock", + "tokio::", + "process::", + ] { + assert!(!config_production.contains(forbidden)); + } } fn public_modules(root: &str) -> BTreeSet<&str> {