diff --git a/Cargo.toml b/Cargo.toml index 2b004f5..f9cf33c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ description = "Simple, modern, ergonomic JSON-RPC 2.0 router built with tower an keywords = ["json-rpc", "jsonrpc", "json"] categories = ["web-programming::http-server", "web-programming::websocket"] -version = "0.3.3" +version = "0.3.4" edition = "2021" rust-version = "1.81" authors = ["init4", "James Prestwich"] diff --git a/src/types/batch.rs b/src/types/batch.rs index 0ae8dea..9ee745e 100644 --- a/src/types/batch.rs +++ b/src/types/batch.rs @@ -60,33 +60,144 @@ impl TryFrom for InboundData { } debug!("Parsing inbound data"); - // Special-case a single request, rejecting invalid JSON. - if bytes.starts_with(b"{") { - let rv: &RawValue = serde_json::from_slice(bytes.as_ref())?; + // We set up the deserializer to read from the byte buffer. + let mut deserializer = serde_json::Deserializer::from_slice(&bytes); - let range = find_range!(bytes, rv.get()); + // If we succesfully deser a batch, we can return it. + if let Ok(reqs) = Vec::<&RawValue>::deserialize(&mut deserializer) { + // `.end()` performs trailing charcter checks + deserializer.end()?; + let reqs = reqs + .into_iter() + .map(|raw| find_range!(bytes, raw.get())) + .collect(); return Ok(Self { bytes, - reqs: vec![range], - single: true, + reqs, + single: false, }); } - // Otherwise, parse the batch - let DeserHelper(reqs) = serde_json::from_slice(bytes.as_ref())?; - let reqs = reqs - .into_iter() - .map(|raw| find_range!(bytes, raw.get())) - .collect(); + // If it's not a batch, it should be a single request. + let rv = <&RawValue>::deserialize(&mut deserializer)?; + + // `.end()` performs trailing charcter checks + deserializer.end()?; + + // If not a JSON object, return an error. + if !rv.get().starts_with("{") { + return Err(RequestError::UnexpectedJsonType); + } + + let range = find_range!(bytes, rv.get()); Ok(Self { bytes, - reqs, - single: false, + reqs: vec![range], + single: true, }) } } -#[derive(Debug, Deserialize)] -struct DeserHelper<'a>(#[serde(borrow)] Vec<&'a RawValue>); +#[cfg(test)] +mod test { + use super::*; + + fn assert_invalid_json(batch: &'static str) { + let bytes = Bytes::from(batch); + let err = InboundData::try_from(bytes).unwrap_err(); + + assert!(matches!(err, RequestError::InvalidJson(_))); + } + + #[test] + fn test_deser_batch() { + let batch = r#"[ + {"id": 1, "method": "foo", "params": [1, 2, 3]}, + {"id": 2, "method": "bar", "params": [4, 5, 6]} + ]"#; + + let bytes = Bytes::from(batch); + let batch = InboundData::try_from(bytes).unwrap(); + + assert_eq!(batch.len(), 2); + assert!(!batch.single()); + } + + #[test] + fn test_deser_single() { + let single = r#"{"id": 1, "method": "foo", "params": [1, 2, 3]}"#; + + let bytes = Bytes::from(single); + let batch = InboundData::try_from(bytes).unwrap(); + + assert_eq!(batch.len(), 1); + assert!(batch.single()); + } + + #[test] + fn test_deser_single_with_whitespace() { + let single = r#" + + {"id": 1, "method": "foo", "params": [1, 2, 3]} + + "#; + + let bytes = Bytes::from(single); + let batch = InboundData::try_from(bytes).unwrap(); + + assert_eq!(batch.len(), 1); + assert!(batch.single()); + } + + #[test] + fn test_broken_batch() { + let batch = r#"[ + {"id": 1, "method": "foo", "params": [1, 2, 3]}, + {"id": 2, "method": "bar", "params": [4, 5, 6] + ]"#; + + assert_invalid_json(batch); + } + + #[test] + fn test_junk_prefix() { + let batch = r#"JUNK[ + {"id": 1, "method": "foo", "params": [1, 2, 3]}, + {"id": 2, "method": "bar", "params": [4, 5, 6]} + ]"#; + + assert_invalid_json(batch); + } + + #[test] + fn test_junk_suffix() { + let batch = r#"[ + {"id": 1, "method": "foo", "params": [1, 2, 3]}, + {"id": 2, "method": "bar", "params": [4, 5, 6]} + ]JUNK"#; + + assert_invalid_json(batch); + } + + #[test] + fn test_invalid_utf8_prefix() { + let batch = r#"\xF1\x80[ + {"id": 1, "method": "foo", "params": [1, 2, 3]}, + {"id": 2, "method": "bar", "params": [4, 5, 6]} + ]"#; + + assert_invalid_json(batch); + } + + #[test] + fn test_invalid_utf8_suffix() { + let batch = r#"[ + {"id": 1, "method": "foo", "params": [1, 2, 3]}, + {"id": 2, "method": "bar", "params": [4, 5, 6]} + ]\xF1\x80"#; + + assert_invalid_json(batch); + } +} diff --git a/src/types/req.rs b/src/types/req.rs index 16464a2..81e83d9 100644 --- a/src/types/req.rs +++ b/src/types/req.rs @@ -192,6 +192,7 @@ impl Request { #[cfg(test)] mod test { + use crate::types::METHOD_LEN_LIMIT; use super::*; @@ -236,4 +237,28 @@ mod test { assert_eq!(size, METHOD_LEN_LIMIT + 1); } + + #[test] + fn test_with_linebreak() { + let bytes = Bytes::from_static( + r#" + + { "id": 1, + "jsonrpc": "2.0", + "method": "eth_getBalance", + "params": ["0x4444d38c385d0969C64c4C8f996D7536d16c28B9", "latest"] + } + + "# + .as_bytes(), + ); + let req = Request::try_from(bytes).unwrap(); + + assert_eq!(req.id(), Some("1")); + assert_eq!(req.method(), r#"eth_getBalance"#); + assert_eq!( + req.params(), + r#"["0x4444d38c385d0969C64c4C8f996D7536d16c28B9", "latest"]"# + ); + } }