Skip to main content

gwk_kernel/wire/
strict.rs

1//! Strict JSON decoding: what `serde_json` will not refuse on its own.
2//!
3//! Three refusals, and only the second needs code here:
4//!
5//! * **Unknown fields** — `deny_unknown_fields` on every contract type. That
6//!   works because the field sets are closed, and it is why the four field-less
7//!   requests are empty STRUCT variants rather than unit ones.
8//! * **Duplicate keys, at every depth** — serde catches a duplicate of a field
9//!   it KNOWS (`{"limit": 1, "limit": 2}` on a typed struct is a
10//!   `duplicate field` error). It cannot catch one inside a
11//!   `serde_json::Value`: a command's inline payload is free-form, so
12//!   `{"cost": 1, "cost": 2}` three levels down silently keeps the last. That
13//!   is the hole this module closes, and it matters because the log stores the
14//!   payload verbatim — a reader replaying it would see a value the sender
15//!   never confirmed.
16//! * **Invalid UTF-8 and trailing bytes** — the body is bytes; both are refused
17//!   before parsing rather than lossily repaired.
18//!
19//! The cost is one extra pass. `parse` walks the bytes into a
20//! [`serde_json::Value`] rejecting duplicates as it goes, then the target type
21//! is deserialized from that value.
22//
23// ponytail: two passes over a ≤4 MiB body. A wrapping `Deserializer` that
24// tracked keys in one pass would avoid the intermediate Value; take that only
25// if the phase's latency budget says this shows up, because the two-pass
26// version is the one whose correctness is obvious by reading it.
27
28use gwk_domain::protocol::KernelErrorCode;
29use serde::Deserialize;
30use serde::de::{DeserializeOwned, MapAccess, SeqAccess, Visitor};
31
32use super::WireError;
33
34/// How a duplicate-key refusal announces itself through `serde_json::Error`,
35/// which carries no room for a code of our own.
36///
37/// The producer and the classifier both read this constant, so the one string
38/// the two of them agree on cannot drift; `a_duplicate_key_is_refused_at_every_depth`
39/// asserts the code, which is what fails if `serde_json` ever reworded around it.
40const DUPLICATE_KEY: &str = "duplicate object key";
41
42/// A `serde_json::Value` that refuses a duplicate key at ANY depth.
43struct NoDuplicateKeys(serde_json::Value);
44
45impl<'de> Deserialize<'de> for NoDuplicateKeys {
46    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
47        d.deserialize_any(StrictValue).map(NoDuplicateKeys)
48    }
49}
50
51struct StrictValue;
52
53impl<'de> Visitor<'de> for StrictValue {
54    type Value = serde_json::Value;
55
56    fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
57        f.write_str("a JSON value with no duplicate object key")
58    }
59
60    fn visit_unit<E>(self) -> Result<Self::Value, E> {
61        Ok(serde_json::Value::Null)
62    }
63
64    fn visit_none<E>(self) -> Result<Self::Value, E> {
65        Ok(serde_json::Value::Null)
66    }
67
68    fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E> {
69        Ok(serde_json::Value::Bool(v))
70    }
71
72    fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E> {
73        Ok(serde_json::Value::Number(v.into()))
74    }
75
76    fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> {
77        Ok(serde_json::Value::Number(v.into()))
78    }
79
80    fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<Self::Value, E> {
81        serde_json::Number::from_f64(v)
82            .map(serde_json::Value::Number)
83            .ok_or_else(|| E::custom("a non-finite number is not JSON"))
84    }
85
86    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> {
87        Ok(serde_json::Value::String(v.to_owned()))
88    }
89
90    fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
91        let mut out = Vec::new();
92        while let Some(NoDuplicateKeys(value)) = seq.next_element()? {
93            out.push(value);
94        }
95        Ok(serde_json::Value::Array(out))
96    }
97
98    fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
99        let mut out = serde_json::Map::new();
100        while let Some(key) = map.next_key::<String>()? {
101            let NoDuplicateKeys(value) = map.next_value()?;
102            // The check is on INSERT, so it fires at whatever depth the
103            // duplicate lives — the recursion is the visitor's, not a separate
104            // walk that could disagree with it.
105            if out.insert(key.clone(), value).is_some() {
106                return Err(serde::de::Error::custom(format!("{DUPLICATE_KEY} {key:?}")));
107            }
108        }
109        Ok(serde_json::Value::Object(out))
110    }
111}
112
113/// Decode one frame body into `T`, refusing everything ADR 0001 requires.
114///
115/// The error code says which wall was hit: [`KernelErrorCode::Schema`] for
116/// bytes that are not the shape the contract declares, and
117/// [`KernelErrorCode::Validation`] for a value the contract's own types refuse.
118/// Both are values a client can branch on, never a dropped connection.
119pub fn decode<T: DeserializeOwned>(body: &[u8]) -> Result<T, WireError> {
120    let text = std::str::from_utf8(body).map_err(|e| {
121        WireError::new(
122            KernelErrorCode::Schema,
123            format!("frame body is not UTF-8: {e}"),
124        )
125    })?;
126
127    // Pass one: structure, with duplicate keys refused at every depth. A
128    // duplicate gets its own contract code rather than the generic schema one —
129    // the contract lists `duplicate_key` separately because a client retrying a
130    // malformed request and a client sending the same key twice are different
131    // bugs on the other end.
132    let mut stream = serde_json::Deserializer::from_str(text).into_iter::<NoDuplicateKeys>();
133    let NoDuplicateKeys(value) = stream
134        .next()
135        .transpose()
136        .map_err(|e| {
137            let text = e.to_string();
138            let code = if text.starts_with(DUPLICATE_KEY) {
139                KernelErrorCode::DuplicateKey
140            } else {
141                KernelErrorCode::Schema
142            };
143            WireError::new(code, format!("frame body: {text}"))
144        })?
145        .ok_or_else(|| WireError::new(KernelErrorCode::Schema, "frame body holds no JSON value"))?;
146    // A second value after the first is trailing garbage, not a second frame.
147    // `byte_offset` is past the first value, so anything but whitespace left
148    // means the sender framed two things as one.
149    let rest = text[stream.byte_offset()..].trim();
150    if !rest.is_empty() {
151        return Err(WireError::new(
152            KernelErrorCode::Schema,
153            format!("{} trailing bytes after the JSON value", rest.len()),
154        ));
155    }
156
157    // Pass two: the contract's own types, where `deny_unknown_fields` and the
158    // self-validating newtypes do the rest.
159    serde_json::from_value(value)
160        .map_err(|e| WireError::new(KernelErrorCode::Validation, format!("frame body: {e}")))
161}
162
163#[cfg(test)]
164mod tests {
165    use gwk_domain::protocol::{ClientControl, KernelRequest};
166
167    use super::*;
168
169    fn control(raw: &str) -> Result<ClientControl, WireError> {
170        decode::<ClientControl>(raw.as_bytes())
171    }
172
173    #[test]
174    fn a_well_formed_request_decodes() {
175        let ok = control(r#"{"type":"request","request_id":"r-1","request":{"type":"health"}}"#)
176            .expect("decode");
177        match ok {
178            ClientControl::Request { request, .. } => {
179                assert_eq!(request, KernelRequest::Health {});
180            }
181            other => panic!("{other:?}"),
182        }
183    }
184
185    #[test]
186    fn a_field_less_request_still_refuses_an_extra_field() {
187        // The reason `Health` is `Health {}` and not `Health`. As a unit
188        // variant serde would accept this and answer as though the caller had
189        // asked for plain health — silently ignoring what they said.
190        let error = control(
191            r#"{"type":"request","request_id":"r-1","request":{"type":"health","limit":500}}"#,
192        )
193        .expect_err("extra field accepted");
194        assert_eq!(error.code, KernelErrorCode::Validation);
195        assert!(error.message.contains("limit"), "{error}");
196    }
197
198    #[test]
199    fn an_unknown_field_on_a_known_request_is_refused() {
200        let error = control(
201            r#"{"type":"request","request_id":"r-1","request":{"type":"read_events","limit":5,"depth":2}}"#,
202        )
203        .expect_err("unknown field accepted");
204        assert_eq!(error.code, KernelErrorCode::Validation);
205        assert!(error.message.contains("depth"), "{error}");
206    }
207
208    #[test]
209    fn a_duplicate_key_is_refused_at_every_depth() {
210        // Top level, and nested inside a free-form payload three levels down —
211        // the second is the one serde cannot see, because a `Value` has no
212        // field set to compare against.
213        for raw in [
214            r#"{"type":"request","type":"request","request_id":"r-1","request":{"type":"health"}}"#,
215            r#"{"type":"request","request_id":"r-1","request":{"type":"submit_command","envelope":{"command_id":"c","project_id":"p","command_type":"ingest_record","schema_version":1,"issued_at":"t","actor":{"kind":"kernel"},"origin":{"system":"gw"},"idempotency_key":"k","payload":{"deep":{"cost":1,"cost":2}}}}}"#,
216        ] {
217            let error = control(raw).expect_err("duplicate key accepted");
218            assert_eq!(error.code, KernelErrorCode::DuplicateKey);
219            assert!(error.message.contains(DUPLICATE_KEY), "{error}");
220        }
221    }
222
223    #[test]
224    fn a_legitimate_repeat_in_two_different_objects_is_not_a_duplicate() {
225        // The check is per-object, not per-name: two array elements each having
226        // a `kind` key is ordinary JSON, and refusing it would be a bug that
227        // only shows up on real payloads.
228        control(
229            r#"{"type":"request","request_id":"r-1","request":{"type":"submit_command","envelope":{"command_id":"c","project_id":"p","command_type":"ingest_record","schema_version":1,"issued_at":"t","actor":{"kind":"kernel"},"origin":{"system":"gw"},"idempotency_key":"k","payload":{"rows":[{"kind":"a"},{"kind":"b"}]}}}}"#,
230        )
231        .expect("a repeated name in sibling objects was refused");
232    }
233
234    #[test]
235    fn bytes_that_are_not_utf8_are_refused_before_parsing() {
236        let error = decode::<ClientControl>(&[0x7b, 0xff, 0x7d]).expect_err("invalid UTF-8");
237        assert_eq!(error.code, KernelErrorCode::Schema);
238        assert!(error.message.contains("UTF-8"), "{error}");
239    }
240
241    #[test]
242    fn a_second_value_in_one_frame_is_trailing_garbage() {
243        let error = control(
244            r#"{"type":"request","request_id":"r-1","request":{"type":"health"}}{"type":"request","request_id":"r-2","request":{"type":"status"}}"#,
245        )
246        .expect_err("two values in one frame accepted");
247        assert_eq!(error.code, KernelErrorCode::Schema);
248        assert!(error.message.contains("trailing"), "{error}");
249    }
250
251    #[test]
252    fn an_empty_body_is_a_refusal_and_not_a_default() {
253        let error = decode::<ClientControl>(b"  ").expect_err("empty body accepted");
254        assert_eq!(error.code, KernelErrorCode::Schema);
255    }
256}