#![cfg(feature = "alloc")]
#[test]
fn collection_limit_is_per_collection_not_cumulative() {
let value = (vec![1u8, 2, 3], vec![4u8, 5, 6]);
let config = rustbinary::options().with_collection_limit(5);
let bytes = config.serialize(&value).unwrap();
assert_eq!(
config.deserialize::<(Vec<u8>, Vec<u8>)>(&bytes).unwrap(),
value
);
let single = vec![1u8, 2, 3, 4, 5, 6];
assert!(matches!(
config.serialize(&single),
Err(rustbinary::Error::CollectionLimit { limit: 5 })
));
}
#[test]
fn large_strings_round_trip_under_byte_limit() {
let big = "x".repeat(1_100_000); let config = rustbinary::options().with_limit(4 * 1024 * 1024);
let bytes = config.serialize(&big).unwrap();
assert_eq!(config.deserialize::<String>(&bytes).unwrap(), big);
}
#[cfg(feature = "compression")]
#[test]
fn decompression_is_bounded_even_without_a_configured_limit() {
let mut hostile = Vec::from(*b"RBZ1");
hostile.extend_from_slice(&1u16.to_le_bytes()); hostile.extend_from_slice(&1u16.to_le_bytes()); hostile.extend_from_slice(&u64::MAX.to_le_bytes()); hostile.extend_from_slice(&1u64.to_le_bytes());
for config in [
rustbinary::legacy_options().with_zstd_compression(3),
rustbinary::options()
.with_no_limit()
.with_zstd_compression(3),
] {
assert!(
matches!(
config.deserialize::<Vec<u8>>(&hostile),
Err(rustbinary::Error::SizeLimit { .. })
),
"decompression must be bounded without a configured limit"
);
}
}
#[test]
fn random_and_truncated_inputs_never_panic() {
let mut state = 0x853c_49e6_748f_ea9bu64;
let mut next = move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
for _ in 0..2_000 {
let len = (next() % 64) as usize;
let input = (0..len).map(|_| (next() & 0xff) as u8).collect::<Vec<_>>();
for cut in [0usize, len / 2, len.saturating_sub(1), len] {
assert!(
std::panic::catch_unwind(|| {
rustbinary::options().deserialize::<nextjson::Value>(&input[..cut])
})
.is_ok(),
"random input of length {len} truncated at {cut} panicked"
);
}
}
}
#[test]
fn hostile_deep_nesting_is_rejected_without_stack_overflow() {
for depth in [129usize, 1_000, 100_000] {
let mut input = vec![0x0a; depth];
input.push(0xff);
let result = std::panic::catch_unwind(|| {
rustbinary::options().deserialize::<nextjson::Value>(&input)
});
assert!(result.is_ok(), "depth {depth} caused a panic");
let error = result.unwrap().unwrap_err();
assert!(
matches!(error, rustbinary::Error::Custom(_)),
"depth {depth}: expected depth-limit error, got {error:?}"
);
}
}
#[test]
fn value_round_trips_losslessly() {
let value = nextjson::json!({
"code": 200,
"ok": true,
"nested": { "a": [1, 2.5, null], "b": false },
"list": ["x", "y"],
});
let bytes = rustbinary::options().serialize(&value).unwrap();
let decoded: nextjson::Value = rustbinary::options().deserialize(&bytes).unwrap();
assert_eq!(decoded, value);
}
#[test]
fn untagged_enum_backtracking_handles_hostile_input() {
#[derive(Debug, PartialEq, nextjson::NsonDeserialize, nextjson::NsonSerialize)]
#[njson(untagged)]
enum Shape {
Point { x: i64, y: i64 },
Label(String),
}
let point = Shape::Point { x: 1, y: 2 };
let bytes = rustbinary::options().serialize(&point).unwrap();
assert_eq!(
rustbinary::options().deserialize::<Shape>(&bytes).unwrap(),
point
);
let label = Shape::Label("hi".into());
let bytes = rustbinary::options().serialize(&label).unwrap();
assert_eq!(
rustbinary::options().deserialize::<Shape>(&bytes).unwrap(),
label
);
let hostile = rustbinary::options()
.serialize(&nextjson::json!({ "x": "not-a-number", "y": 2 }))
.unwrap();
let result = std::panic::catch_unwind(|| rustbinary::options().deserialize::<Shape>(&hostile));
assert!(result.is_ok());
assert!(result.unwrap().is_err());
for len in 0..hostile.len() {
let truncated = &hostile[..len];
assert!(
std::panic::catch_unwind(|| { rustbinary::options().deserialize::<Shape>(truncated) })
.is_ok(),
"truncation at {len} panicked"
);
}
}
#[cfg(feature = "cbor")]
#[test]
fn cbor_decode_enforces_collection_limit() {
let config = rustbinary::options()
.with_collection_limit(4)
.with_cbor_format();
let bytes = config.serialize(&vec![1u32, 2, 3, 4]).unwrap();
assert_eq!(
config.deserialize::<Vec<u32>>(&bytes).unwrap(),
vec![1, 2, 3, 4]
);
let mut hostile = vec![0x9f]; hostile.extend(std::iter::repeat_n(0x01, 5)); hostile.push(0xff);
assert!(matches!(
config.deserialize::<Vec<u32>>(&hostile),
Err(rustbinary::Error::CollectionLimit { limit: 4 })
));
let nested = [0x9f, 0x01, 0x9f, 0x01, 0x01, 0x01, 0x01, 0x01, 0xff, 0xff];
assert!(matches!(
config.deserialize::<nextjson::Value>(&nested),
Err(rustbinary::Error::CollectionLimit { limit: 4 })
));
}
#[test]
fn i128_min_encodes_without_panic() {
let bytes = rustbinary::options().serialize(&i128::MIN).unwrap();
assert_eq!(
rustbinary::options().deserialize::<i128>(&bytes).unwrap(),
i128::MIN
);
}
#[test]
fn f32_typed_round_trip_is_lossless() {
for value in [
1.5f32,
-0.0,
f32::MAX,
f32::MIN_POSITIVE,
std::f32::consts::PI,
] {
let bytes = rustbinary::options().serialize(&value).unwrap();
let decoded: f32 = rustbinary::options().deserialize(&bytes).unwrap();
assert_eq!(decoded.to_bits(), value.to_bits(), "f32 {value} lost bits");
}
}
#[test]
fn option_round_trips_through_null_tag() {
let none: Option<u32> = None;
let some: Option<u32> = Some(42);
assert_eq!(
rustbinary::options()
.deserialize::<Option<u32>>(&rustbinary::options().serialize(&none).unwrap())
.unwrap(),
None
);
assert_eq!(
rustbinary::options()
.deserialize::<Option<u32>>(&rustbinary::options().serialize(&some).unwrap())
.unwrap(),
Some(42)
);
}
#[test]
fn nextjson_bytes_encodes_as_array_spelling() {
let value = nextjson::Bytes(b"\x00\x01binary");
let encoded = rustbinary::options().serialize(&value).unwrap();
assert_eq!(encoded[0], 0x0a);
assert_eq!(*encoded.last().unwrap(), 0xff);
assert_eq!(
rustbinary::options()
.serialize(&value.as_bytes().to_vec())
.unwrap(),
encoded
);
assert_eq!(
rustbinary::options()
.deserialize::<Vec<u8>>(&encoded)
.unwrap(),
b"\x00\x01binary"
);
}