Skip to main content

macula_rust/frame/
check_payload.rs

1//! The checks a sender runs so that nothing the decoding rule refuses on
2//! arrival leaves this node, as macula_frame's check_payload/1 and
3//! check_frame/1: a map key that is not text or an integer, two keys of one
4//! map that encode alike, an integer outside -2^63 to 2^63-1, a float that is
5//! NaN or infinite, too deep a nesting, and too many items. Text is valid
6//! UTF-8 by construction here.
7
8use crate::cbor::{self, Value, MAX_ELEMENTS, MAX_NESTING_DEPTH};
9
10use super::{FrameError, MAX_FRAME_BYTES};
11
12/// How many lists and maps a payload may nest, the outermost counted: a
13/// payload travels inside a frame's map, which takes one level.
14pub const MAX_PAYLOAD_NESTING: usize = MAX_NESTING_DEPTH - 1;
15
16/// How many of the decoding rule's items a payload leaves for the frame
17/// around it.
18pub const FRAME_RESERVED_ELEMENTS: usize = 64;
19
20/// How many CBOR items a payload may hold, itself included.
21pub const MAX_PAYLOAD_ELEMENTS: usize = MAX_ELEMENTS - FRAME_RESERVED_ELEMENTS;
22
23/// Whether `payload` is admissible as a frame payload. It also refuses a
24/// payload whose own encoding is over the frame cap.
25pub fn check_payload(payload: &Value) -> Result<(), FrameError> {
26    let mut check = RuleCheck {
27        subject: "payload",
28        max_items: MAX_PAYLOAD_ELEMENTS,
29        max_nesting: MAX_PAYLOAD_NESTING,
30        items: 0,
31    };
32    check
33        .value(payload, &mut Vec::new())
34        .map_err(FrameError::Payload)?;
35    let encoded = cbor::encode(payload).map_err(|e| FrameError::Payload(e.to_string()))?;
36    if encoded.len() > MAX_FRAME_BYTES {
37        return Err(FrameError::Payload(format!(
38            "the payload encodes to {} bytes, over the {MAX_FRAME_BYTES}-byte frame cap",
39            encoded.len()
40        )));
41    }
42    Ok(())
43}
44
45/// Whether the whole `frame` is one the decoding rule accepts where it
46/// arrives, the check macula runs on every frame before it is sent.
47pub fn check_frame(frame: &Value) -> Result<(), FrameError> {
48    let mut check = RuleCheck {
49        subject: "frame",
50        max_items: MAX_ELEMENTS,
51        max_nesting: MAX_NESTING_DEPTH,
52        items: 0,
53    };
54    check
55        .value(frame, &mut Vec::new())
56        .map_err(FrameError::BreaksDecodingRule)
57}
58
59/// A walk of a payload or a whole frame, its subject, under the decoding
60/// rule's limits for it, counting its items.
61struct RuleCheck {
62    subject: &'static str,
63    max_items: usize,
64    max_nesting: usize,
65    items: usize,
66}
67
68impl RuleCheck {
69    fn value(&mut self, v: &Value, path: &mut Vec<String>) -> Result<(), String> {
70        self.items += 1;
71        if self.items > self.max_items {
72            return Err(format!(
73                "the {} holds more than {} items, at {}",
74                self.subject,
75                self.max_items,
76                self.at(path)
77            ));
78        }
79        match v {
80            Value::Float(f) if !f.is_finite() => {
81                Err(format!("a float that is not finite at {}", self.at(path)))
82            }
83            Value::Int(n) if i64::try_from(*n).is_err() => Err(format!(
84                "an integer outside -2^63 to 2^63-1 at {}",
85                self.at(path)
86            )),
87            Value::List(items) => {
88                self.nesting(path)?;
89                for (i, item) in items.iter().enumerate() {
90                    path.push(i.to_string());
91                    self.value(item, path)?;
92                    path.pop();
93                }
94                Ok(())
95            }
96            Value::Map(pairs) => {
97                self.nesting(path)?;
98                let mut seen = std::collections::HashSet::with_capacity(pairs.len());
99                for (key, value) in pairs {
100                    if !matches!(key, Value::Text(_) | Value::Int(_)) {
101                        return Err(format!(
102                            "a map key that is not text or an integer at {}",
103                            self.at(path)
104                        ));
105                    }
106                    self.value(key, path)?;
107                    let encoded = cbor::encode(key).map_err(|e| e.to_string())?;
108                    if !seen.insert(encoded) {
109                        return Err(format!(
110                            "two keys of the map at {} encode alike",
111                            self.at(path)
112                        ));
113                    }
114                    path.push(match key {
115                        Value::Text(t) => t.clone(),
116                        other => format!("{other:?}"),
117                    });
118                    self.value(value, path)?;
119                    path.pop();
120                }
121                Ok(())
122            }
123            _ => Ok(()),
124        }
125    }
126
127    /// Refuses a list or map at `path` that would nest more than the limit.
128    fn nesting(&self, path: &[String]) -> Result<(), String> {
129        if path.len() >= self.max_nesting {
130            return Err(format!(
131                "lists and maps at {} nest more than {} levels",
132                self.at(path),
133                self.max_nesting
134            ));
135        }
136        Ok(())
137    }
138
139    fn at(&self, path: &[String]) -> String {
140        if path.is_empty() {
141            format!("the {} root", self.subject)
142        } else {
143            path.join(".")
144        }
145    }
146}