commit 99949e7ed4eb8950b11d3a297c530a4f85121a25
parent 63fe5ef47a3931d8b5b5b7f0ce60a95daa918338
Author: triesap <tyson@radroots.org>
Date: Tue, 11 Aug 2026 05:55:50 +0000
service-host: add validated config values
- parse canonical positive duration and binary byte units
- enforce compile-time positive count maxima
- freeze structured JSON logging configuration
- validate optional operations binds through network policy
Diffstat:
4 files changed, 773 insertions(+), 20 deletions(-)
diff --git a/crates/service_host/src/config/mod.rs b/crates/service_host/src/config/mod.rs
@@ -1,9 +1,14 @@
//! Strict, service-neutral configuration document mechanics.
mod document;
+mod value;
pub use document::{
CONFIG_DOCUMENT_MAX_UTF8_BYTES, CONFIG_SCHEMA_ID_MAX_UTF8_BYTES, ConfigDocumentError,
ConfigDocumentErrorKind, ConfigDocumentExpectation, ConfigDocumentExpectationError,
ConfigDocumentLocation, load_config_document,
};
+pub use value::{
+ BoundedCount, BoundedCountError, ByteLimit, ByteLimitError, LoggingFormat, LoggingFormatError,
+ OptionalOperationsBind, OptionalOperationsBindError, PositiveDuration, PositiveDurationError,
+};
diff --git a/crates/service_host/src/config/value.rs b/crates/service_host/src/config/value.rs
@@ -0,0 +1,746 @@
+//! Validated service-neutral configuration leaf values.
+
+use core::fmt;
+use core::str::FromStr;
+use core::time::Duration;
+use std::error::Error;
+
+use serde::de::Error as _;
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+
+use crate::operations::{
+ OperationsBindPolicy, OperationsConfigError, OperationsListenAddress, OperationsListenerConfig,
+ OperationsTransportLimits,
+};
+
+const NANOSECONDS_PER_MICROSECOND: u64 = 1_000;
+const NANOSECONDS_PER_MILLISECOND: u64 = 1_000_000;
+const NANOSECONDS_PER_SECOND: u64 = 1_000_000_000;
+const NANOSECONDS_PER_MINUTE: u64 = 60 * NANOSECONDS_PER_SECOND;
+const NANOSECONDS_PER_HOUR: u64 = 60 * NANOSECONDS_PER_MINUTE;
+const NANOSECONDS_PER_DAY: u64 = 24 * NANOSECONDS_PER_HOUR;
+
+const BYTES_PER_KIBIBYTE: u64 = 1024;
+const BYTES_PER_MEBIBYTE: u64 = 1024 * BYTES_PER_KIBIBYTE;
+const BYTES_PER_GIBIBYTE: u64 = 1024 * BYTES_PER_MEBIBYTE;
+const BYTES_PER_TEBIBYTE: u64 = 1024 * BYTES_PER_GIBIBYTE;
+
+/// A positive duration represented by at most `u64::MAX` nanoseconds.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub struct PositiveDuration(Duration);
+
+impl PositiveDuration {
+ /// Validates a positive, canonically representable duration.
+ pub fn new(value: Duration) -> Result<Self, PositiveDurationError> {
+ if value.is_zero() {
+ return Err(PositiveDurationError::Zero);
+ }
+ if value.as_nanos() > u128::from(u64::MAX) {
+ return Err(PositiveDurationError::Overflow);
+ }
+ Ok(Self(value))
+ }
+
+ /// Returns the validated standard duration.
+ #[must_use]
+ pub const fn duration(self) -> Duration {
+ self.0
+ }
+
+ /// Returns the exact total nanoseconds.
+ #[must_use]
+ pub fn nanoseconds(self) -> u64 {
+ u64::try_from(self.0.as_nanos()).expect("validated positive duration fits u64 nanoseconds")
+ }
+}
+
+impl FromStr for PositiveDuration {
+ type Err = PositiveDurationError;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ let nanoseconds = parse_human_quantity(
+ value,
+ &[
+ ("ms", NANOSECONDS_PER_MILLISECOND),
+ ("us", NANOSECONDS_PER_MICROSECOND),
+ ("ns", 1),
+ ("d", NANOSECONDS_PER_DAY),
+ ("h", NANOSECONDS_PER_HOUR),
+ ("m", NANOSECONDS_PER_MINUTE),
+ ("s", NANOSECONDS_PER_SECOND),
+ ],
+ )
+ .map_err(PositiveDurationError::from_quantity)?;
+ Self::new(Duration::from_nanos(nanoseconds))
+ }
+}
+
+impl fmt::Display for PositiveDuration {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ format_human_quantity(
+ formatter,
+ self.nanoseconds(),
+ &[
+ ("d", NANOSECONDS_PER_DAY),
+ ("h", NANOSECONDS_PER_HOUR),
+ ("m", NANOSECONDS_PER_MINUTE),
+ ("s", NANOSECONDS_PER_SECOND),
+ ("ms", NANOSECONDS_PER_MILLISECOND),
+ ("us", NANOSECONDS_PER_MICROSECOND),
+ ("ns", 1),
+ ],
+ )
+ }
+}
+
+impl Serialize for PositiveDuration {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.collect_str(self)
+ }
+}
+
+impl<'de> Deserialize<'de> for PositiveDuration {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ String::deserialize(deserializer)?
+ .parse()
+ .map_err(D::Error::custom)
+ }
+}
+
+/// Safe positive-duration parse failure.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum PositiveDurationError {
+ Invalid,
+ Zero,
+ Overflow,
+}
+
+impl PositiveDurationError {
+ const fn from_quantity(error: HumanQuantityError) -> Self {
+ match error {
+ HumanQuantityError::Invalid => Self::Invalid,
+ HumanQuantityError::Zero => Self::Zero,
+ HumanQuantityError::Overflow => Self::Overflow,
+ }
+ }
+}
+
+impl fmt::Display for PositiveDurationError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("positive duration is invalid")
+ }
+}
+
+impl Error for PositiveDurationError {}
+
+/// A positive exact byte quantity using canonical binary human units.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub struct ByteLimit(u64);
+
+impl ByteLimit {
+ /// Validates a positive byte quantity.
+ pub const fn new(bytes: u64) -> Result<Self, ByteLimitError> {
+ if bytes == 0 {
+ Err(ByteLimitError::Zero)
+ } else {
+ Ok(Self(bytes))
+ }
+ }
+
+ /// Returns the exact byte quantity.
+ #[must_use]
+ pub const fn bytes(self) -> u64 {
+ self.0
+ }
+}
+
+impl FromStr for ByteLimit {
+ type Err = ByteLimitError;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ let bytes = parse_human_quantity(
+ value,
+ &[
+ ("KiB", BYTES_PER_KIBIBYTE),
+ ("MiB", BYTES_PER_MEBIBYTE),
+ ("GiB", BYTES_PER_GIBIBYTE),
+ ("TiB", BYTES_PER_TEBIBYTE),
+ ("B", 1),
+ ],
+ )
+ .map_err(ByteLimitError::from_quantity)?;
+ Self::new(bytes)
+ }
+}
+
+impl fmt::Display for ByteLimit {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ format_human_quantity(
+ formatter,
+ self.bytes(),
+ &[
+ ("TiB", BYTES_PER_TEBIBYTE),
+ ("GiB", BYTES_PER_GIBIBYTE),
+ ("MiB", BYTES_PER_MEBIBYTE),
+ ("KiB", BYTES_PER_KIBIBYTE),
+ ("B", 1),
+ ],
+ )
+ }
+}
+
+impl Serialize for ByteLimit {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.collect_str(self)
+ }
+}
+
+impl<'de> Deserialize<'de> for ByteLimit {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ String::deserialize(deserializer)?
+ .parse()
+ .map_err(D::Error::custom)
+ }
+}
+
+/// Safe byte-limit parse failure.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum ByteLimitError {
+ Invalid,
+ Zero,
+ Overflow,
+}
+
+impl ByteLimitError {
+ const fn from_quantity(error: HumanQuantityError) -> Self {
+ match error {
+ HumanQuantityError::Invalid => Self::Invalid,
+ HumanQuantityError::Zero => Self::Zero,
+ HumanQuantityError::Overflow => Self::Overflow,
+ }
+ }
+}
+
+impl fmt::Display for ByteLimitError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("byte limit is invalid")
+ }
+}
+
+impl Error for ByteLimitError {}
+
+/// A positive count bounded by its compile-time service-owned maximum.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub struct BoundedCount<const MAXIMUM: u32>(u32);
+
+impl<const MAXIMUM: u32> BoundedCount<MAXIMUM> {
+ /// Validates one positive count against the compile-time maximum.
+ pub const fn new(value: u32) -> Result<Self, BoundedCountError> {
+ if MAXIMUM == 0 {
+ Err(BoundedCountError::InvalidMaximum)
+ } else if value == 0 {
+ Err(BoundedCountError::Zero)
+ } else if value > MAXIMUM {
+ Err(BoundedCountError::ExceedsMaximum)
+ } else {
+ Ok(Self(value))
+ }
+ }
+
+ /// Returns the exact count.
+ #[must_use]
+ pub const fn value(self) -> u32 {
+ self.0
+ }
+
+ /// Returns the compile-time maximum.
+ #[must_use]
+ pub const fn maximum() -> u32 {
+ MAXIMUM
+ }
+}
+
+impl<const MAXIMUM: u32> fmt::Display for BoundedCount<MAXIMUM> {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(formatter)
+ }
+}
+
+impl<const MAXIMUM: u32> Serialize for BoundedCount<MAXIMUM> {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_u32(self.0)
+ }
+}
+
+impl<'de, const MAXIMUM: u32> Deserialize<'de> for BoundedCount<MAXIMUM> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let value = u64::deserialize(deserializer)?;
+ let value = u32::try_from(value)
+ .map_err(|_| D::Error::custom(BoundedCountError::ExceedsMaximum))?;
+ Self::new(value).map_err(D::Error::custom)
+ }
+}
+
+/// Safe bounded-count validation failure.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum BoundedCountError {
+ InvalidMaximum,
+ Zero,
+ ExceedsMaximum,
+}
+
+impl fmt::Display for BoundedCountError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("bounded count is outside its supported positive range")
+ }
+}
+
+impl Error for BoundedCountError {}
+
+/// Closed v1 logging encoding for safe structured service logs.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub enum LoggingFormat {
+ #[default]
+ Json,
+}
+
+impl FromStr for LoggingFormat {
+ type Err = LoggingFormatError;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ match value {
+ "json" => Ok(Self::Json),
+ _ => Err(LoggingFormatError::Unsupported),
+ }
+ }
+}
+
+impl fmt::Display for LoggingFormat {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(match self {
+ Self::Json => "json",
+ })
+ }
+}
+
+impl Serialize for LoggingFormat {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.collect_str(self)
+ }
+}
+
+impl<'de> Deserialize<'de> for LoggingFormat {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ String::deserialize(deserializer)?
+ .parse()
+ .map_err(D::Error::custom)
+ }
+}
+
+/// Safe unsupported logging-format failure.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum LoggingFormatError {
+ Unsupported,
+}
+
+impl fmt::Display for LoggingFormatError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("logging format is unsupported")
+ }
+}
+
+impl Error for LoggingFormatError {}
+
+/// Explicitly disabled or explicitly addressed operations binding.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum OptionalOperationsBind {
+ Disabled,
+ Listen(OperationsListenAddress),
+}
+
+impl OptionalOperationsBind {
+ /// Returns a disabled operations binding.
+ #[must_use]
+ pub const fn disabled() -> Self {
+ Self::Disabled
+ }
+
+ /// Returns an explicitly addressed operations binding.
+ #[must_use]
+ pub const fn listen(address: OperationsListenAddress) -> Self {
+ Self::Listen(address)
+ }
+
+ /// Returns whether an operations binding is explicitly enabled.
+ #[must_use]
+ pub const fn is_enabled(self) -> bool {
+ matches!(self, Self::Listen(_))
+ }
+
+ /// Returns the selected address when enabled.
+ #[must_use]
+ pub const fn address(self) -> Option<OperationsListenAddress> {
+ match self {
+ Self::Disabled => None,
+ Self::Listen(address) => Some(address),
+ }
+ }
+
+ /// Applies the existing network-scope and transport-limit authority.
+ pub fn into_listener_config(
+ self,
+ bind_policy: OperationsBindPolicy,
+ limits: OperationsTransportLimits,
+ ) -> Result<OperationsListenerConfig, OperationsConfigError> {
+ match self {
+ Self::Disabled => Ok(OperationsListenerConfig::disabled()),
+ Self::Listen(address) => {
+ OperationsListenerConfig::enabled(address, bind_policy, limits)
+ }
+ }
+ }
+}
+
+impl Default for OptionalOperationsBind {
+ fn default() -> Self {
+ Self::disabled()
+ }
+}
+
+impl FromStr for OptionalOperationsBind {
+ type Err = OptionalOperationsBindError;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ if value == "disabled" {
+ return Ok(Self::Disabled);
+ }
+ value
+ .parse()
+ .map(Self::Listen)
+ .map_err(|_| OptionalOperationsBindError::Invalid)
+ }
+}
+
+impl fmt::Debug for OptionalOperationsBind {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("OptionalOperationsBind")
+ .field("enabled", &self.is_enabled())
+ .field("listen", &self.address().map(|_| "[redacted]"))
+ .finish()
+ }
+}
+
+impl fmt::Display for OptionalOperationsBind {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Disabled => formatter.write_str("disabled"),
+ Self::Listen(address) => address.fmt(formatter),
+ }
+ }
+}
+
+impl Serialize for OptionalOperationsBind {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.collect_str(self)
+ }
+}
+
+impl<'de> Deserialize<'de> for OptionalOperationsBind {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ String::deserialize(deserializer)?
+ .parse()
+ .map_err(D::Error::custom)
+ }
+}
+
+/// Safe optional operations-bind parse failure.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum OptionalOperationsBindError {
+ Invalid,
+}
+
+impl fmt::Display for OptionalOperationsBindError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("optional operations bind is invalid")
+ }
+}
+
+impl Error for OptionalOperationsBindError {}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+enum HumanQuantityError {
+ Invalid,
+ Zero,
+ Overflow,
+}
+
+fn parse_human_quantity(value: &str, units: &[(&str, u64)]) -> Result<u64, HumanQuantityError> {
+ let Some((number, multiplier)) = units.iter().find_map(|(unit, multiplier)| {
+ value.strip_suffix(unit).map(|number| (number, *multiplier))
+ }) else {
+ return Err(HumanQuantityError::Invalid);
+ };
+ if number.is_empty()
+ || !number.bytes().all(|byte| byte.is_ascii_digit())
+ || (number.len() > 1 && number.starts_with('0'))
+ {
+ return Err(HumanQuantityError::Invalid);
+ }
+ let quantity = number
+ .parse::<u64>()
+ .map_err(|_| HumanQuantityError::Overflow)?;
+ if quantity == 0 {
+ return Err(HumanQuantityError::Zero);
+ }
+ quantity
+ .checked_mul(multiplier)
+ .ok_or(HumanQuantityError::Overflow)
+}
+
+fn format_human_quantity(
+ formatter: &mut fmt::Formatter<'_>,
+ value: u64,
+ units: &[(&str, u64)],
+) -> fmt::Result {
+ let (unit, multiplier) = units
+ .iter()
+ .find(|(_, multiplier)| value.is_multiple_of(*multiplier))
+ .expect("unit inventory ends in multiplier one");
+ write!(formatter, "{}{unit}", value / multiplier)
+}
+
+#[cfg(test)]
+mod tests {
+ use serde::{Deserialize, Serialize};
+
+ use super::*;
+
+ #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
+ #[serde(deny_unknown_fields)]
+ struct Values {
+ duration: PositiveDuration,
+ bytes: ByteLimit,
+ count: BoundedCount<64>,
+ logging: LoggingFormat,
+ operations_bind: OptionalOperationsBind,
+ }
+
+ #[test]
+ fn human_duration_units_parse_and_display_canonically() {
+ for (source, nanoseconds, display) in [
+ ("1ns", 1, "1ns"),
+ ("1us", 1_000, "1us"),
+ ("1ms", 1_000_000, "1ms"),
+ ("1500ms", 1_500_000_000, "1500ms"),
+ ("1s", 1_000_000_000, "1s"),
+ ("60s", 60_000_000_000, "1m"),
+ ("1m", 60_000_000_000, "1m"),
+ ("1h", 3_600_000_000_000, "1h"),
+ ("1d", 86_400_000_000_000, "1d"),
+ ] {
+ let value: PositiveDuration = source.parse().unwrap();
+ assert_eq!(value.nanoseconds(), nanoseconds);
+ assert_eq!(value.to_string(), display);
+ assert_eq!(value.duration().as_nanos(), u128::from(nanoseconds));
+ }
+ }
+
+ #[test]
+ fn duration_zero_invalid_and_overflow_inputs_fail_closed() {
+ for source in ["", "1", "0s", "01s", "+1s", "-1s", "1.5s", "1S", " 1s"] {
+ assert!(source.parse::<PositiveDuration>().is_err(), "{source}");
+ }
+ assert_eq!(
+ "18446744073709551616ns"
+ .parse::<PositiveDuration>()
+ .unwrap_err(),
+ PositiveDurationError::Overflow
+ );
+ assert_eq!(
+ "18446744073709551615d"
+ .parse::<PositiveDuration>()
+ .unwrap_err(),
+ PositiveDurationError::Overflow
+ );
+ assert_eq!(
+ PositiveDuration::new(Duration::MAX).unwrap_err(),
+ PositiveDurationError::Overflow
+ );
+ let maximum = PositiveDuration::new(Duration::from_nanos(u64::MAX)).unwrap();
+ assert_eq!(maximum.nanoseconds(), u64::MAX);
+ assert_eq!(maximum.to_string(), format!("{}ns", u64::MAX));
+ }
+
+ #[test]
+ fn binary_byte_units_parse_and_display_canonically() {
+ for (source, bytes, display) in [
+ ("1B", 1, "1B"),
+ ("1KiB", 1024, "1KiB"),
+ ("1536B", 1536, "1536B"),
+ ("1MiB", 1_048_576, "1MiB"),
+ ("1GiB", 1_073_741_824, "1GiB"),
+ ("1TiB", 1_099_511_627_776, "1TiB"),
+ ] {
+ let value: ByteLimit = source.parse().unwrap();
+ assert_eq!(value.bytes(), bytes);
+ assert_eq!(value.to_string(), display);
+ }
+ }
+
+ #[test]
+ fn byte_zero_invalid_and_overflow_inputs_fail_closed() {
+ for source in ["", "1", "0B", "01B", "+1B", "-1B", "1.5KiB", "1KB", " 1B"] {
+ assert!(source.parse::<ByteLimit>().is_err(), "{source}");
+ }
+ assert_eq!(
+ "18446744073709551616B".parse::<ByteLimit>().unwrap_err(),
+ ByteLimitError::Overflow
+ );
+ assert_eq!(
+ "18446744073709551615KiB".parse::<ByteLimit>().unwrap_err(),
+ ByteLimitError::Overflow
+ );
+ let maximum = ByteLimit::new(u64::MAX).unwrap();
+ assert_eq!(maximum.bytes(), u64::MAX);
+ assert_eq!(maximum.to_string(), format!("{}B", u64::MAX));
+ }
+
+ #[test]
+ fn bounded_count_rejects_zero_invalid_maximum_overflow_and_just_over() {
+ assert_eq!(BoundedCount::<64>::new(1).unwrap().value(), 1);
+ assert_eq!(BoundedCount::<64>::new(64).unwrap().value(), 64);
+ assert_eq!(BoundedCount::<64>::maximum(), 64);
+ assert_eq!(
+ BoundedCount::<64>::new(0).unwrap_err(),
+ BoundedCountError::Zero
+ );
+ assert_eq!(
+ BoundedCount::<64>::new(65).unwrap_err(),
+ BoundedCountError::ExceedsMaximum
+ );
+ assert_eq!(
+ BoundedCount::<0>::new(1).unwrap_err(),
+ BoundedCountError::InvalidMaximum
+ );
+ assert!(toml::from_str::<Values>(
+ "duration='1s'\nbytes='1KiB'\ncount=4294967296\nlogging='json'\noperations_bind='disabled'",
+ )
+ .is_err());
+ }
+
+ #[test]
+ fn serde_and_display_are_exact_for_every_common_leaf() {
+ let source = concat!(
+ "duration = \"2m\"\n",
+ "bytes = \"8MiB\"\n",
+ "count = 32\n",
+ "logging = \"json\"\n",
+ "operations_bind = \"127.0.0.1:9100\"\n",
+ );
+ let values: Values = toml::from_str(source).unwrap();
+ assert_eq!(values.duration.to_string(), "2m");
+ assert_eq!(values.bytes.to_string(), "8MiB");
+ assert_eq!(values.count.to_string(), "32");
+ assert_eq!(values.logging.to_string(), "json");
+ assert_eq!(values.operations_bind.to_string(), "127.0.0.1:9100");
+ assert_eq!(toml::to_string(&values).unwrap(), source);
+ }
+
+ #[test]
+ fn logging_format_is_closed_to_structured_json() {
+ assert_eq!(LoggingFormat::default(), LoggingFormat::Json);
+ assert_eq!("json".parse(), Ok(LoggingFormat::Json));
+ for source in ["", "JSON", "text", "pretty", "compact"] {
+ assert_eq!(
+ source.parse::<LoggingFormat>(),
+ Err(LoggingFormatError::Unsupported)
+ );
+ }
+ }
+
+ #[test]
+ fn optional_operations_bind_is_explicit_redacted_and_policy_checked() {
+ let disabled: OptionalOperationsBind = "disabled".parse().unwrap();
+ assert!(!disabled.is_enabled());
+ assert_eq!(disabled.address(), None);
+ assert_eq!(disabled.to_string(), "disabled");
+ assert_eq!(
+ disabled
+ .into_listener_config(
+ OperationsBindPolicy::LoopbackOnly,
+ OperationsTransportLimits::DEFAULT,
+ )
+ .unwrap(),
+ OperationsListenerConfig::disabled()
+ );
+
+ let loopback: OptionalOperationsBind = "127.0.0.1:9100".parse().unwrap();
+ assert!(loopback.is_enabled());
+ assert_eq!(loopback.to_string(), "127.0.0.1:9100");
+ assert!(!format!("{loopback:?}").contains("127.0.0.1"));
+ assert!(
+ loopback
+ .into_listener_config(
+ OperationsBindPolicy::LoopbackOnly,
+ OperationsTransportLimits::DEFAULT,
+ )
+ .is_ok()
+ );
+
+ let public: OptionalOperationsBind = "0.0.0.0:9100".parse().unwrap();
+ assert!(
+ public
+ .into_listener_config(
+ OperationsBindPolicy::LoopbackOnly,
+ OperationsTransportLimits::DEFAULT,
+ )
+ .is_err()
+ );
+ assert!(
+ public
+ .into_listener_config(
+ OperationsBindPolicy::Public,
+ OperationsTransportLimits::DEFAULT,
+ )
+ .is_ok()
+ );
+
+ for source in ["", "Disabled", "127.0.0.1:0", "localhost:9100"] {
+ assert_eq!(
+ source.parse::<OptionalOperationsBind>(),
+ Err(OptionalOperationsBindError::Invalid)
+ );
+ }
+ }
+}
diff --git a/crates/service_host/src/lib.rs b/crates/service_host/src/lib.rs
@@ -40,9 +40,11 @@ pub use build_info::{
StatusBuildInfo,
};
pub use config::{
- CONFIG_DOCUMENT_MAX_UTF8_BYTES, CONFIG_SCHEMA_ID_MAX_UTF8_BYTES, ConfigDocumentError,
- ConfigDocumentErrorKind, ConfigDocumentExpectation, ConfigDocumentExpectationError,
- ConfigDocumentLocation, load_config_document,
+ BoundedCount, BoundedCountError, ByteLimit, ByteLimitError, CONFIG_DOCUMENT_MAX_UTF8_BYTES,
+ CONFIG_SCHEMA_ID_MAX_UTF8_BYTES, ConfigDocumentError, ConfigDocumentErrorKind,
+ ConfigDocumentExpectation, ConfigDocumentExpectationError, ConfigDocumentLocation,
+ LoggingFormat, LoggingFormatError, OptionalOperationsBind, OptionalOperationsBindError,
+ PositiveDuration, PositiveDurationError, load_config_document,
};
pub use entropy::{EntropyError, EntropySource, SystemEntropy};
pub use error::{HostError, HostErrorCode, HostErrorKind, SafeHostError};
diff --git a/crates/service_host/tests/package_boundary.rs b/crates/service_host/tests/package_boundary.rs
@@ -2,10 +2,8 @@ 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 CONFIG_DOCUMENT_SOURCE: &str = include_str!("../src/config/document.rs");
+const CONFIG_VALUE_SOURCE: &str = include_str!("../src/config/value.rs");
const ADMIN_SOURCE: &str = concat!(
include_str!("../src/admin/mod.rs"),
include_str!("../src/admin/client.rs"),
@@ -96,19 +94,21 @@ 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));
+ for source in [CONFIG_DOCUMENT_SOURCE, CONFIG_VALUE_SOURCE] {
+ let production = source.split_once("#[cfg(test)]").unwrap().0;
+ for forbidden in [
+ "create_dir",
+ "read_dir",
+ "std::env",
+ "TcpStream",
+ "UdpSocket",
+ "SystemTime",
+ "MonotonicClock",
+ "tokio::",
+ "process::",
+ ] {
+ assert!(!production.contains(forbidden));
+ }
}
}