Skip to main content

netget/server/gnmi/
value.rs

1//! Typed model boundary; binary protobuf and JSON bytes never enter an event.
2use super::{codec, proto::gnmi as pb};
3use serde::{Deserialize, Serialize};
4use std::{collections::BTreeMap, io::Write};
5use tonic::Status;
6
7#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
8#[serde(deny_unknown_fields)]
9pub struct Path {
10    #[serde(default)]
11    pub origin: String,
12    #[serde(default)]
13    pub target: String,
14    #[serde(default)]
15    pub elem: Vec<Element>,
16}
17#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
18#[serde(deny_unknown_fields)]
19pub struct Element {
20    pub name: String,
21    #[serde(default)]
22    pub key: BTreeMap<String, String>,
23}
24#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
25#[serde(
26    tag = "kind",
27    content = "value",
28    rename_all = "snake_case",
29    deny_unknown_fields
30)]
31pub enum Value {
32    String(String),
33    Int(String),
34    Uint(String),
35    Bool(bool),
36    Double(f64),
37    Decimal(Decimal),
38    Leaflist(Vec<Value>),
39    Json(serde_json::Value),
40    JsonIetf(serde_json::Value),
41    Ascii(String),
42}
43#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
44#[serde(deny_unknown_fields)]
45pub struct Decimal {
46    pub digits: String,
47    pub precision: u32,
48}
49#[derive(Clone, Debug, Serialize, Deserialize)]
50#[serde(deny_unknown_fields)]
51pub struct Update {
52    pub path: Path,
53    pub value: Value,
54    #[serde(default)]
55    pub duplicates: u32,
56}
57#[derive(Clone, Debug, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub struct Notification {
60    pub timestamp: String,
61    #[serde(default)]
62    pub prefix: Path,
63    #[serde(default)]
64    pub update: Vec<Update>,
65    #[serde(default)]
66    pub delete: Vec<Path>,
67    #[serde(default)]
68    pub atomic: bool,
69}
70fn limit(ok: bool) -> Result<(), Status> {
71    if ok {
72        Ok(())
73    } else {
74        Err(Status::resource_exhausted("gNMI typed value/path bound"))
75    }
76}
77fn text(value: &str, max: usize) -> Result<(), Status> {
78    limit(value.len() <= max)?;
79    if value.contains('\0') {
80        return Err(Status::invalid_argument("NUL in gNMI text"));
81    }
82    Ok(())
83}
84impl Path {
85    pub fn check(&self) -> Result<(), Status> {
86        text(&self.origin, 256)?;
87        text(&self.target, 256)?;
88        limit(self.elem.len() <= 32)?;
89        for elem in &self.elem {
90            text(&elem.name, 128)?;
91            if elem.name.is_empty() {
92                return Err(Status::invalid_argument("empty path element name"));
93            }
94            limit(elem.key.len() <= 8)?;
95            for (key, value) in &elem.key {
96                text(key, 128)?;
97                text(value, 1024)?;
98                if key.is_empty() {
99                    return Err(Status::invalid_argument("empty path key"));
100                }
101            }
102        }
103        Ok(())
104    }
105    pub fn into_proto(self) -> Result<pb::Path, Status> {
106        self.check()?;
107        Ok(pb::Path {
108            element: vec![],
109            origin: self.origin,
110            target: self.target,
111            elem: self
112                .elem
113                .into_iter()
114                .map(|e| pb::PathElem {
115                    name: e.name,
116                    key: e.key.into_iter().collect(),
117                })
118                .collect(),
119        })
120    }
121    pub fn from_proto(path: Option<pb::Path>) -> Result<Self, Status> {
122        let path = path.unwrap_or_default();
123        if !path.element.is_empty() {
124            return Err(Status::unimplemented(
125                "deprecated string paths are excluded; use Path.elem",
126            ));
127        }
128        let result = Self {
129            origin: path.origin,
130            target: path.target,
131            elem: path
132                .elem
133                .into_iter()
134                .map(|e| Element {
135                    name: e.name,
136                    key: e.key.into_iter().collect(),
137                })
138                .collect(),
139        };
140        result.check()?;
141        Ok(result)
142    }
143}
144struct Writer(Vec<u8>);
145impl Write for Writer {
146    fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
147        if self.0.len().saturating_add(bytes.len()) > codec::MAX_VALUE_BYTES {
148            return Err(std::io::Error::other("JSON bound"));
149        }
150        self.0.extend_from_slice(bytes);
151        Ok(bytes.len())
152    }
153    fn flush(&mut self) -> std::io::Result<()> {
154        Ok(())
155    }
156}
157pub fn json_bytes(value: &serde_json::Value) -> Result<Vec<u8>, Status> {
158    let mut writer = Writer(Vec::new());
159    serde_json::to_writer(&mut writer, value)
160        .map_err(|_| Status::resource_exhausted("gNMI JSON exceeds 64 KiB"))?;
161    super::json::parse(&writer.0)?;
162    Ok(writer.0)
163}
164impl Value {
165    pub fn into_proto(self) -> Result<pb::TypedValue, Status> {
166        use pb::typed_value::Value as V;
167        let value = match self {
168            Self::String(v) => {
169                limit(v.len() <= codec::MAX_VALUE_BYTES)?;
170                V::StringVal(v)
171            }
172            Self::Ascii(v) => {
173                limit(v.len() <= codec::MAX_VALUE_BYTES)?;
174                if !v.is_ascii() {
175                    return Err(Status::invalid_argument("ASCII value is not ASCII"));
176                }
177                V::AsciiVal(v)
178            }
179            Self::Int(v) => V::IntVal(v.parse().map_err(|_| {
180                Status::invalid_argument("int value must be an i64 decimal string")
181            })?),
182            Self::Uint(v) => V::UintVal(v.parse().map_err(|_| {
183                Status::invalid_argument("uint value must be a u64 decimal string")
184            })?),
185            Self::Bool(v) => V::BoolVal(v),
186            Self::Double(v) => {
187                if !v.is_finite() {
188                    return Err(Status::invalid_argument("nonfinite value"));
189                }
190                V::DoubleVal(v)
191            }
192            Self::Decimal(v) => {
193                if v.precision > 18 {
194                    return Err(Status::invalid_argument("decimal precision exceeds 18"));
195                }
196                V::DecimalVal(pb::Decimal64 {
197                    digits: v.digits.parse().map_err(|_| {
198                        Status::invalid_argument("decimal digits must be an i64 string")
199                    })?,
200                    precision: v.precision,
201                })
202            }
203            Self::Json(v) => V::JsonVal(json_bytes(&v)?),
204            Self::JsonIetf(v) => V::JsonIetfVal(json_bytes(&v)?),
205            Self::Leaflist(values) => {
206                limit(values.len() <= 64)?;
207                if values
208                    .iter()
209                    .any(|v| matches!(v, Self::Leaflist(_) | Self::Json(_) | Self::JsonIetf(_)))
210                {
211                    return Err(Status::invalid_argument(
212                        "leaf-list elements must be scalars",
213                    ));
214                }
215                V::LeaflistVal(pb::ScalarArray {
216                    element: values
217                        .into_iter()
218                        .map(Self::into_proto)
219                        .collect::<Result<_, _>>()?,
220                })
221            }
222        };
223        Ok(pb::TypedValue { value: Some(value) })
224    }
225    pub fn from_proto(value: pb::TypedValue) -> Result<Self, Status> {
226        use pb::typed_value::Value as V;
227        let result = match value
228            .value
229            .ok_or_else(|| Status::invalid_argument("missing TypedValue member"))?
230        {
231            V::StringVal(v) => Self::String(v),
232            V::AsciiVal(v) => Self::Ascii(v),
233            V::IntVal(v) => Self::Int(v.to_string()),
234            V::UintVal(v) => Self::Uint(v.to_string()),
235            V::BoolVal(v) => Self::Bool(v),
236            V::DoubleVal(v) => Self::Double(v),
237            V::FloatVal(v) => Self::Double(f64::from(v)),
238            V::DecimalVal(v) => Self::Decimal(Decimal {
239                digits: v.digits.to_string(),
240                precision: v.precision,
241            }),
242            V::JsonVal(v) => Self::Json(super::json::parse(&v)?),
243            V::JsonIetfVal(v) => Self::JsonIetf(super::json::parse(&v)?),
244            V::LeaflistVal(v) => Self::Leaflist(
245                v.element
246                    .into_iter()
247                    .map(Self::from_proto)
248                    .collect::<Result<_, _>>()?,
249            ),
250            V::BytesVal(_) | V::AnyVal(_) | V::ProtoBytes(_) => {
251                return Err(Status::unimplemented("opaque values are excluded"))
252            }
253        };
254        // Enforce semantic constraints even for programmatically constructed messages.
255        result.clone().into_proto()?;
256        Ok(result)
257    }
258}
259impl Update {
260    pub fn into_proto(self) -> Result<pb::Update, Status> {
261        Ok(pb::Update {
262            path: Some(self.path.into_proto()?),
263            value: None,
264            val: Some(self.value.into_proto()?),
265            duplicates: self.duplicates,
266        })
267    }
268    pub fn from_proto(v: pb::Update) -> Result<Self, Status> {
269        if v.value.is_some() {
270            return Err(Status::unimplemented("legacy values are excluded"));
271        }
272        Ok(Self {
273            path: Path::from_proto(v.path)?,
274            value: Value::from_proto(
275                v.val
276                    .ok_or_else(|| Status::invalid_argument("missing update value"))?,
277            )?,
278            duplicates: v.duplicates,
279        })
280    }
281}
282impl Notification {
283    pub fn into_proto(self) -> Result<pb::Notification, Status> {
284        limit(self.update.len() <= 256 && self.delete.len() <= 256)?;
285        Ok(pb::Notification {
286            timestamp: self.timestamp.parse().map_err(|_| {
287                Status::invalid_argument("timestamp must be i64 nanosecond decimal string")
288            })?,
289            prefix: Some(self.prefix.into_proto()?),
290            update: self
291                .update
292                .into_iter()
293                .map(Update::into_proto)
294                .collect::<Result<_, _>>()?,
295            delete: self
296                .delete
297                .into_iter()
298                .map(Path::into_proto)
299                .collect::<Result<_, _>>()?,
300            atomic: self.atomic,
301        })
302    }
303    pub fn from_proto(v: pb::Notification) -> Result<Self, Status> {
304        limit(v.update.len() <= 256 && v.delete.len() <= 256)?;
305        Ok(Self {
306            timestamp: v.timestamp.to_string(),
307            prefix: Path::from_proto(v.prefix)?,
308            update: v
309                .update
310                .into_iter()
311                .map(Update::from_proto)
312                .collect::<Result<_, _>>()?,
313            delete: v
314                .delete
315                .into_iter()
316                .map(|p| Path::from_proto(Some(p)))
317                .collect::<Result<_, _>>()?,
318            atomic: v.atomic,
319        })
320    }
321}
322pub fn encoding(name: &str) -> Result<i32, Status> {
323    Ok(match name {
324        "JSON" => 0,
325        "PROTO" => 2,
326        "ASCII" => 3,
327        "JSON_IETF" => 4,
328        _ => return Err(Status::unimplemented("unsupported gNMI encoding")),
329    })
330}
331pub fn encoding_name(value: i32) -> Result<&'static str, Status> {
332    match value {
333        0 => Ok("JSON"),
334        2 => Ok("PROTO"),
335        3 => Ok("ASCII"),
336        4 => Ok("JSON_IETF"),
337        _ => Err(Status::unimplemented("unsupported gNMI encoding")),
338    }
339}
340pub fn notification_encoding(notification: &pb::Notification, encoding: i32) -> Result<(), Status> {
341    encoding_name(encoding)?;
342    for update in &notification.update {
343        use pb::typed_value::Value as V;
344        let value = update
345            .val
346            .as_ref()
347            .and_then(|v| v.value.as_ref())
348            .ok_or_else(|| Status::invalid_argument("missing typed value"))?;
349        if !match encoding {
350            0 => matches!(value, V::JsonVal(_)),
351            4 => matches!(value, V::JsonIetfVal(_)),
352            3 => matches!(value, V::AsciiVal(_)),
353            2 => !matches!(
354                value,
355                V::JsonVal(_)
356                    | V::JsonIetfVal(_)
357                    | V::AsciiVal(_)
358                    | V::BytesVal(_)
359                    | V::AnyVal(_)
360                    | V::ProtoBytes(_)
361            ),
362            _ => false,
363        } {
364            return Err(Status::invalid_argument(
365                "notification value does not match requested encoding",
366            ));
367        }
368    }
369    Ok(())
370}
371pub fn checked<T: prost::Message + codec::WireName>(v: T) -> Result<T, Status> {
372    if v.encoded_len() > codec::MAX_MESSAGE_BYTES {
373        return Err(Status::resource_exhausted("gNMI message exceeds 1 MiB"));
374    }
375    codec::validate_wire(T::NAME, &v.encode_to_vec())?;
376    Ok(v)
377}