Skip to main content

corpus_ipc/models/
config.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use std::fmt;
4
5use serde::de::{
6    self, Deserializer, IntoDeserializer, MapAccess, SeqAccess, Visitor,
7    value::{MapAccessDeserializer, SeqAccessDeserializer},
8};
9use serde::{Deserialize, Serialize};
10
11use crate::validation::{
12    ProtocolLimits, Validate, ValidationError, add_to_total, check_count, check_finite,
13    check_finite_slice, check_opt_string, check_string,
14};
15
16use super::de::{
17    de_config_map, de_config_string, de_finite_f32, de_float_array, de_opt_session_id, prefix_path,
18};
19
20/// Configuration payload for runtime parameter updates.
21#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
22#[serde(try_from = "ConfigPayloadWire")]
23pub struct ConfigPayload {
24    /// Target session (None = global).
25    pub session_id: Option<String>,
26    /// Configuration key-value pairs.
27    pub config: std::collections::HashMap<String, ConfigValue>,
28}
29
30#[derive(Deserialize)]
31struct ConfigPayloadWire {
32    #[serde(deserialize_with = "de_opt_session_id")]
33    session_id: Option<String>,
34    #[serde(deserialize_with = "de_config_map")]
35    config: std::collections::HashMap<String, ConfigValue>,
36}
37
38impl TryFrom<ConfigPayloadWire> for ConfigPayload {
39    type Error = ValidationError;
40
41    fn try_from(wire: ConfigPayloadWire) -> Result<Self, Self::Error> {
42        let payload = ConfigPayload {
43            session_id: wire.session_id,
44            config: wire.config,
45        };
46        payload.validate()?;
47        Ok(payload)
48    }
49}
50
51impl Validate for ConfigPayload {
52    fn validate_with(&self, limits: ProtocolLimits) -> Result<(), ValidationError> {
53        check_opt_string("session_id", self.session_id.as_deref(), limits)?;
54        check_count("config", self.config.len(), limits.max_metadata_entries)?;
55        let mut total = 0;
56        add_to_total(&mut total, self.config.len(), limits, "aggregate")?;
57        validate_config_entries(&self.config, limits, &mut total)
58    }
59}
60
61fn validate_config_entries(
62    config: &std::collections::HashMap<String, ConfigValue>,
63    limits: ProtocolLimits,
64    total: &mut usize,
65) -> Result<(), ValidationError> {
66    for (key, value) in config {
67        if key.is_empty() {
68            return Err(ValidationError::nested_metadata("config.<empty>"));
69        }
70        check_string("config.<key>", key, limits)?;
71        value
72            .validate_with(limits)
73            .map_err(|err| prefix_path(err, &format!("config.{key}")))?;
74        if let ConfigValue::FloatArray(values) = value {
75            add_to_total(total, values.len(), limits, "aggregate")?;
76        }
77    }
78    Ok(())
79}
80
81/// Configuration value types.
82///
83/// Serialize uses `#[serde(untagged)]` so plain JSON numbers/strings/arrays/booleans
84/// are emitted inside `ConfigPayload::config`.
85///
86/// **JSON number behavior (intentional, pre-existing):**
87/// JSON numbers (e.g. `42` or `1.5`) always deserialize as `Float`. `Integer`
88/// is only reached for values that were originally `ConfigValue::Integer` in
89/// Rust (in-memory), not from JSON. Deserialize is a `deserialize_any`
90/// visitor rather than `#[serde(untagged)]` so JSON decimals still decode
91/// when a consumer unifies serde_json `arbitrary_precision` (that feature
92/// presents non-integer numbers to `deserialize_any` as a private Number
93/// map, which untagged + `deserialize_with = f32` does not accept). The
94/// `visit_map` arm deserializes the map into a `serde_json::Value` and
95/// preserves the JSON value type: only `Value::Number` is accepted (its
96/// decimal token is parsed directly to `f32`); any other value type -
97/// including a user object spoofing the magic key - is rejected. See the
98/// `visit_map` arm for the one residual, arbitrary_precision-only edge case
99/// that serde_json makes indistinguishable from a genuine number.
100///
101/// Round-tripping `Integer(42)` through JSON yields `Float(42.0)`.
102/// Large integers (> ~2^24) may lose precision in f32.
103/// Consumers relying on exact integer identity should be aware.
104#[derive(Serialize, Debug, Clone, PartialEq)]
105#[serde(untagged)]
106pub enum ConfigValue {
107    /// Floating-point value.
108    ///
109    /// JSON numbers (integers and floats) deserialize as `Float`.
110    Float(f32),
111
112    /// Integer value (u64).
113    ///
114    /// Typically only produced when a Rust `ConfigValue::Integer` is
115    /// constructed in memory. Plain JSON numbers land in `Float`.
116    Integer(u64),
117
118    /// String value.
119    ///
120    /// Allows string-valued config (e.g. mode names, paths) in `ConfigPayload::config`.
121    String(String),
122
123    /// Boolean value.
124    Boolean(bool),
125    FloatArray(Vec<f32>),
126}
127
128impl<'de> Deserialize<'de> for ConfigValue {
129    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
130        struct ConfigValueVisitor;
131
132        impl<'de> Visitor<'de> for ConfigValueVisitor {
133            type Value = ConfigValue;
134
135            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136                f.write_str("a JSON number, string, boolean, or array of finite floats")
137            }
138
139            fn visit_bool<E: de::Error>(self, value: bool) -> Result<Self::Value, E> {
140                Ok(ConfigValue::Boolean(value))
141            }
142
143            fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
144                de_finite_f32(value.into_deserializer()).map(ConfigValue::Float)
145            }
146
147            fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
148                de_finite_f32(value.into_deserializer()).map(ConfigValue::Float)
149            }
150
151            fn visit_f32<E: de::Error>(self, value: f32) -> Result<Self::Value, E> {
152                de_finite_f32(value.into_deserializer()).map(ConfigValue::Float)
153            }
154
155            fn visit_f64<E: de::Error>(self, value: f64) -> Result<Self::Value, E> {
156                de_finite_f32(value.into_deserializer()).map(ConfigValue::Float)
157            }
158
159            fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
160                de_config_string(value.into_deserializer()).map(ConfigValue::String)
161            }
162
163            fn visit_string<E: de::Error>(self, value: String) -> Result<Self::Value, E> {
164                de_config_string(value.into_deserializer()).map(ConfigValue::String)
165            }
166
167            fn visit_seq<A: SeqAccess<'de>>(self, seq: A) -> Result<Self::Value, A::Error> {
168                de_float_array(SeqAccessDeserializer::new(seq)).map(ConfigValue::FloatArray)
169            }
170
171            fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
172                // serde_json `arbitrary_precision` presents JSON floats to
173                // `deserialize_any` as a synthetic map keyed by the private token
174                // `$serde_json::private::Number`. Deserialize the map into a
175                // `serde_json::Value` first and preserve the JSON value *type*:
176                // serde_json's own key classifier turns the synthetic number map
177                // back into `Value::Number`, while an ordinary user object (e.g.
178                // `{"not":"a-number"}`) surfaces as `Value::Object`. Accept only
179                // `Value::Number`; reject every other variant (`Object`, `Array`,
180                // `String`, `Bool`, `Null`) with the crate's typed validation
181                // error. That rejects spoofed user objects in the default build,
182                // where the spoof reaches this arm as a genuine `Value::Object`.
183                //
184                // For a genuine number we parse its decimal token straight to
185                // `f32` rather than going through `as_f64()`; the intermediate
186                // `f64` widening double-rounds values near an `f32` midpoint and
187                // can pick the wrong neighbour.
188                //
189                // Known limitation (arbitrary_precision only): when a consumer
190                // unifies `serde_json/arbitrary_precision`, serde_json represents a
191                // real number and a user object literally keyed
192                // `$serde_json::private::Number` with a single string value as the
193                // exact same map, so the classifier collapses both into
194                // `Value::Number`. The exact-form spoof is therefore
195                // indistinguishable from a real number at this layer and cannot be
196                // rejected without also rejecting genuine decimals. Malformed
197                // spoofs (extra keys, non-string value, a different key) still
198                // surface as `Value::Object` and are rejected.
199                match serde_json::Value::deserialize(MapAccessDeserializer::new(map))? {
200                    serde_json::Value::Number(number) => {
201                        let Ok(parsed) = number.to_string().parse::<f32>() else {
202                            return Err(de::Error::custom(ValidationError::non_finite(
203                                "value",
204                                f32::NAN,
205                            )));
206                        };
207                        de_finite_f32(parsed.into_deserializer()).map(ConfigValue::Float)
208                    }
209                    _ => Err(de::Error::custom(ValidationError::nested_metadata("value"))),
210                }
211            }
212        }
213
214        deserializer.deserialize_any(ConfigValueVisitor)
215    }
216}
217
218impl Validate for ConfigValue {
219    fn validate_with(&self, limits: ProtocolLimits) -> Result<(), ValidationError> {
220        match self {
221            Self::Float(value) => check_finite("value", *value),
222            Self::Integer(_) | Self::Boolean(_) => Ok(()),
223            Self::String(value) => check_string("value", value, limits),
224            Self::FloatArray(values) => {
225                check_count("value", values.len(), limits.max_channel_values)?;
226                check_finite_slice("value", values)
227            }
228        }
229    }
230}