commit 92b0585b4e1e5dd6dd536e49a4a6efee69de20d7
parent b30761b8fdf8c04d8e4d4de3e5db12d26fdbd600
Author: triesap <tyson@radroots.org>
Date: Sat, 15 Aug 2026 21:51:16 +0000
service-host: bound status and admin ingestion
Diffstat:
6 files changed, 738 insertions(+), 143 deletions(-)
diff --git a/crates/service_host/README.md b/crates/service_host/README.md
@@ -95,6 +95,12 @@ exclude raw paths, SQL, credentials, payloads, and provider or relay errors.
Where a host error retains an original cause for trusted inspection, that cause
is not rendered by its safe projection.
+Reason-code iterators and wire arrays stop at the first item beyond their fixed
+maximum. Administration payloads are traversed directly from the bounded input
+or streamed directly into the capped response writer; validation does not
+materialize an intermediate `serde_json::Value` tree. Recursive null rejection
+and duplicate or unknown field rejection remain fail closed.
+
## Process and runtime ownership
The crate does not parse a CLI, read configuration from environment variables,
diff --git a/crates/service_host/src/admin/client.rs b/crates/service_host/src/admin/client.rs
@@ -2,7 +2,7 @@
use core::fmt;
use serde::{Deserialize, Deserializer, Serialize, de, de::DeserializeOwned};
-use serde_json::Value;
+use std::collections::BTreeSet;
use std::error::Error;
use std::io;
use std::path::{Path, PathBuf};
@@ -300,7 +300,7 @@ impl AdminClient {
AdminClientError::sourced(AdminClientErrorKind::RequestEncoding, error)
}
})?;
- let _: StrictJsonValue = serde_json::from_slice(&body).map_err(|error| {
+ let _: StrictJsonPayload = serde_json::from_slice(&body).map_err(|error| {
AdminClientError::sourced(AdminClientErrorKind::RequestEncoding, error)
})?;
self.execute(AdminHttpMethod::Post, target, Bytes::from(body))
@@ -521,16 +521,10 @@ fn decode_response<T>(
where
T: DeserializeOwned + Serialize,
{
- let strict = serde_json::from_slice::<StrictJsonValue>(body).map_err(|error| {
+ let strict = serde_json::from_slice::<StrictResponseEnvelope>(body).map_err(|error| {
AdminClientError::sourced(AdminClientErrorKind::MalformedResponse, error)
})?;
- let version = strict
- .0
- .as_object()
- .and_then(|object| object.get("contract_version"))
- .and_then(Value::as_u64)
- .ok_or_else(|| AdminClientError::simple(AdminClientErrorKind::MalformedResponse))?;
- if version != u64::from(ADMIN_CONTRACT_VERSION) {
+ if strict.contract_version != u64::from(ADMIN_CONTRACT_VERSION) {
return Err(AdminClientError::simple(
AdminClientErrorKind::UnsupportedContractVersion,
));
@@ -647,9 +641,10 @@ where
}
}
-struct StrictJsonValue(Value);
+#[derive(Clone, Copy)]
+struct StrictJsonPayload;
-impl<'de> Deserialize<'de> for StrictJsonValue {
+impl<'de> Deserialize<'de> for StrictJsonPayload {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
@@ -661,36 +656,37 @@ impl<'de> Deserialize<'de> for StrictJsonValue {
struct StrictJsonVisitor;
impl<'de> de::Visitor<'de> for StrictJsonVisitor {
- type Value = StrictJsonValue;
+ type Value = StrictJsonPayload;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("JSON without duplicate object keys or null values")
}
- fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
- Ok(StrictJsonValue(Value::Bool(value)))
+ fn visit_bool<E>(self, _value: bool) -> Result<Self::Value, E> {
+ Ok(StrictJsonPayload)
}
- fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
- Ok(StrictJsonValue(Value::Number(value.into())))
+ fn visit_i64<E>(self, _value: i64) -> Result<Self::Value, E> {
+ Ok(StrictJsonPayload)
}
- fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
- Ok(StrictJsonValue(Value::Number(value.into())))
+ fn visit_u64<E>(self, _value: u64) -> Result<Self::Value, E> {
+ Ok(StrictJsonPayload)
}
fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
where
E: de::Error,
{
- serde_json::Number::from_f64(value)
- .map(Value::Number)
- .map(StrictJsonValue)
- .ok_or_else(|| E::custom("non-finite JSON number"))
+ if value.is_finite() {
+ Ok(StrictJsonPayload)
+ } else {
+ Err(E::custom("non-finite JSON number"))
+ }
}
- fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> {
- Ok(StrictJsonValue(Value::String(value.to_owned())))
+ fn visit_str<E>(self, _value: &str) -> Result<Self::Value, E> {
+ Ok(StrictJsonPayload)
}
fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E>
@@ -700,8 +696,8 @@ impl<'de> de::Visitor<'de> for StrictJsonVisitor {
self.visit_str(value)
}
- fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
- Ok(StrictJsonValue(Value::String(value)))
+ fn visit_string<E>(self, _value: String) -> Result<Self::Value, E> {
+ Ok(StrictJsonPayload)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
@@ -722,26 +718,67 @@ impl<'de> de::Visitor<'de> for StrictJsonVisitor {
where
A: de::SeqAccess<'de>,
{
- let mut values = Vec::with_capacity(sequence.size_hint().unwrap_or(0));
- while let Some(value) = sequence.next_element::<StrictJsonValue>()? {
- values.push(value.0);
+ while sequence.next_element::<StrictJsonPayload>()?.is_some() {}
+ Ok(StrictJsonPayload)
+ }
+
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: de::MapAccess<'de>,
+ {
+ let mut keys = BTreeSet::new();
+ while let Some(key) = map.next_key::<String>()? {
+ if !keys.insert(key) {
+ return Err(de::Error::custom("duplicate JSON object key"));
+ }
+ map.next_value::<StrictJsonPayload>()?;
}
- Ok(StrictJsonValue(Value::Array(values)))
+ Ok(StrictJsonPayload)
+ }
+}
+
+struct StrictResponseEnvelope {
+ contract_version: u64,
+}
+
+impl<'de> Deserialize<'de> for StrictResponseEnvelope {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer.deserialize_map(StrictResponseVisitor)
+ }
+}
+
+struct StrictResponseVisitor;
+
+impl<'de> de::Visitor<'de> for StrictResponseVisitor {
+ type Value = StrictResponseEnvelope;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("an admin response envelope without duplicate keys or null values")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
- let mut values = serde_json::Map::with_capacity(map.size_hint().unwrap_or(0));
+ let mut keys = BTreeSet::new();
+ let mut contract_version = None;
while let Some(key) = map.next_key::<String>()? {
- if values.contains_key(&key) {
+ if !keys.insert(key.clone()) {
return Err(de::Error::custom("duplicate JSON object key"));
}
- let value = map.next_value::<StrictJsonValue>()?;
- values.insert(key, value.0);
+ if key == "contract_version" {
+ contract_version = Some(map.next_value::<u64>()?);
+ } else {
+ map.next_value::<StrictJsonPayload>()?;
+ }
}
- Ok(StrictJsonValue(Value::Object(values)))
+ Ok(StrictResponseEnvelope {
+ contract_version: contract_version
+ .ok_or_else(|| de::Error::missing_field("contract_version"))?,
+ })
}
}
@@ -1179,25 +1216,19 @@ mod tests {
#[test]
fn strict_response_target_and_error_helpers_cover_the_full_value_surface() {
- for (document, expected) in [
- ("true", Value::Bool(true)),
- ("-3", Value::Number((-3).into())),
- ("4", Value::Number(4_u64.into())),
- (
- "2.5",
- Value::Number(serde_json::Number::from_f64(2.5).unwrap()),
- ),
- (r#""text""#, Value::String("text".to_owned())),
- ("[true,2]", serde_json::json!([true, 2])),
- (r#"{"value":3}"#, serde_json::json!({"value": 3})),
+ for document in [
+ "true",
+ "-3",
+ "4",
+ "2.5",
+ r#""text""#,
+ "[true,2]",
+ r#"{"value":3}"#,
] {
- assert_eq!(
- serde_json::from_str::<StrictJsonValue>(document).unwrap().0,
- expected
- );
+ serde_json::from_str::<StrictJsonPayload>(document).unwrap();
}
for rejected in ["null", "[1,null]", r#"{"same":1,"same":2}"#] {
- assert!(serde_json::from_str::<StrictJsonValue>(rejected).is_err());
+ assert!(serde_json::from_str::<StrictJsonPayload>(rejected).is_err());
}
let target = AdminClientTarget::new("/v1/items/value%2D1?page=1&limit=2").unwrap();
diff --git a/crates/service_host/src/admin/model.rs b/crates/service_host/src/admin/model.rs
@@ -208,7 +208,7 @@ where
T: Serialize,
{
fn new(value: T) -> Result<Self, AdminPayloadError> {
- checked_json_value(&value)?;
+ checked_non_null(&value)?;
Ok(Self(value))
}
}
@@ -221,9 +221,8 @@ where
where
S: Serializer,
{
- checked_json_value(&self.0)
- .map_err(ser::Error::custom)?
- .serialize(serializer)
+ checked_non_null(&self.0).map_err(ser::Error::custom)?;
+ self.0.serialize(serializer)
}
}
@@ -846,23 +845,444 @@ where
}
}
-fn checked_json_value(value: &impl Serialize) -> Result<serde_json::Value, AdminPayloadError> {
- let value = serde_json::to_value(value).map_err(|_| AdminPayloadError::Encoding)?;
- if contains_null(&value) {
- Err(AdminPayloadError::NullForbidden)
- } else {
- Ok(value)
+#[derive(Clone, Copy, Debug)]
+enum PayloadValidationError {
+ Null,
+ Encoding,
+}
+
+impl fmt::Display for PayloadValidationError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("admin payload validation failed")
+ }
+}
+
+impl Error for PayloadValidationError {}
+
+impl ser::Error for PayloadValidationError {
+ fn custom<T>(_message: T) -> Self
+ where
+ T: fmt::Display,
+ {
+ Self::Encoding
+ }
+}
+
+#[derive(Clone, Copy)]
+struct NonNullSerializer<'failure> {
+ failure: &'failure core::cell::Cell<Option<PayloadValidationError>>,
+}
+
+impl NonNullSerializer<'_> {
+ fn finish(self) -> Result<(), PayloadValidationError> {
+ self.failure.get().map_or(Ok(()), Err)
+ }
+
+ fn reject(self, error: PayloadValidationError) -> Result<(), PayloadValidationError> {
+ if self.failure.get().is_none() {
+ self.failure.set(Some(error));
+ }
+ Err(error)
+ }
+}
+
+impl<'failure> ser::Serializer for NonNullSerializer<'failure> {
+ type Ok = ();
+ type Error = PayloadValidationError;
+ type SerializeSeq = NonNullCompound<'failure>;
+ type SerializeTuple = NonNullCompound<'failure>;
+ type SerializeTupleStruct = NonNullCompound<'failure>;
+ type SerializeTupleVariant = NonNullCompound<'failure>;
+ type SerializeMap = NonNullCompound<'failure>;
+ type SerializeStruct = NonNullCompound<'failure>;
+ type SerializeStructVariant = NonNullCompound<'failure>;
+
+ fn serialize_bool(self, _value: bool) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+
+ fn serialize_i8(self, _value: i8) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+
+ fn serialize_i16(self, _value: i16) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+
+ fn serialize_i32(self, _value: i32) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+
+ fn serialize_i64(self, _value: i64) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+
+ fn serialize_i128(self, _value: i128) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+
+ fn serialize_u8(self, _value: u8) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+
+ fn serialize_u16(self, _value: u16) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+
+ fn serialize_u32(self, _value: u32) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+
+ fn serialize_u64(self, _value: u64) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+
+ fn serialize_u128(self, _value: u128) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+
+ fn serialize_f32(self, _value: f32) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+
+ fn serialize_f64(self, _value: f64) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+
+ fn serialize_char(self, _value: char) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+
+ fn serialize_str(self, _value: &str) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+
+ fn serialize_bytes(self, _value: &[u8]) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+
+ fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
+ self.reject(PayloadValidationError::Null)
+ }
+
+ fn serialize_some<T>(self, value: &T) -> Result<Self::Ok, Self::Error>
+ where
+ T: ?Sized + Serialize,
+ {
+ value.serialize(self)
+ }
+
+ fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
+ self.reject(PayloadValidationError::Null)
+ }
+
+ fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
+ self.reject(PayloadValidationError::Null)
+ }
+
+ fn serialize_unit_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ ) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+
+ fn serialize_newtype_struct<T>(
+ self,
+ _name: &'static str,
+ value: &T,
+ ) -> Result<Self::Ok, Self::Error>
+ where
+ T: ?Sized + Serialize,
+ {
+ value.serialize(self)
+ }
+
+ fn serialize_newtype_variant<T>(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ value: &T,
+ ) -> Result<Self::Ok, Self::Error>
+ where
+ T: ?Sized + Serialize,
+ {
+ value.serialize(self)
+ }
+
+ fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
+ self.finish()?;
+ Ok(NonNullCompound(self))
+ }
+
+ fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
+ self.finish()?;
+ Ok(NonNullCompound(self))
+ }
+
+ fn serialize_tuple_struct(
+ self,
+ _name: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeTupleStruct, Self::Error> {
+ self.finish()?;
+ Ok(NonNullCompound(self))
+ }
+
+ fn serialize_tuple_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeTupleVariant, Self::Error> {
+ self.finish()?;
+ Ok(NonNullCompound(self))
+ }
+
+ fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
+ self.finish()?;
+ Ok(NonNullCompound(self))
+ }
+
+ fn serialize_struct(
+ self,
+ _name: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeStruct, Self::Error> {
+ self.finish()?;
+ Ok(NonNullCompound(self))
+ }
+
+ fn serialize_struct_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeStructVariant, Self::Error> {
+ self.finish()?;
+ Ok(NonNullCompound(self))
+ }
+
+ fn collect_str<T>(self, _value: &T) -> Result<Self::Ok, Self::Error>
+ where
+ T: ?Sized + fmt::Display,
+ {
+ self.finish()
+ }
+
+ fn is_human_readable(&self) -> bool {
+ true
+ }
+}
+
+struct NonNullCompound<'failure>(NonNullSerializer<'failure>);
+
+impl NonNullCompound<'_> {
+ fn value<T>(&mut self, value: &T) -> Result<(), PayloadValidationError>
+ where
+ T: ?Sized + Serialize,
+ {
+ value.serialize(self.0)
+ }
+
+ fn finish(self) -> Result<(), PayloadValidationError> {
+ self.0.finish()
+ }
+}
+
+impl ser::SerializeSeq for NonNullCompound<'_> {
+ type Ok = ();
+ type Error = PayloadValidationError;
+
+ fn serialize_element<T>(&mut self, value: &T) -> Result<(), Self::Error>
+ where
+ T: ?Sized + Serialize,
+ {
+ self.value(value)
+ }
+
+ fn end(self) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+}
+
+impl ser::SerializeTuple for NonNullCompound<'_> {
+ type Ok = ();
+ type Error = PayloadValidationError;
+
+ fn serialize_element<T>(&mut self, value: &T) -> Result<(), Self::Error>
+ where
+ T: ?Sized + Serialize,
+ {
+ self.value(value)
+ }
+
+ fn end(self) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+}
+
+impl ser::SerializeTupleStruct for NonNullCompound<'_> {
+ type Ok = ();
+ type Error = PayloadValidationError;
+
+ fn serialize_field<T>(&mut self, value: &T) -> Result<(), Self::Error>
+ where
+ T: ?Sized + Serialize,
+ {
+ self.value(value)
+ }
+
+ fn end(self) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+}
+
+impl ser::SerializeTupleVariant for NonNullCompound<'_> {
+ type Ok = ();
+ type Error = PayloadValidationError;
+
+ fn serialize_field<T>(&mut self, value: &T) -> Result<(), Self::Error>
+ where
+ T: ?Sized + Serialize,
+ {
+ self.value(value)
+ }
+
+ fn end(self) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+}
+
+impl ser::SerializeMap for NonNullCompound<'_> {
+ type Ok = ();
+ type Error = PayloadValidationError;
+
+ fn serialize_key<T>(&mut self, key: &T) -> Result<(), Self::Error>
+ where
+ T: ?Sized + Serialize,
+ {
+ self.value(key)
+ }
+
+ fn serialize_value<T>(&mut self, value: &T) -> Result<(), Self::Error>
+ where
+ T: ?Sized + Serialize,
+ {
+ self.value(value)
+ }
+
+ fn end(self) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+}
+
+impl ser::SerializeStruct for NonNullCompound<'_> {
+ type Ok = ();
+ type Error = PayloadValidationError;
+
+ fn serialize_field<T>(&mut self, _key: &'static str, value: &T) -> Result<(), Self::Error>
+ where
+ T: ?Sized + Serialize,
+ {
+ self.value(value)
+ }
+
+ fn end(self) -> Result<Self::Ok, Self::Error> {
+ self.finish()
}
}
-fn contains_null(value: &serde_json::Value) -> bool {
- match value {
- serde_json::Value::Null => true,
- serde_json::Value::Array(values) => values.iter().any(contains_null),
- serde_json::Value::Object(values) => values.values().any(contains_null),
- serde_json::Value::Bool(_)
- | serde_json::Value::Number(_)
- | serde_json::Value::String(_) => false,
+impl ser::SerializeStructVariant for NonNullCompound<'_> {
+ type Ok = ();
+ type Error = PayloadValidationError;
+
+ fn serialize_field<T>(&mut self, _key: &'static str, value: &T) -> Result<(), Self::Error>
+ where
+ T: ?Sized + Serialize,
+ {
+ self.value(value)
+ }
+
+ fn end(self) -> Result<Self::Ok, Self::Error> {
+ self.finish()
+ }
+}
+
+#[derive(Default)]
+struct NonNullJsonWriter {
+ inside_string: bool,
+ escaped: bool,
+ null_progress: usize,
+ rejected: bool,
+}
+
+impl std::io::Write for NonNullJsonWriter {
+ fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
+ if self.rejected {
+ return Err(std::io::Error::other("JSON null is forbidden"));
+ }
+ for &byte in bytes {
+ if self.inside_string {
+ if self.escaped {
+ self.escaped = false;
+ } else if byte == b'\\' {
+ self.escaped = true;
+ } else if byte == b'"' {
+ self.inside_string = false;
+ }
+ continue;
+ }
+
+ if byte == b'"' {
+ self.inside_string = true;
+ self.null_progress = 0;
+ continue;
+ }
+
+ let expected = b"null";
+ if byte == expected[self.null_progress] {
+ self.null_progress += 1;
+ if self.null_progress == expected.len() {
+ self.rejected = true;
+ return Err(std::io::Error::other("JSON null is forbidden"));
+ }
+ } else {
+ self.null_progress = usize::from(byte == b'n');
+ }
+ }
+ Ok(bytes.len())
+ }
+
+ fn flush(&mut self) -> std::io::Result<()> {
+ if self.rejected {
+ Err(std::io::Error::other("JSON null is forbidden"))
+ } else {
+ Ok(())
+ }
+ }
+}
+
+fn checked_non_null(value: &impl Serialize) -> Result<(), AdminPayloadError> {
+ let failure = core::cell::Cell::new(None);
+ let serializer = NonNullSerializer { failure: &failure };
+ let result = value.serialize(serializer);
+ match failure.get().or_else(|| result.err()) {
+ None => {
+ let mut writer = NonNullJsonWriter::default();
+ match serde_json::to_writer(&mut writer, value) {
+ Ok(()) => Ok(()),
+ Err(_) if writer.rejected => Err(AdminPayloadError::NullForbidden),
+ Err(_) => Err(AdminPayloadError::Encoding),
+ }
+ }
+ Some(PayloadValidationError::Null) => Err(AdminPayloadError::NullForbidden),
+ Some(PayloadValidationError::Encoding) => Err(AdminPayloadError::Encoding),
}
}
@@ -1728,6 +2148,48 @@ mod tests {
);
}
+ struct IgnoredNestedNull;
+
+ impl Serialize for IgnoredNestedNull {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut structure = serializer.serialize_struct("IgnoredNestedNull", 1)?;
+ let _ = ser::SerializeStruct::serialize_field(
+ &mut structure,
+ "ignored",
+ &Option::<u8>::None,
+ );
+ ser::SerializeStruct::end(structure)
+ }
+ }
+
+ #[test]
+ fn construction_rejects_a_nested_null_even_when_custom_serialization_ignores_the_error() {
+ assert!(matches!(
+ AdminSuccessResponse::new(correlation_id(), IgnoredNestedNull),
+ Err(AdminPayloadError::NullForbidden)
+ ));
+
+ let raw = serde_json::value::RawValue::from_string("null".to_owned()).unwrap();
+ assert!(matches!(
+ AdminSuccessResponse::new(correlation_id(), raw),
+ Err(AdminPayloadError::NullForbidden)
+ ));
+
+ let raw = serde_json::value::RawValue::from_string(
+ r#"{"null":"escaped \"null\" text"}"#.to_owned(),
+ )
+ .unwrap();
+ assert!(AdminSuccessResponse::new(correlation_id(), raw).is_ok());
+
+ let mut writer = NonNullJsonWriter::default();
+ assert_eq!(std::io::Write::write(&mut writer, b"nu").unwrap(), 2);
+ assert!(std::io::Write::write(&mut writer, b"ll").is_err());
+ assert!(std::io::Write::write(&mut writer, b"true").is_err());
+ }
+
#[test]
fn ordinary_debug_redacts_request_and_result_payloads() {
let request = AdminMutationRequest::new(
diff --git a/crates/service_host/src/admin/server.rs b/crates/service_host/src/admin/server.rs
@@ -2,7 +2,7 @@
use core::fmt;
use serde::{Deserialize, Deserializer, Serialize, de, de::DeserializeOwned};
-use serde_json::{Value, value::RawValue};
+use serde_json::value::RawValue;
use std::collections::{BTreeMap, BTreeSet};
use std::convert::Infallible;
use std::error::Error;
@@ -315,7 +315,7 @@ impl AdminRequest {
BoundedEncodingError::Limit => AdminRouteOutcomeError::ResponseLimit,
BoundedEncodingError::Encoding => AdminRouteOutcomeError::Encoding,
})?;
- let _: StrictJsonValue =
+ let _: StrictJsonPayload =
serde_json::from_slice(&encoded).map_err(|_| AdminRouteOutcomeError::InvalidPayload)?;
let encoded =
String::from_utf8(encoded).expect("serde_json output must always be valid UTF-8");
@@ -440,18 +440,19 @@ struct ServerSuccessEnvelope<'a> {
result: &'a RawValue,
}
-struct StrictJsonValue(Value);
+#[derive(Clone, Copy)]
+struct StrictJsonPayload;
-impl Serialize for StrictJsonValue {
+impl Serialize for StrictJsonPayload {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
- self.0.serialize(serializer)
+ serializer.serialize_bool(true)
}
}
-impl<'de> Deserialize<'de> for StrictJsonValue {
+impl<'de> Deserialize<'de> for StrictJsonPayload {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
@@ -463,36 +464,37 @@ impl<'de> Deserialize<'de> for StrictJsonValue {
struct StrictJsonVisitor;
impl<'de> de::Visitor<'de> for StrictJsonVisitor {
- type Value = StrictJsonValue;
+ type Value = StrictJsonPayload;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("JSON without duplicate object keys or null values")
}
- fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
- Ok(StrictJsonValue(Value::Bool(value)))
+ fn visit_bool<E>(self, _value: bool) -> Result<Self::Value, E> {
+ Ok(StrictJsonPayload)
}
- fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
- Ok(StrictJsonValue(Value::Number(value.into())))
+ fn visit_i64<E>(self, _value: i64) -> Result<Self::Value, E> {
+ Ok(StrictJsonPayload)
}
- fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
- Ok(StrictJsonValue(Value::Number(value.into())))
+ fn visit_u64<E>(self, _value: u64) -> Result<Self::Value, E> {
+ Ok(StrictJsonPayload)
}
fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
where
E: de::Error,
{
- serde_json::Number::from_f64(value)
- .map(Value::Number)
- .map(StrictJsonValue)
- .ok_or_else(|| E::custom("non-finite JSON number"))
+ if value.is_finite() {
+ Ok(StrictJsonPayload)
+ } else {
+ Err(E::custom("non-finite JSON number"))
+ }
}
- fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> {
- Ok(StrictJsonValue(Value::String(value.to_owned())))
+ fn visit_str<E>(self, _value: &str) -> Result<Self::Value, E> {
+ Ok(StrictJsonPayload)
}
fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E>
@@ -502,8 +504,8 @@ impl<'de> de::Visitor<'de> for StrictJsonVisitor {
self.visit_str(value)
}
- fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
- Ok(StrictJsonValue(Value::String(value)))
+ fn visit_string<E>(self, _value: String) -> Result<Self::Value, E> {
+ Ok(StrictJsonPayload)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
@@ -524,26 +526,22 @@ impl<'de> de::Visitor<'de> for StrictJsonVisitor {
where
A: de::SeqAccess<'de>,
{
- let mut values = Vec::with_capacity(sequence.size_hint().unwrap_or(0));
- while let Some(value) = sequence.next_element::<StrictJsonValue>()? {
- values.push(value.0);
- }
- Ok(StrictJsonValue(Value::Array(values)))
+ while sequence.next_element::<StrictJsonPayload>()?.is_some() {}
+ Ok(StrictJsonPayload)
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
- let mut values = serde_json::Map::with_capacity(map.size_hint().unwrap_or(0));
+ let mut keys = BTreeSet::new();
while let Some(key) = map.next_key::<String>()? {
- if values.contains_key(&key) {
+ if !keys.insert(key) {
return Err(de::Error::custom("duplicate JSON object key"));
}
- let value = map.next_value::<StrictJsonValue>()?;
- values.insert(key, value.0);
+ map.next_value::<StrictJsonPayload>()?;
}
- Ok(StrictJsonValue(Value::Object(values)))
+ Ok(StrictJsonPayload)
}
}
@@ -984,18 +982,18 @@ async fn process_request(
}
if method == AdminHttpMethod::Post {
- let envelope = match serde_json::from_slice::<AdminMutationRequest<StrictJsonValue>>(&body)
- {
- Ok(envelope) => envelope,
- Err(_) => {
- return failure_response(
- StatusCode::BAD_REQUEST,
- correlation.current(),
- known_error("malformed_json", "admin request body is not valid JSON"),
- state.limits,
- );
- }
- };
+ let envelope =
+ match serde_json::from_slice::<AdminMutationRequest<StrictJsonPayload>>(&body) {
+ Ok(envelope) => envelope,
+ Err(_) => {
+ return failure_response(
+ StatusCode::BAD_REQUEST,
+ correlation.current(),
+ known_error("malformed_json", "admin request body is not valid JSON"),
+ state.limits,
+ );
+ }
+ };
if let Some(caller_correlation) = envelope.correlation_id().cloned() {
correlation.replace(caller_correlation);
} else if let Some(error) = deferred_entropy_failure {
@@ -2094,25 +2092,19 @@ mod tests {
headers.insert(CONTENT_TYPE, HeaderValue::from_static("text/plain"));
assert!(!is_json_content_type(&headers));
- for (document, expected) in [
- ("true", Value::Bool(true)),
- ("-7", Value::Number((-7).into())),
- ("9", Value::Number(9_u64.into())),
- (
- "1.5",
- Value::Number(serde_json::Number::from_f64(1.5).unwrap()),
- ),
- (r#""text""#, Value::String("text".to_owned())),
- ("[true,2]", serde_json::json!([true, 2])),
- (r#"{"value":3}"#, serde_json::json!({"value": 3})),
+ for document in [
+ "true",
+ "-7",
+ "9",
+ "1.5",
+ r#""text""#,
+ "[true,2]",
+ r#"{"value":3}"#,
] {
- assert_eq!(
- serde_json::from_str::<StrictJsonValue>(document).unwrap().0,
- expected
- );
+ serde_json::from_str::<StrictJsonPayload>(document).unwrap();
}
for rejected in ["null", "[1,null]", r#"{"same":1,"same":2}"#] {
- assert!(serde_json::from_str::<StrictJsonValue>(rejected).is_err());
+ assert!(serde_json::from_str::<StrictJsonPayload>(rejected).is_err());
}
let route = AdminRoutePath::new("/v1/items/{item_id}").unwrap();
diff --git a/crates/service_host/src/status/reason.rs b/crates/service_host/src/status/reason.rs
@@ -115,15 +115,18 @@ impl ReasonCodes {
}
pub fn new(values: impl IntoIterator<Item = ReasonCode>) -> Result<Self, StatusContractError> {
- let mut values: Vec<_> = values.into_iter().collect();
- values.sort_unstable();
- values.dedup();
- if values.len() > REASON_CODES_MAX_ITEMS {
- return Err(StatusContractError::TooManyReasonCodes {
- maximum: REASON_CODES_MAX_ITEMS,
- });
+ let mut bounded = Vec::with_capacity(REASON_CODES_MAX_ITEMS);
+ for value in values.into_iter().take(REASON_CODES_MAX_ITEMS + 1) {
+ if bounded.len() == REASON_CODES_MAX_ITEMS {
+ return Err(StatusContractError::TooManyReasonCodes {
+ maximum: REASON_CODES_MAX_ITEMS,
+ });
+ }
+ bounded.push(value);
}
- Ok(Self(values))
+ bounded.sort_unstable();
+ bounded.dedup();
+ Ok(Self(bounded))
}
#[must_use]
@@ -142,13 +145,43 @@ impl<'de> Deserialize<'de> for ReasonCodes {
where
D: Deserializer<'de>,
{
- let raw = Vec::<ReasonCode>::deserialize(deserializer)?;
- if raw.windows(2).any(|pair| pair[0] >= pair[1]) {
- return Err(serde::de::Error::custom(
- "reason codes must be unique and canonically sorted",
- ));
+ struct ReasonCodesVisitor;
+
+ impl<'de> serde::de::Visitor<'de> for ReasonCodesVisitor {
+ type Value = ReasonCodes;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("a bounded canonically sorted reason-code array")
+ }
+
+ fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
+ where
+ A: serde::de::SeqAccess<'de>,
+ {
+ let mut values = Vec::with_capacity(
+ sequence
+ .size_hint()
+ .unwrap_or(0)
+ .min(REASON_CODES_MAX_ITEMS),
+ );
+ while let Some(value) = sequence.next_element::<ReasonCode>()? {
+ if values.len() == REASON_CODES_MAX_ITEMS {
+ return Err(serde::de::Error::custom(
+ "reason-code collection exceeds its item limit",
+ ));
+ }
+ if values.last().is_some_and(|previous| previous >= &value) {
+ return Err(serde::de::Error::custom(
+ "reason codes must be unique and canonically sorted",
+ ));
+ }
+ values.push(value);
+ }
+ Ok(ReasonCodes(values))
+ }
}
- Self::new(raw).map_err(serde::de::Error::custom)
+
+ deserializer.deserialize_seq(ReasonCodesVisitor)
}
}
@@ -227,4 +260,54 @@ mod tests {
assert!(serde_json::from_str::<ReasonCodes>(r#"["z_reason","a_reason"]"#).is_err());
assert!(serde_json::from_str::<ReasonCodes>(r#"["a_reason","a_reason"]"#).is_err());
}
+
+ #[test]
+ fn collection_ingestion_stops_at_maximum_plus_one() {
+ use core::cell::Cell;
+
+ struct CountedInfinite<'a> {
+ calls: &'a Cell<usize>,
+ value: ReasonCode,
+ }
+
+ impl Iterator for CountedInfinite<'_> {
+ type Item = ReasonCode;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.calls.set(self.calls.get() + 1);
+ Some(self.value.clone())
+ }
+ }
+
+ let calls = Cell::new(0);
+ assert_eq!(
+ ReasonCodes::new(CountedInfinite {
+ calls: &calls,
+ value: ReasonCode::new("same_reason").unwrap(),
+ }),
+ Err(StatusContractError::TooManyReasonCodes {
+ maximum: REASON_CODES_MAX_ITEMS,
+ })
+ );
+ assert_eq!(calls.get(), REASON_CODES_MAX_ITEMS + 1);
+
+ let maximum = (0..REASON_CODES_MAX_ITEMS)
+ .map(|index| format!("reason_{index:02}"))
+ .collect::<Vec<_>>();
+ let maximum_json = serde_json::to_string(&maximum).unwrap();
+ assert_eq!(
+ serde_json::from_str::<ReasonCodes>(&maximum_json)
+ .unwrap()
+ .as_slice()
+ .len(),
+ REASON_CODES_MAX_ITEMS
+ );
+ let over_maximum = (0..=REASON_CODES_MAX_ITEMS)
+ .map(|index| format!("reason_{index:02}"))
+ .collect::<Vec<_>>();
+ assert!(
+ serde_json::from_str::<ReasonCodes>(&serde_json::to_string(&over_maximum).unwrap())
+ .is_err()
+ );
+ }
}
diff --git a/crates/service_host/tests/package_boundary.rs b/crates/service_host/tests/package_boundary.rs
@@ -91,6 +91,26 @@ fn service_host_is_unpublished_lint_governed_and_dependency_bounded() {
assert!(ROOT.contains("pub use radroots_runtime_paths::{InstanceId, ServiceId};"));
assert!(!STATUS_SOURCE.contains("serde(untagged)"));
assert!(!ADMIN_SOURCE.contains("serde(untagged)"));
+ for required in [
+ ".take(REASON_CODES_MAX_ITEMS + 1)",
+ "deserialize_seq(ReasonCodesVisitor)",
+ ] {
+ assert!(STATUS_SOURCE.contains(required));
+ }
+ for required in [
+ "struct NonNullSerializer",
+ "struct StrictJsonPayload",
+ "encode_bounded(result, self.response_body_limit)",
+ ] {
+ assert!(ADMIN_SOURCE.contains(required));
+ }
+ for forbidden in [
+ "serde_json::Value",
+ "serde_json::to_value",
+ "StrictJsonValue",
+ ] {
+ assert!(!ADMIN_SOURCE.contains(forbidden));
+ }
for forbidden in ["tokio::signal", "ctrl_c", "signal_hook"] {
assert!(!LIFECYCLE_SOURCE.contains(forbidden));
}
@@ -129,6 +149,7 @@ fn documentation_and_reviewed_public_api_are_complete_and_dependency_safe() {
"BoundedCount::<64>::new(8)",
"cached_service_state(CachedServiceState::new",
"parent.child_token()",
+ "streamed directly into the capped response writer",
] {
assert!(README.contains(required), "README is missing `{required}`");
}