use qubit_budget::ResourceLimit;
use qubit_budget::json::JsonDecodeLimits;
use qubit_budget::json::JsonDecodeSession;
use qubit_budget::json::JsonResource;
use qubit_budget::json::JsonValueLimits;
use qubit_json::decode::JsonDecodeErrorKind;
use qubit_json::decode::JsonDecoder;
use qubit_json::decode::JsonSyntaxErrorReason;
use serde::Deserializer;
use serde::de::DeserializeSeed;
use serde::de::Error as DeError;
use serde_json::Value;
use serde_json::json;
struct FailingSeed;
impl<'de> DeserializeSeed<'de> for FailingSeed {
type Value = ();
fn deserialize<D>(self, _deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
Err(D::Error::custom("seed failure"))
}
}
struct NonConsumingSeed;
impl<'de> DeserializeSeed<'de> for NonConsumingSeed {
type Value = ();
fn deserialize<D>(self, _deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
Ok(())
}
}
fn nested_array(container_depth: usize) -> String {
format!("{}0{}", "[".repeat(container_depth), "]".repeat(container_depth))
}
#[test]
fn test_json_text_decoder_decodes_typed_value() {
let session = JsonDecodeSession::from_limits(JsonDecodeLimits::<JsonResource, usize>::builder().build());
let value = JsonDecoder::new(session)
.decode_utf8::<bool>(b"true")
.expect("JSON boolean should decode");
assert!(value);
}
#[test]
fn test_json_text_decoder_validates_complete_document() {
let session = JsonDecodeSession::from_limits(JsonDecodeLimits::<JsonResource, usize>::builder().build());
JsonDecoder::new(session)
.validate_utf8(br#"{"ok":[true,null]}"#)
.expect("a complete JSON document should validate");
}
#[test]
fn test_json_text_decoder_maps_seed_failure() {
let session = JsonDecodeSession::from_limits(JsonDecodeLimits::<JsonResource, usize>::builder().build());
assert!(
JsonDecoder::new(session)
.decode_seed_utf8(FailingSeed, b"true")
.is_err()
);
}
#[test]
fn test_json_text_decoder_rejects_unconsumed_seed_input() {
let session = JsonDecodeSession::from_limits(JsonDecodeLimits::<JsonResource, usize>::builder().build());
assert!(
JsonDecoder::new(session)
.decode_seed_utf8(NonConsumingSeed, b"true")
.is_err()
);
}
#[test]
fn test_json_text_decoder_preserves_private_number_marker_object() {
const MARKER: &str = concat!("$", "serde_json", "::private::Number");
let input = format!(r#"{{"{MARKER}":"123"}}"#);
let limits = JsonDecodeLimits::<JsonResource, usize>::builder()
.value_limits(
JsonValueLimits::builder()
.number_bytes_limit(ResourceLimit::new(JsonResource::NumberBytes, 1))
.build(),
)
.build();
let value = JsonDecoder::with_limits(limits)
.decode_str::<Value>(&input)
.expect("the marker-shaped object contains no JSON number token");
assert_eq!(value, json!({MARKER: "123"}));
}
#[test]
fn test_json_text_decoder_rejects_integer_above_u64() {
let error = JsonDecoder::unlimited()
.decode_utf8::<Value>(b"18446744073709551616")
.expect_err("an integer above u64 must be rejected");
assert!(error.to_string().contains("outside the supported 64-bit range"));
}
#[test]
fn test_json_text_decoder_enforces_integer_boundaries() {
let mut decoder = JsonDecoder::unlimited();
assert_eq!(
decoder.decode_utf8::<Value>(b"-9223372036854775808").unwrap(),
json!(i64::MIN),
);
assert_eq!(
decoder.decode_utf8::<Value>(b"18446744073709551615").unwrap(),
json!(u64::MAX),
);
for input in [b"-9223372036854775809".as_slice(), b"18446744073709551616".as_slice()] {
let error = decoder
.decode_utf8::<Value>(input)
.expect_err("an integer outside the supported range must fail");
assert_eq!(
error.syntax_error().map(|error| error.reason()),
Some(JsonSyntaxErrorReason::IntegerOutOfRange),
);
}
}
#[test]
fn test_json_text_decoder_rejects_float_outside_finite_f64_range() {
let error = JsonDecoder::unlimited()
.decode_utf8::<Value>(b"1e400")
.expect_err("infinite f64 results must be rejected");
assert_eq!(
error.syntax_error().map(|error| error.reason()),
Some(JsonSyntaxErrorReason::FloatOutOfRange),
);
}
#[test]
fn test_json_text_decoder_prioritizes_number_budget_over_range() {
let input = b"18446744073709551616";
let limits = JsonDecodeLimits::<JsonResource, usize>::builder()
.value_limits(
JsonValueLimits::builder()
.number_bytes_limit(ResourceLimit::new(JsonResource::NumberBytes, input.len() - 1))
.build(),
)
.build();
let error = JsonDecoder::with_limits(limits)
.decode_utf8::<Value>(input)
.expect_err("the tighter number-byte budget must reject first");
assert_eq!(error.kind(), JsonDecodeErrorKind::Budget);
}
#[test]
fn test_json_text_decoder_classifies_serde_depth_as_deserialization() {
let shallow = nested_array(127);
let deep = nested_array(128);
let mut decoder = JsonDecoder::unlimited();
decoder
.validate_str(&shallow)
.expect("the shallow document must pass lexical admission");
decoder
.decode_str::<Value>(&shallow)
.expect("the shallow document must materialize as a JSON value");
decoder
.validate_str(&deep)
.expect("the deep document remains valid lexical JSON");
let error = decoder
.decode_str::<Value>(&deep)
.expect_err("the deep value must hit Serde's recursion guard");
assert_eq!(error.kind(), JsonDecodeErrorKind::Deserialize);
decoder
.validate_utf8(deep.as_bytes())
.expect("UTF-8 lexical admission must use the same depth contract");
let error = decoder
.decode_utf8::<Value>(deep.as_bytes())
.expect_err("the deep UTF-8 value must hit Serde's recursion guard");
assert_eq!(error.kind(), JsonDecodeErrorKind::Deserialize);
}