|
|
|
|
@@ -0,0 +1,356 @@
|
|
|
|
|
//! Cross-format compatibility and deterministic mutation tests.
|
|
|
|
|
|
|
|
|
|
use crate::{Error, OSD};
|
|
|
|
|
use libremetaverse_types::UUID;
|
|
|
|
|
use libremetaverse_types::compat::Uri;
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use std::panic::{AssertUnwindSafe, catch_unwind};
|
|
|
|
|
use std::time::{Duration, UNIX_EPOCH};
|
|
|
|
|
|
|
|
|
|
fn variants() -> Vec<OSD> {
|
|
|
|
|
vec![
|
|
|
|
|
OSD::Undefined,
|
|
|
|
|
OSD::Boolean(true),
|
|
|
|
|
OSD::Integer(-42),
|
|
|
|
|
OSD::Real(12.5),
|
|
|
|
|
OSD::String("text".into()),
|
|
|
|
|
OSD::UUID(UUID::new_with_string("97f4aeca-88a1-42a1-b385-b97b18abb255".into()).unwrap()),
|
|
|
|
|
OSD::Date(UNIX_EPOCH + Duration::from_secs(1_705_314_645)),
|
|
|
|
|
OSD::Uri(Uri("https://example.com/path".into())),
|
|
|
|
|
OSD::Binary(vec![0, 1, 255]),
|
|
|
|
|
OSD::Map(HashMap::from([("key".into(), OSD::Integer(7))])),
|
|
|
|
|
OSD::Array(vec![OSD::String("nested".into()), OSD::Boolean(false)]),
|
|
|
|
|
OSD::LlsdXml("<string>raw XML</string>".into()),
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn every_osd_variant_has_documented_cross_format_behavior() {
|
|
|
|
|
for value in variants() {
|
|
|
|
|
if matches!(value, OSD::LlsdXml(_)) {
|
|
|
|
|
assert!(crate::binary::serialize(value.clone()).is_err());
|
|
|
|
|
assert!(crate::notation::serialize(value.clone()).is_err());
|
|
|
|
|
} else {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
crate::binary::deserialize_bytes(crate::binary::serialize(value.clone()).unwrap())
|
|
|
|
|
.unwrap(),
|
|
|
|
|
value
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
crate::notation::deserialize_string(
|
|
|
|
|
crate::notation::serialize(value.clone()).unwrap()
|
|
|
|
|
)
|
|
|
|
|
.unwrap(),
|
|
|
|
|
value
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let xml = crate::xml_codec::deserialize_bytes(
|
|
|
|
|
crate::xml_codec::serialize_bytes(value.clone()).unwrap(),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let expected_xml = if matches!(value, OSD::LlsdXml(_)) {
|
|
|
|
|
OSD::String("raw XML".into())
|
|
|
|
|
} else {
|
|
|
|
|
value.clone()
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(xml, expected_xml);
|
|
|
|
|
|
|
|
|
|
let json = crate::json_codec::deserialize_string(
|
|
|
|
|
crate::json_codec::serialize(value.clone(), Some(true)).unwrap(),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(json, expected_json(&value));
|
|
|
|
|
|
|
|
|
|
let protobuf = crate::protobuf::deserialize_bytes(
|
|
|
|
|
crate::protobuf::serialize(value.clone(), Some(false)).unwrap(),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let expected_protobuf = if matches!(value, OSD::LlsdXml(_)) {
|
|
|
|
|
OSD::Undefined
|
|
|
|
|
} else {
|
|
|
|
|
value
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(protobuf, expected_protobuf);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn every_encoder_is_deterministic_for_unordered_maps() {
|
|
|
|
|
let mut forward = HashMap::new();
|
|
|
|
|
forward.insert("a".into(), OSD::String("first".into()));
|
|
|
|
|
forward.insert("m".into(), OSD::Boolean(true));
|
|
|
|
|
forward.insert("z".into(), OSD::Integer(1));
|
|
|
|
|
let mut reverse = HashMap::new();
|
|
|
|
|
reverse.insert("z".into(), OSD::Integer(1));
|
|
|
|
|
reverse.insert("m".into(), OSD::Boolean(true));
|
|
|
|
|
reverse.insert("a".into(), OSD::String("first".into()));
|
|
|
|
|
let forward = OSD::Map(forward);
|
|
|
|
|
let reverse = OSD::Map(reverse);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
crate::binary::serialize(forward.clone()).unwrap(),
|
|
|
|
|
crate::binary::serialize(reverse.clone()).unwrap()
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
crate::notation::serialize(forward.clone()).unwrap(),
|
|
|
|
|
crate::notation::serialize(reverse.clone()).unwrap()
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
crate::xml_codec::serialize_bytes(forward.clone()).unwrap(),
|
|
|
|
|
crate::xml_codec::serialize_bytes(reverse.clone()).unwrap()
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
crate::json_codec::serialize(forward.clone(), Some(true)).unwrap(),
|
|
|
|
|
crate::json_codec::serialize(reverse.clone(), Some(true)).unwrap()
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
crate::protobuf::serialize(forward, Some(false)).unwrap(),
|
|
|
|
|
crate::protobuf::serialize(reverse, Some(false)).unwrap()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn malformed_corpora_and_focused_mutations_never_panic() {
|
|
|
|
|
for (name, seed) in hex_corpus(include_str!(
|
|
|
|
|
"../../../fuzz/corpus/binary_llsd/malformed.hex"
|
|
|
|
|
)) {
|
|
|
|
|
assert_no_panic(
|
|
|
|
|
&format!("binary corpus {name}"),
|
|
|
|
|
seed,
|
|
|
|
|
crate::binary::deserialize_bytes,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
for (name, seed) in text_corpus(include_str!(
|
|
|
|
|
"../../../fuzz/corpus/notation_llsd/malformed.txt"
|
|
|
|
|
)) {
|
|
|
|
|
assert_no_panic(&format!("notation corpus {name}"), seed, notation_bytes);
|
|
|
|
|
}
|
|
|
|
|
for (name, seed) in text_corpus(include_str!("../../../fuzz/corpus/json_osd/malformed.txt")) {
|
|
|
|
|
assert_no_panic(&format!("JSON corpus {name}"), seed, json_bytes);
|
|
|
|
|
}
|
|
|
|
|
for (name, seed) in hex_corpus(include_str!(
|
|
|
|
|
"../../../fuzz/corpus/protobuf_osd/malformed.hex"
|
|
|
|
|
)) {
|
|
|
|
|
assert_no_panic(
|
|
|
|
|
&format!("Protobuf corpus {name}"),
|
|
|
|
|
seed,
|
|
|
|
|
crate::protobuf::deserialize_bytes,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
for (name, seed) in [
|
|
|
|
|
(
|
|
|
|
|
"XML DTD",
|
|
|
|
|
include_bytes!("../../../fuzz/corpus/xml_llsd/doctype_entity.xml").as_slice(),
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"XML external entity",
|
|
|
|
|
include_bytes!("../../../fuzz/corpus/xml_llsd/external_entity.xml").as_slice(),
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"XML named entity",
|
|
|
|
|
include_bytes!("../../../fuzz/corpus/xml_llsd/unknown_entity.xml").as_slice(),
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"XML nesting",
|
|
|
|
|
include_bytes!("../../../fuzz/corpus/xml_llsd/malformed_nesting.xml").as_slice(),
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"XML truncated scalar",
|
|
|
|
|
include_bytes!("../../../fuzz/corpus/xml_llsd/truncated_scalar.xml").as_slice(),
|
|
|
|
|
),
|
|
|
|
|
] {
|
|
|
|
|
assert_no_panic(name, seed.to_vec(), crate::xml_codec::deserialize_bytes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let value = OSD::Map(HashMap::from([
|
|
|
|
|
("array".into(), OSD::Array(variants())),
|
|
|
|
|
("binary".into(), OSD::Binary(vec![0, 1, 2, 255])),
|
|
|
|
|
]));
|
|
|
|
|
let binary = crate::binary::serialize(value.clone()).unwrap_err();
|
|
|
|
|
assert!(matches!(binary, Error::Parse { .. }));
|
|
|
|
|
let fuzzable = OSD::Map(HashMap::from([
|
|
|
|
|
(
|
|
|
|
|
"array".into(),
|
|
|
|
|
OSD::Array(variants().into_iter().take(11).collect()),
|
|
|
|
|
),
|
|
|
|
|
("binary".into(), OSD::Binary(vec![0, 1, 2, 255])),
|
|
|
|
|
]));
|
|
|
|
|
mutate(
|
|
|
|
|
"binary",
|
|
|
|
|
&crate::binary::serialize(fuzzable.clone()).unwrap(),
|
|
|
|
|
crate::binary::deserialize_bytes,
|
|
|
|
|
);
|
|
|
|
|
mutate(
|
|
|
|
|
"notation",
|
|
|
|
|
&crate::notation::serialize(fuzzable.clone())
|
|
|
|
|
.unwrap()
|
|
|
|
|
.into_bytes(),
|
|
|
|
|
notation_bytes,
|
|
|
|
|
);
|
|
|
|
|
mutate(
|
|
|
|
|
"XML",
|
|
|
|
|
&crate::xml_codec::serialize_bytes(fuzzable.clone()).unwrap(),
|
|
|
|
|
crate::xml_codec::deserialize_bytes,
|
|
|
|
|
);
|
|
|
|
|
mutate(
|
|
|
|
|
"JSON",
|
|
|
|
|
&crate::json_codec::serialize(fuzzable.clone(), Some(true))
|
|
|
|
|
.unwrap()
|
|
|
|
|
.into_bytes(),
|
|
|
|
|
json_bytes,
|
|
|
|
|
);
|
|
|
|
|
mutate(
|
|
|
|
|
"Protobuf",
|
|
|
|
|
&crate::protobuf::serialize(fuzzable, Some(false)).unwrap(),
|
|
|
|
|
crate::protobuf::deserialize_bytes,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn malformed_errors_retain_positions_and_context() {
|
|
|
|
|
let cases = [
|
|
|
|
|
crate::binary::deserialize_bytes(vec![b'i']),
|
|
|
|
|
crate::notation::deserialize_string("s(4)\"x".into()),
|
|
|
|
|
crate::xml_codec::deserialize_string("<llsd><integer>1</real></llsd>".into()),
|
|
|
|
|
crate::json_codec::deserialize_string("[1,".into()),
|
|
|
|
|
crate::protobuf::deserialize_bytes(vec![0x08]),
|
|
|
|
|
];
|
|
|
|
|
for result in cases {
|
|
|
|
|
match result {
|
|
|
|
|
Err(Error::Parse { position, context }) => {
|
|
|
|
|
assert!(position < OSD::DEFAULT_MAX_BINARY_BYTES);
|
|
|
|
|
assert!(!context.is_empty());
|
|
|
|
|
}
|
|
|
|
|
result => panic!("expected contextual parse error, got {result:?}"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn expected_json(value: &OSD) -> OSD {
|
|
|
|
|
match value {
|
|
|
|
|
OSD::Undefined | OSD::LlsdXml(_) => OSD::Undefined,
|
|
|
|
|
OSD::Boolean(value) => OSD::Boolean(*value),
|
|
|
|
|
OSD::Integer(value) => OSD::Integer(*value),
|
|
|
|
|
OSD::Real(value) => OSD::Real(*value),
|
|
|
|
|
OSD::String(value) => {
|
|
|
|
|
if value.is_empty() {
|
|
|
|
|
OSD::Undefined
|
|
|
|
|
} else {
|
|
|
|
|
OSD::String(value.clone())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
OSD::UUID(value) => OSD::String(value.to_string()),
|
|
|
|
|
OSD::Date(value) => OSD::String(crate::model::format_system_time_for_codec(*value)),
|
|
|
|
|
OSD::Uri(Uri(value)) => OSD::String(crate::model::format_uri_for_codec(value)),
|
|
|
|
|
OSD::Binary(values) => OSD::Array(
|
|
|
|
|
values
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|value| OSD::Integer(i32::from(*value)))
|
|
|
|
|
.collect(),
|
|
|
|
|
),
|
|
|
|
|
OSD::Map(values) => OSD::Map(
|
|
|
|
|
values
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(key, value)| (key.clone(), expected_json(value)))
|
|
|
|
|
.collect(),
|
|
|
|
|
),
|
|
|
|
|
OSD::Array(values) => OSD::Array(values.iter().map(expected_json).collect()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn hex_corpus(input: &'static str) -> Vec<(&'static str, Vec<u8>)> {
|
|
|
|
|
input
|
|
|
|
|
.lines()
|
|
|
|
|
.filter(|line| !line.is_empty() && !line.starts_with('#'))
|
|
|
|
|
.map(|line| {
|
|
|
|
|
let (name, value) = line.split_once(':').expect("named hex corpus seed");
|
|
|
|
|
let bytes = if value.is_empty() {
|
|
|
|
|
Vec::new()
|
|
|
|
|
} else {
|
|
|
|
|
decode_hex(value)
|
|
|
|
|
};
|
|
|
|
|
(name, bytes)
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn decode_hex(value: &str) -> Vec<u8> {
|
|
|
|
|
assert_eq!(value.len() % 2, 0, "corpus hex must have byte pairs");
|
|
|
|
|
value
|
|
|
|
|
.as_bytes()
|
|
|
|
|
.chunks_exact(2)
|
|
|
|
|
.map(|pair| {
|
|
|
|
|
u8::from_str_radix(std::str::from_utf8(pair).expect("ASCII corpus hex"), 16)
|
|
|
|
|
.expect("valid corpus hex")
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn text_corpus(input: &'static str) -> Vec<(&'static str, Vec<u8>)> {
|
|
|
|
|
input
|
|
|
|
|
.lines()
|
|
|
|
|
.filter(|line| !line.is_empty() && !line.starts_with('#'))
|
|
|
|
|
.map(|line| {
|
|
|
|
|
let (name, value) = line.split_once(':').expect("named text corpus seed");
|
|
|
|
|
(name, value.as_bytes().to_vec())
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn notation_bytes(bytes: Vec<u8>) -> Result<OSD, Error> {
|
|
|
|
|
crate::notation::deserialize_string(String::from_utf8(bytes).map_err(|_| Error::Argument)?)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn json_bytes(bytes: Vec<u8>) -> Result<OSD, Error> {
|
|
|
|
|
crate::json_codec::deserialize_string(String::from_utf8(bytes).map_err(|_| Error::Argument)?)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn assert_no_panic<F>(name: &str, bytes: Vec<u8>, decoder: F)
|
|
|
|
|
where
|
|
|
|
|
F: Fn(Vec<u8>) -> Result<OSD, Error>,
|
|
|
|
|
{
|
|
|
|
|
assert!(
|
|
|
|
|
catch_unwind(AssertUnwindSafe(|| decoder(bytes))).is_ok(),
|
|
|
|
|
"{name} panicked"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn mutate<F>(name: &str, seed: &[u8], decoder: F)
|
|
|
|
|
where
|
|
|
|
|
F: Fn(Vec<u8>) -> Result<OSD, Error>,
|
|
|
|
|
{
|
|
|
|
|
let mut truncations = vec![
|
|
|
|
|
0,
|
|
|
|
|
1.min(seed.len()),
|
|
|
|
|
2.min(seed.len()),
|
|
|
|
|
seed.len() / 4,
|
|
|
|
|
seed.len() / 2,
|
|
|
|
|
seed.len() * 3 / 4,
|
|
|
|
|
seed.len().saturating_sub(2),
|
|
|
|
|
seed.len().saturating_sub(1),
|
|
|
|
|
seed.len(),
|
|
|
|
|
];
|
|
|
|
|
truncations.sort_unstable();
|
|
|
|
|
truncations.dedup();
|
|
|
|
|
for length in truncations {
|
|
|
|
|
assert_no_panic(name, seed[..length].to_vec(), &decoder);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sample the complete input at a bounded number of evenly spaced points.
|
|
|
|
|
// This covers tags, lengths, payloads and delimiters without turning this
|
|
|
|
|
// deterministic regression test into an unbounded fuzzing job in CI.
|
|
|
|
|
let step = seed.len().div_ceil(64).max(1);
|
|
|
|
|
let mut indexes = (0..seed.len()).step_by(step).collect::<Vec<_>>();
|
|
|
|
|
if let Some(last) = seed.len().checked_sub(1) {
|
|
|
|
|
indexes.push(last);
|
|
|
|
|
}
|
|
|
|
|
indexes.sort_unstable();
|
|
|
|
|
indexes.dedup();
|
|
|
|
|
for index in indexes {
|
|
|
|
|
for mask in [0x01, 0x80] {
|
|
|
|
|
let mut mutation = seed.to_owned();
|
|
|
|
|
mutation[index] ^= mask;
|
|
|
|
|
assert_no_panic(name, mutation, &decoder);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|