Skip to main content

netget/server/grpc/
value_codec.rs

1//! Bounded conversion of the field-name JSON representation used by both gRPC peers.
2//!
3//! These bounds also apply to values constructed in memory, independently of
4//! serde/prost parser recursion limits. All inputs are borrowed; rejected input
5//! remains owned by its caller.
6
7use anyhow::{bail, Context, Result};
8use base64::{engine::general_purpose::STANDARD, Engine as _};
9use prost_reflect::{DynamicMessage, FieldDescriptor, Kind, MapKey, MessageDescriptor, Value};
10use serde_json::Value as Json;
11
12/// Root depth is zero; every message field, map value, or list element adds one.
13pub const MAX_VALUE_DEPTH: usize = 32;
14pub const MAX_VALUE_NODES: usize = 100_000;
15/// Retained-content accounting, including node/key overhead; not serialized wire size.
16pub const MAX_VALUE_BYTES: usize = 8 * 1024 * 1024;
17pub const VALUE_NODE_OVERHEAD_BYTES: usize = 64;
18const MAP_KEY_OVERHEAD_BYTES: usize = 32;
19
20/// A resource refusal, distinct from a value that does not match the schema.
21#[derive(Debug)]
22pub struct ValueLimitExceeded {
23    pub dimension: &'static str,
24    pub limit: usize,
25}
26impl std::fmt::Display for ValueLimitExceeded {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        write!(
29            f,
30            "gRPC value exceeds {} limit {}",
31            self.dimension, self.limit
32        )
33    }
34}
35impl std::error::Error for ValueLimitExceeded {}
36
37#[derive(Default)]
38struct Budget {
39    nodes: usize,
40    bytes: usize,
41}
42impl Budget {
43    fn node(&mut self, depth: usize) -> Result<()> {
44        if depth > MAX_VALUE_DEPTH {
45            return Err(ValueLimitExceeded {
46                dimension: "depth",
47                limit: MAX_VALUE_DEPTH,
48            }
49            .into());
50        }
51        self.children(1)?;
52        self.nodes += 1;
53        self.bytes(VALUE_NODE_OVERHEAD_BYTES)
54    }
55
56    fn children(&self, count: usize) -> Result<()> {
57        if count > MAX_VALUE_NODES.saturating_sub(self.nodes) {
58            return Err(ValueLimitExceeded {
59                dimension: "nodes",
60                limit: MAX_VALUE_NODES,
61            }
62            .into());
63        }
64        Ok(())
65    }
66
67    fn bytes(&mut self, count: usize) -> Result<()> {
68        if count > MAX_VALUE_BYTES.saturating_sub(self.bytes) {
69            return Err(ValueLimitExceeded {
70                dimension: "bytes",
71                limit: MAX_VALUE_BYTES,
72            }
73            .into());
74        }
75        self.bytes += count;
76        Ok(())
77    }
78
79    fn key(&mut self, key: &str) -> Result<()> {
80        self.bytes(MAP_KEY_OVERHEAD_BYTES)?;
81        self.bytes(key.len())
82    }
83}
84
85/// Convert one complete message, refusing invalid values rather than substituting defaults.
86pub fn json_to_dynamic_message(
87    json: &Json,
88    descriptor: &MessageDescriptor,
89) -> Result<DynamicMessage> {
90    json_to_message(json, descriptor, 0, &mut Budget::default())
91}
92
93/// Convert the complete message without relying on the protobuf decoder's recursion cap.
94pub fn dynamic_message_to_json(message: &DynamicMessage) -> Result<Json> {
95    message_to_json(message, 0, &mut Budget::default())
96}
97
98/// Convert a reflected value with the same limits used for complete messages.
99pub fn proto_value_to_json(value: &Value) -> Result<Json> {
100    value_to_json(value, 0, &mut Budget::default())
101}
102
103fn json_to_message(
104    json: &Json,
105    descriptor: &MessageDescriptor,
106    depth: usize,
107    budget: &mut Budget,
108) -> Result<DynamicMessage> {
109    budget.node(depth)?;
110    let object = json
111        .as_object()
112        .with_context(|| format!("message {} requires a JSON object", descriptor.full_name()))?;
113    budget.children(object.len())?;
114    let mut message = DynamicMessage::new(descriptor.clone());
115    let mut oneofs = std::collections::HashSet::new();
116    for (name, value) in object {
117        budget.key(name)?;
118        let field = descriptor.get_field_by_name(name).with_context(|| {
119            format!(
120                "unknown field {name:?} in message {}",
121                descriptor.full_name()
122            )
123        })?;
124        if let Some(oneof) = field.containing_oneof() {
125            if !oneofs.insert(oneof.full_name().to_owned()) {
126                bail!("multiple values supplied for oneof {}", oneof.full_name());
127            }
128        }
129        let value = json_to_field_value(value, &field, depth + 1, budget)?;
130        message
131            .try_set_field(&field, value)
132            .with_context(|| format!("invalid value for field {}", field.full_name()))?;
133    }
134    Ok(message)
135}
136
137fn json_to_field_value(
138    json: &Json,
139    field: &FieldDescriptor,
140    depth: usize,
141    budget: &mut Budget,
142) -> Result<Value> {
143    if field.is_map() {
144        budget.node(depth)?;
145        let Kind::Message(entry) = field.kind() else {
146            bail!("map field has no entry message")
147        };
148        let key_field = entry.get_field(1).context("map entry has no key field")?;
149        let value_field = entry.get_field(2).context("map entry has no value field")?;
150        let object = json
151            .as_object()
152            .with_context(|| format!("field {} requires a map object", field.full_name()))?;
153        budget.children(object.len())?;
154        let mut map = std::collections::HashMap::new();
155        for (key, value) in object {
156            budget.key(key)?;
157            let key = parse_map_key(key, key_field.kind())?;
158            if map.contains_key(&key) {
159                bail!("duplicate protobuf map key in field {}", field.full_name());
160            }
161            map.insert(
162                key,
163                json_to_proto_value(value, &value_field, depth + 1, budget)?,
164            );
165        }
166        return Ok(Value::Map(map));
167    }
168    if field.is_list() {
169        budget.node(depth)?;
170        let array = json
171            .as_array()
172            .with_context(|| format!("field {} requires an array", field.full_name()))?;
173        budget.children(array.len())?;
174        let mut list = Vec::new();
175        for item in array {
176            list.push(json_to_proto_value(item, field, depth + 1, budget)?);
177        }
178        return Ok(Value::List(list));
179    }
180    json_to_proto_value(json, field, depth, budget)
181}
182
183fn json_to_proto_value(
184    json: &Json,
185    field: &FieldDescriptor,
186    depth: usize,
187    budget: &mut Budget,
188) -> Result<Value> {
189    let kind = field.kind();
190    if let Kind::Message(descriptor) = kind {
191        return Ok(Value::Message(json_to_message(
192            json,
193            &descriptor,
194            depth,
195            budget,
196        )?));
197    }
198    budget.node(depth)?;
199    let invalid = || {
200        format!(
201            "value does not match field {} ({:?})",
202            field.full_name(),
203            field.kind()
204        )
205    };
206    Ok(match kind {
207        Kind::Bool => Value::Bool(json.as_bool().with_context(invalid)?),
208        Kind::Int32 | Kind::Sint32 | Kind::Sfixed32 => Value::I32(
209            i32::try_from(json.as_i64().with_context(invalid)?)
210                .context("integer does not fit int32")?,
211        ),
212        Kind::Int64 | Kind::Sint64 | Kind::Sfixed64 => {
213            Value::I64(json.as_i64().with_context(invalid)?)
214        }
215        Kind::Uint32 | Kind::Fixed32 => Value::U32(
216            u32::try_from(json.as_u64().with_context(invalid)?)
217                .context("integer does not fit uint32")?,
218        ),
219        Kind::Uint64 | Kind::Fixed64 => Value::U64(json.as_u64().with_context(invalid)?),
220        Kind::Float => {
221            let value = json.as_f64().with_context(invalid)?;
222            if !value.is_finite() || value.abs() > f32::MAX as f64 {
223                bail!(
224                    "field {} is outside the finite float range",
225                    field.full_name()
226                );
227            }
228            let narrowed = value as f32;
229            if value != 0.0 && narrowed == 0.0 {
230                bail!(
231                    "field {} underflows the nonzero float range",
232                    field.full_name()
233                );
234            }
235            Value::F32(narrowed)
236        }
237        Kind::Double => {
238            let value = json.as_f64().with_context(invalid)?;
239            if !value.is_finite() {
240                bail!("field {} requires a finite number", field.full_name());
241            }
242            Value::F64(value)
243        }
244        Kind::String => {
245            let value = json.as_str().with_context(invalid)?;
246            budget.bytes(value.len())?;
247            Value::String(value.to_owned())
248        }
249        Kind::Bytes => {
250            let value = json.as_str().with_context(invalid)?;
251            // Base64 input is at least as large as decoded output. Charge it before decoding.
252            budget.bytes(value.len())?;
253            Value::Bytes(
254                STANDARD
255                    .decode(value)
256                    .context("invalid protobuf bytes base64")?
257                    .into(),
258            )
259        }
260        Kind::Enum(descriptor) => {
261            let number = if let Some(number) = json.as_i64() {
262                i32::try_from(number).context("enum number does not fit int32")?
263            } else if let Some(name) = json.as_str() {
264                budget.bytes(name.len())?;
265                descriptor
266                    .get_value_by_name(name)
267                    .with_context(|| {
268                        format!("unknown value {name:?} for enum {}", descriptor.full_name())
269                    })?
270                    .number()
271            } else {
272                bail!("enum {} requires a name or integer", descriptor.full_name())
273            };
274            if descriptor.get_value(number).is_none() {
275                bail!("unknown value {number} for enum {}", descriptor.full_name());
276            }
277            Value::EnumNumber(number)
278        }
279        Kind::Message(_) => unreachable!("messages handled before charging scalar nodes"),
280    })
281}
282
283fn parse_map_key(key: &str, kind: Kind) -> Result<MapKey> {
284    Ok(match kind {
285        Kind::String => MapKey::String(key.to_owned()),
286        Kind::Bool => MapKey::Bool(key.parse().context("map key is not a boolean")?),
287        Kind::Int32 | Kind::Sint32 | Kind::Sfixed32 => {
288            MapKey::I32(key.parse().context("map key is not int32")?)
289        }
290        Kind::Int64 | Kind::Sint64 | Kind::Sfixed64 => {
291            MapKey::I64(key.parse().context("map key is not int64")?)
292        }
293        Kind::Uint32 | Kind::Fixed32 => MapKey::U32(key.parse().context("map key is not uint32")?),
294        Kind::Uint64 | Kind::Fixed64 => MapKey::U64(key.parse().context("map key is not uint64")?),
295        _ => bail!("unsupported protobuf map key kind"),
296    })
297}
298
299fn message_to_json(message: &DynamicMessage, depth: usize, budget: &mut Budget) -> Result<Json> {
300    budget.node(depth)?;
301    let mut object = serde_json::Map::new();
302    // Iterate populated values only, borrowing children instead of cloning their trees.
303    for (field, value) in message.fields() {
304        budget.key(field.name())?;
305        let json = value_to_json(value, depth + 1, budget)?;
306        object.insert(field.name().to_owned(), json);
307    }
308    Ok(Json::Object(object))
309}
310
311fn value_to_json(value: &Value, depth: usize, budget: &mut Budget) -> Result<Json> {
312    if let Value::Message(message) = value {
313        return message_to_json(message, depth, budget);
314    }
315    budget.node(depth)?;
316    Ok(match value {
317        Value::Bool(value) => Json::Bool(*value),
318        Value::I32(value) => Json::Number((*value).into()),
319        Value::I64(value) => Json::Number((*value).into()),
320        Value::U32(value) => Json::Number((*value).into()),
321        Value::U64(value) => Json::Number((*value).into()),
322        Value::F32(value) => Json::Number(
323            serde_json::Number::from_f64(*value as f64)
324                .context("non-finite protobuf float cannot be represented in field-name JSON")?,
325        ),
326        Value::F64(value) => Json::Number(
327            serde_json::Number::from_f64(*value)
328                .context("non-finite protobuf double cannot be represented in field-name JSON")?,
329        ),
330        Value::String(value) => {
331            budget.bytes(value.len())?;
332            Json::String(value.clone())
333        }
334        Value::Bytes(value) => {
335            let length = value
336                .len()
337                .checked_add(2)
338                .and_then(|n| (n / 3).checked_mul(4))
339                .ok_or(ValueLimitExceeded {
340                    dimension: "bytes",
341                    limit: MAX_VALUE_BYTES,
342                })?;
343            budget.bytes(length)?;
344            Json::String(STANDARD.encode(value))
345        }
346        Value::EnumNumber(value) => Json::Number((*value).into()),
347        Value::List(values) => {
348            budget.children(values.len())?;
349            let mut array = Vec::new();
350            for value in values {
351                array.push(value_to_json(value, depth + 1, budget)?);
352            }
353            Json::Array(array)
354        }
355        Value::Map(values) => {
356            budget.children(values.len())?;
357            let mut object = serde_json::Map::new();
358            for (key, value) in values {
359                // Charge string keys before cloning. Numeric keys have a fixed small bound.
360                if let MapKey::String(key) = key {
361                    budget.key(key)?;
362                }
363                let key_text = map_key_to_string(key);
364                if !matches!(key, MapKey::String(_)) {
365                    budget.key(&key_text)?;
366                }
367                if object.contains_key(&key_text) {
368                    bail!("distinct protobuf map keys have the same JSON representation");
369                }
370                let json = value_to_json(value, depth + 1, budget)?;
371                object.insert(key_text, json);
372            }
373            Json::Object(object)
374        }
375        Value::Message(_) => unreachable!("messages handled before charging scalar nodes"),
376    })
377}
378
379fn map_key_to_string(key: &MapKey) -> String {
380    match key {
381        MapKey::Bool(value) => value.to_string(),
382        MapKey::I32(value) => value.to_string(),
383        MapKey::I64(value) => value.to_string(),
384        MapKey::U32(value) => value.to_string(),
385        MapKey::U64(value) => value.to_string(),
386        MapKey::String(value) => value.clone(),
387    }
388}