use qubit_budget::ResourceLimit;
use qubit_budget::StructureLimits;
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::Deserialize;
use serde::Deserializer;
use serde::de::DeserializeSeed;
use serde::de::Error as DeError;
use serde::de::IgnoredAny;
#[test]
fn test_escaped_and_direct_unicode_charge_equal_decoded_payload() {
let limits = JsonDecodeLimits::<JsonResource, usize>::builder()
.value_limits(
JsonValueLimits::<JsonResource, usize>::builder()
.payload_bytes_limit(ResourceLimit::new(JsonResource::PayloadBytes, 3))
.build(),
)
.build();
for input in [br#""\u4e2d""#.as_slice(), "\"中\"".as_bytes()] {
let session = JsonDecodeSession::from_limits(limits);
assert_eq!(
JsonDecoder::new(session)
.decode_utf8::<String>(input)
.expect("three decoded UTF-8 bytes must fit"),
"中"
);
}
}
#[test]
fn test_deeply_nested_input_fails_by_limit_without_stack_overflow() {
let input = format!("{}0{}", "[".repeat(20_000), "]".repeat(20_000));
let session = JsonDecodeSession::from_limits(
JsonDecodeLimits::<JsonResource, usize>::builder()
.value_limits(
JsonValueLimits::<JsonResource, usize>::builder()
.structure_limits(
StructureLimits::builder().depth_limit(ResourceLimit::new(JsonResource::Depth, 128)),
)
.build(),
)
.build(),
);
let error = JsonDecoder::new(session)
.decode_utf8::<serde_json::Value>(input.as_bytes())
.expect_err("the depth limit must reject the document");
assert_eq!(error.kind(), JsonDecodeErrorKind::Budget);
}
#[test]
fn test_json_decode_reports_structured_syntax_locations() {
let cases = [
(b"".as_slice(), 0, 1, 1, JsonSyntaxErrorReason::UnexpectedEnd),
(br#"{"a" 1}"#.as_slice(), 5, 1, 6, JsonSyntaxErrorReason::ExpectedColon),
(
br#"[1 2]"#.as_slice(),
3,
1,
4,
JsonSyntaxErrorReason::ExpectedCommaOrArrayEnd,
),
(
br#"{"a":1 "b":2}"#.as_slice(),
7,
1,
8,
JsonSyntaxErrorReason::ExpectedCommaOrObjectEnd,
),
(br#""\x""#.as_slice(), 2, 1, 3, JsonSyntaxErrorReason::InvalidEscape),
(br#"01"#.as_slice(), 1, 1, 2, JsonSyntaxErrorReason::InvalidNumber),
(
br#"true false"#.as_slice(),
5,
1,
6,
JsonSyntaxErrorReason::TrailingCharacters,
),
];
for (input, offset, line, column, reason) in cases {
let session = JsonDecodeSession::from_limits(JsonDecodeLimits::<JsonResource, usize>::builder().build());
let error = JsonDecoder::new(session)
.decode_utf8::<serde_json::Value>(input)
.expect_err("input should be rejected");
let error = error.syntax_error().expect("expected structured syntax error");
assert_eq!(error.offset(), offset);
assert_eq!(error.line(), line);
assert_eq!(error.column(), column);
assert_eq!(error.reason(), reason);
}
}
#[test]
fn test_json_decode_counts_unicode_columns_and_crlf_lines() {
let input = "{\r\n \"中\" 1}".as_bytes();
let session = JsonDecodeSession::from_limits(JsonDecodeLimits::<JsonResource, usize>::builder().build());
let error = JsonDecoder::new(session)
.decode_utf8::<serde_json::Value>(input)
.expect_err("missing colon should be rejected");
let error = error.syntax_error().expect("expected structured syntax error");
assert_eq!(error.line(), 2);
assert_eq!(error.column(), 7);
assert_eq!(error.reason(), JsonSyntaxErrorReason::ExpectedColon);
}
#[test]
fn test_typed_decode_failure_consumes_input_before_the_next_attempt() {
let session = JsonDecodeSession::from_limits(
JsonDecodeLimits::<JsonResource, usize>::builder()
.input_bytes_limit(ResourceLimit::new(JsonResource::InputBytes, 3))
.build(),
);
let mut decoder = JsonDecoder::new(session);
let error = decoder
.decode_utf8::<u8>(br#""x""#)
.expect_err("the string must fail typed decoding");
assert_eq!(error.kind(), JsonDecodeErrorKind::Deserialize);
let error = decoder
.decode_utf8::<u8>(b"0")
.expect_err("the exhausted input budget must reject the next value");
assert_eq!(error.kind(), JsonDecodeErrorKind::Budget);
}
#[test]
fn test_seed_rejection_keeps_input_and_rolls_back_value() {
let input = br#"{"value":1}"#;
let session = JsonDecodeSession::from_limits(
JsonDecodeLimits::<JsonResource, usize>::builder()
.max_input_bytes(64)
.max_nodes(8)
.build(),
);
let mut decoder = JsonDecoder::new(session);
assert!(decoder.decode_seed_utf8(RejectSeed, input).is_err());
assert_eq!(
decoder
.session()
.input_budget()
.expect("configured input budget")
.used(),
input.len()
);
assert_eq!(decoder.session().value_budget().used_nodes(), Some(0));
}
#[test]
fn test_syntax_rejection_rolls_back_value_and_reuses_session() {
let rejected = br#"{"value":]"#;
let accepted = br#"null"#;
let session = JsonDecodeSession::from_limits(
JsonDecodeLimits::<JsonResource, usize>::builder()
.max_input_bytes(rejected.len() + accepted.len())
.max_nodes(1)
.build(),
);
let mut decoder = JsonDecoder::new(session);
assert!(decoder.decode_utf8::<serde_json::Value>(rejected).is_err());
assert_eq!(
decoder
.session()
.input_budget()
.expect("configured input budget")
.used(),
rejected.len()
);
assert_eq!(decoder.session().value_budget().used_nodes(), Some(0));
assert_eq!(
decoder
.decode_utf8::<serde_json::Value>(accepted)
.expect("a fresh value must fit after syntax rollback"),
serde_json::Value::Null
);
assert_eq!(
decoder
.session()
.input_budget()
.expect("configured input budget")
.used(),
rejected.len() + accepted.len()
);
assert_eq!(decoder.session().value_budget().used_nodes(), Some(1));
}
#[test]
fn test_budget_rejection_keeps_input_and_rolls_back_value() {
let input = br#"[null]"#;
let session = JsonDecodeSession::from_limits(
JsonDecodeLimits::<JsonResource, usize>::builder()
.max_input_bytes(input.len())
.max_nodes(1)
.build(),
);
let mut decoder = JsonDecoder::new(session);
let error = decoder
.decode_utf8::<serde_json::Value>(input)
.expect_err("the node budget must reject the document");
assert_eq!(error.kind(), JsonDecodeErrorKind::Budget);
assert_eq!(
decoder
.session()
.input_budget()
.expect("configured input budget")
.used(),
input.len()
);
assert_eq!(decoder.session().value_budget().used_nodes(), Some(0));
}
#[test]
fn test_decoder_seed_rejects_integer_outside_64_bit_range() {
let input = b"123456789012345678901234567890";
let session = JsonDecodeSession::from_limits(
JsonDecodeLimits::<JsonResource, usize>::builder()
.value_limits(
JsonValueLimits::<JsonResource, usize>::builder()
.number_bytes_limit(ResourceLimit::new(JsonResource::NumberBytes, input.len()))
.build(),
)
.build(),
);
let mut decoder = JsonDecoder::new(session);
let error = decoder
.decode_seed_utf8(IgnoreSeed, input)
.expect_err("an integer above u64::MAX must be rejected");
assert_eq!(error.kind(), JsonDecodeErrorKind::InvalidJson);
}
#[test]
fn test_point_limit_fails_before_seed_and_keeps_work_charged() {
let limits = JsonDecodeLimits::<JsonResource, usize>::builder()
.value_limits(
JsonValueLimits::<JsonResource, usize>::builder()
.structure_limits(StructureLimits::builder().nodes_limit(ResourceLimit::new(JsonResource::Nodes, 1)))
.string_bytes_limit(ResourceLimit::new(JsonResource::StringBytes, 1))
.build(),
)
.build();
let session = JsonDecodeSession::from_limits(limits);
let mut decoder = JsonDecoder::new(session);
let error = decoder
.decode_seed_utf8(PanicSeed, br#""ab""#)
.expect_err("string limit must fail");
assert_eq!(error.kind(), JsonDecodeErrorKind::Budget);
assert_eq!(decoder.session().value_budget().used_nodes(), Some(0));
}
#[test]
fn test_decoder_supports_usize_quantities() {
let limits = JsonDecodeLimits::<JsonResource, usize>::builder()
.value_limits(
JsonValueLimits::<JsonResource, usize>::builder()
.structure_limits(
StructureLimits::builder().nodes_limit(ResourceLimit::new(JsonResource::Nodes, 4_usize)),
)
.build(),
)
.build();
let session = JsonDecodeSession::<JsonResource, usize>::from_limits(limits);
let mut decoder = JsonDecoder::new(session);
let value: serde_json::Value = decoder
.decode_utf8(br#"[1,"x"]"#)
.expect("usize JSON budgets must admit a fitting document");
assert_eq!(decoder.session().value_budget().used_nodes(), Some(3));
assert!(value.is_array());
}
struct PanicSeed;
impl<'de> DeserializeSeed<'de> for PanicSeed {
type Value = ();
fn deserialize<D>(self, _: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
panic!("seed must not run before lexical admission succeeds");
}
}
struct RejectSeed;
impl<'de> DeserializeSeed<'de> for RejectSeed {
type Value = ();
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
let _ = serde_json::Value::deserialize(deserializer)?;
Err(DeError::custom("reject after JSON admission"))
}
}
struct IgnoreSeed;
impl<'de> DeserializeSeed<'de> for IgnoreSeed {
type Value = ();
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
IgnoredAny::deserialize(deserializer).map(|_| ())
}
}