Skip to main content

contextvm_sdk/core/
validation.rs

1//! Message validation utilities.
2
3use crate::core::constants::MAX_MESSAGE_SIZE;
4use crate::core::types::JsonRpcMessage;
5
6/// Validate that a message's content doesn't exceed the maximum size (1MB).
7pub fn validate_message_size(content: &str) -> bool {
8    content.len() <= MAX_MESSAGE_SIZE
9}
10
11/// Validate size and structure, then parse into a [`JsonRpcMessage`].
12pub fn validate_and_parse(content: &str) -> Option<JsonRpcMessage> {
13    if !validate_message_size(content) {
14        tracing::warn!("Message size validation failed: {} bytes", content.len());
15        return None;
16    }
17
18    let value: serde_json::Value = serde_json::from_str(content).ok()?;
19    validate_message(&value)
20}
21
22/// Validate size against a custom byte bound, then parse into a [`JsonRpcMessage`].
23///
24/// Unlike [`validate_and_parse`], which enforces the 1 MB per-event cap
25/// ([`MAX_MESSAGE_SIZE`]), this bounds `content` to `max_bytes` — the receiver
26/// policy's `maxTransferBytes` (100 MiB default). It is the dedicated entrypoint
27/// for **reassembled** CEP-22 oversized payloads, which legitimately exceed the
28/// per-event cap (the very limit the transfer profile works around). The bound
29/// keeps a single ceiling on reassembled-payload memory rather than leaving it
30/// unbounded.
31pub fn validate_and_parse_oversized(content: &str, max_bytes: usize) -> Option<JsonRpcMessage> {
32    if content.len() > max_bytes {
33        tracing::warn!(
34            "Oversized message exceeds max transfer bytes: {} > {}",
35            content.len(),
36            max_bytes
37        );
38        return None;
39    }
40
41    let value: serde_json::Value = serde_json::from_str(content).ok()?;
42    validate_message(&value)
43}
44
45/// Validate that a JSON value is a well-formed JSON-RPC 2.0 message.
46///
47/// Checks:
48/// - Has `jsonrpc: "2.0"`
49/// - Is one of: request (id + method), response (id + result), error (id + error), notification (method, no id)
50pub fn validate_message(value: &serde_json::Value) -> Option<JsonRpcMessage> {
51    // Must have jsonrpc field
52    let jsonrpc = value.get("jsonrpc")?.as_str()?;
53    if jsonrpc != "2.0" {
54        return None;
55    }
56
57    serde_json::from_value(value.clone()).ok()
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use serde_json::json;
64
65    #[test]
66    fn test_valid_request() {
67        let msg = json!({"jsonrpc": "2.0", "id": 1, "method": "tools/list"});
68        let parsed = validate_message(&msg).unwrap();
69        assert!(parsed.is_request());
70    }
71
72    #[test]
73    fn test_valid_notification() {
74        let msg = json!({"jsonrpc": "2.0", "method": "notifications/initialized"});
75        let parsed = validate_message(&msg).unwrap();
76        assert!(parsed.is_notification());
77    }
78
79    #[test]
80    fn test_valid_response() {
81        let msg = json!({"jsonrpc": "2.0", "id": 1, "result": {"tools": []}});
82        let parsed = validate_message(&msg).unwrap();
83        assert!(parsed.is_response());
84    }
85
86    #[test]
87    fn test_invalid_version() {
88        let msg = json!({"jsonrpc": "1.0", "id": 1, "method": "test"});
89        assert!(validate_message(&msg).is_none());
90    }
91
92    #[test]
93    fn test_size_validation() {
94        assert!(validate_message_size("hello"));
95        let big = "x".repeat(MAX_MESSAGE_SIZE + 1);
96        assert!(!validate_message_size(&big));
97    }
98
99    #[test]
100    fn test_validate_and_parse_valid_request() {
101        let content = r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#;
102        let msg = validate_and_parse(content).unwrap();
103        assert!(msg.is_request());
104        assert_eq!(msg.method(), Some("tools/list"));
105    }
106
107    #[test]
108    fn test_validate_and_parse_rejects_oversized() {
109        let padding = "x".repeat(MAX_MESSAGE_SIZE);
110        let content = format!(r#"{{"jsonrpc":"2.0","id":1,"method":"{}"}}"#, padding);
111        assert!(validate_and_parse(&content).is_none());
112    }
113
114    #[test]
115    fn test_validate_and_parse_rejects_invalid_version() {
116        let content = r#"{"jsonrpc":"1.0","id":1,"method":"test"}"#;
117        assert!(validate_and_parse(content).is_none());
118    }
119
120    #[test]
121    fn test_validate_and_parse_rejects_invalid_json() {
122        assert!(validate_and_parse("not json").is_none());
123    }
124
125    #[test]
126    fn test_validate_and_parse_oversized_bypasses_1mb_cap() {
127        // A valid message larger than the 1 MB per-event cap.
128        let big_value = "m".repeat(MAX_MESSAGE_SIZE + 1024);
129        let content = format!(r#"{{"jsonrpc":"2.0","id":1,"result":{{"value":"{big_value}"}}}}"#);
130        assert!(content.len() > MAX_MESSAGE_SIZE);
131
132        // The standard entrypoint rejects it on size...
133        assert!(validate_and_parse(&content).is_none());
134        // ...but the oversized entrypoint accepts it under a higher bound.
135        let parsed = validate_and_parse_oversized(&content, 100 * 1024 * 1024).unwrap();
136        assert!(parsed.is_response());
137    }
138
139    #[test]
140    fn test_validate_and_parse_oversized_enforces_its_own_bound() {
141        let content = r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#;
142        // Rejected when the content exceeds the supplied max_bytes.
143        assert!(validate_and_parse_oversized(content, 8).is_none());
144        // Accepted under a sufficient bound.
145        assert!(validate_and_parse_oversized(content, 1024)
146            .unwrap()
147            .is_request());
148    }
149
150    #[test]
151    fn test_validate_and_parse_oversized_rejects_invalid_version() {
152        let content = r#"{"jsonrpc":"1.0","id":1,"method":"test"}"#;
153        assert!(validate_and_parse_oversized(content, 1024).is_none());
154    }
155}