use crate::{parse_with_dup_check, Canonicalizer, JcsError};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct ResourceCeilings {
pub max_input_bytes: usize,
pub max_nodes: usize,
pub max_depth: usize,
pub max_object_keys: usize,
pub max_string_bytes: usize,
pub max_array_len: usize,
}
impl Default for ResourceCeilings {
fn default() -> Self {
Self {
max_input_bytes: 1 << 20,
max_nodes: 100_000,
max_depth: 32,
max_object_keys: 128,
max_string_bytes: 1 << 20,
max_array_len: 1024,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnforcedRule {
pub rule: String,
pub used: Option<usize>,
pub limit: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BoundaryEnforcementReceipt {
pub canonicalization_profile: String,
pub enforced_rules: Vec<EnforcedRule>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BoundaryAdmission {
pub value: Value,
pub canonical_bytes: Vec<u8>,
pub receipt: BoundaryEnforcementReceipt,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct BoundaryProfile {
pub resource_ceilings: ResourceCeilings,
}
#[derive(Default)]
struct ResourceUse {
nodes: usize,
depth: usize,
object_keys: usize,
string_bytes: usize,
array_len: usize,
}
impl BoundaryProfile {
pub fn new(resource_ceilings: ResourceCeilings) -> Self {
Self { resource_ceilings }
}
pub fn rfc8785() -> Self {
Self::default()
}
pub fn parse(&self, input: &str) -> Result<BoundaryAdmission, JcsError> {
self.check(
"input_bytes",
input.len(),
self.resource_ceilings.max_input_bytes,
)?;
let value = parse_with_dup_check(input)?;
let mut use_counts = ResourceUse::default();
self.inspect(&value, 0, &mut use_counts)?;
let canonical_bytes = Canonicalizer::new().canonicalize_bytes(&value)?;
let rules = [
(
"input_bytes",
input.len(),
self.resource_ceilings.max_input_bytes,
),
("nodes", use_counts.nodes, self.resource_ceilings.max_nodes),
("depth", use_counts.depth, self.resource_ceilings.max_depth),
(
"object_keys",
use_counts.object_keys,
self.resource_ceilings.max_object_keys,
),
(
"string_bytes",
use_counts.string_bytes,
self.resource_ceilings.max_string_bytes,
),
(
"array_len",
use_counts.array_len,
self.resource_ceilings.max_array_len,
),
]
.into_iter()
.map(|(rule, used, limit)| EnforcedRule {
rule: rule.to_owned(),
used: Some(used),
limit: Some(limit),
})
.chain(
["duplicate_keys", "canonicalization"]
.into_iter()
.map(|rule| EnforcedRule {
rule: rule.to_owned(),
used: None,
limit: None,
}),
)
.collect();
Ok(BoundaryAdmission {
value,
canonical_bytes,
receipt: BoundaryEnforcementReceipt {
canonicalization_profile: "rfc8785".to_owned(),
enforced_rules: rules,
},
})
}
pub fn check_resources(&self, value: &Value) -> Result<(), JcsError> {
self.inspect(value, 0, &mut ResourceUse::default())
}
fn inspect(
&self,
value: &Value,
depth: usize,
use_counts: &mut ResourceUse,
) -> Result<(), JcsError> {
use_counts.nodes += 1;
use_counts.depth = use_counts.depth.max(depth);
self.check("nodes", use_counts.nodes, self.resource_ceilings.max_nodes)?;
self.check("depth", depth, self.resource_ceilings.max_depth)?;
match value {
Value::Object(map) => {
use_counts.object_keys = use_counts.object_keys.max(map.len());
self.check(
"object_keys",
map.len(),
self.resource_ceilings.max_object_keys,
)?;
for (key, child) in map {
use_counts.string_bytes = use_counts.string_bytes.max(key.len());
self.check(
"string_bytes",
key.len(),
self.resource_ceilings.max_string_bytes,
)?;
self.inspect(child, depth + 1, use_counts)?;
}
}
Value::Array(items) => {
use_counts.array_len = use_counts.array_len.max(items.len());
self.check(
"array_len",
items.len(),
self.resource_ceilings.max_array_len,
)?;
for child in items {
self.inspect(child, depth + 1, use_counts)?;
}
}
Value::String(string) => {
use_counts.string_bytes = use_counts.string_bytes.max(string.len());
self.check(
"string_bytes",
string.len(),
self.resource_ceilings.max_string_bytes,
)?;
}
_ => {}
}
Ok(())
}
fn check(&self, resource: &str, used: usize, limit: usize) -> Result<(), JcsError> {
if used > limit {
return Err(JcsError::ResourceCeilingExceeded {
resource: resource.to_owned(),
used,
limit,
});
}
Ok(())
}
pub fn identifier(&self) -> &'static str {
"rfc8785"
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn check_resources_accepts_bounded_value() {
BoundaryProfile::default()
.check_resources(&json!({"a": "hello", "b": [1, 2, 3]}))
.unwrap();
}
#[test]
fn check_resources_rejects_excess_depth() {
let mut profile = BoundaryProfile::default();
profile.resource_ceilings.max_depth = 2;
assert!(matches!(
profile.check_resources(&json!({"a": {"b": {"c": 1}}})),
Err(JcsError::ResourceCeilingExceeded { ref resource, .. }) if resource == "depth"
));
}
}