mod bytes;
mod collections;
mod float;
mod internet;
mod numeric;
mod regex;
mod special;
mod text;
use crate::cbor_utils::map_get;
use crate::native::core::state::MAX_DEPTH;
use crate::native::core::{EngineError, ManyState, NativeTestCase, Span, Status};
use ciborium::Value;
pub(super) fn require<'a>(schema: &'a Value, field: &str) -> Result<&'a Value, EngineError> {
map_get(schema, field).ok_or_else(|| {
EngineError::InvalidArgument(format!("schema is missing required \"{field}\" field"))
})
}
pub(crate) fn interpret_schema(
ntc: &mut NativeTestCase,
schema: &Value,
) -> Result<Value, EngineError> {
use crate::cbor_utils::as_text;
let schema_type = map_get(schema, "type").and_then(as_text).ok_or_else(|| {
EngineError::InvalidArgument("schema is missing a string \"type\" field".to_string())
})?;
let span_idx = ntc.spans.len();
let span_start = ntc.nodes.len();
let depth = ntc.span_stack.len() as u32;
let parent = ntc.span_stack.last().copied();
ntc.spans.push(Span {
start: span_start,
end: span_start,
label: schema_type.to_string(),
depth,
parent,
discarded: false,
});
ntc.span_stack.push(span_idx);
if depth + 1 > MAX_DEPTH && ntc.status.is_none() {
ntc.status = Some(Status::Invalid);
ntc.freeze();
}
let result = match schema_type {
"integer" => numeric::interpret_integer(ntc, schema),
"boolean" => numeric::interpret_boolean(ntc),
"constant" => numeric::interpret_constant(schema),
"null" => Ok(Value::Null),
"float" => float::interpret_float(ntc, schema),
"binary" => bytes::interpret_binary(ntc, schema),
"string" => text::interpret_string(ntc, schema),
"regex" => regex::interpret_regex(ntc, schema),
"tuple" => collections::interpret_tuple(ntc, schema),
"one_of" => collections::interpret_one_of(ntc, schema),
"sampled_from" => collections::interpret_sampled_from(ntc, schema),
"list" => collections::interpret_list(ntc, schema),
"dict" => collections::interpret_dict(ntc, schema),
"date" => special::interpret_date(ntc),
"time" => special::interpret_time(ntc),
"datetime" => special::interpret_datetime(ntc),
"ip_address" => special::interpret_ip_address(ntc, schema),
"uuid" => special::interpret_uuid(ntc, schema),
"domain" => internet::interpret_domain(ntc, schema),
"email" => internet::interpret_email(ntc),
"url" => internet::interpret_url(ntc),
other => Err(EngineError::InvalidArgument(format!(
"unknown schema type: {other:?}"
))),
};
ntc.span_stack.pop();
if let Some(span) = ntc.spans.get_mut(span_idx) {
span.end = ntc.nodes.len();
}
result
}
pub(crate) fn many_more(
ntc: &mut NativeTestCase,
state: &mut ManyState,
) -> Result<bool, EngineError> {
let should_continue = if state.min_size as f64 == state.max_size {
state.count < state.min_size
} else {
let forced = if state.force_stop {
Some(false)
} else if state.count < state.min_size {
Some(true)
} else if state.count as f64 >= state.max_size {
Some(false)
} else {
None
};
ntc.weighted(state.p_continue, forced)?
};
if should_continue {
state.count += 1;
}
Ok(should_continue)
}
pub(crate) fn many_reject(
ntc: &mut NativeTestCase,
state: &mut ManyState,
) -> Result<(), EngineError> {
assert!(state.count > 0);
state.count -= 1;
state.rejections += 1;
if state.rejections > std::cmp::max(3, 2 * state.count) {
if state.count < state.min_size {
ntc.status = Some(Status::Invalid);
return Err(EngineError::StopTest);
} else {
state.force_stop = true;
}
}
Ok(())
}
pub(super) fn cbor_to_i128(value: &Value) -> Result<i128, EngineError> {
match value {
Value::Integer(i) => Ok((*i).into()),
Value::Tag(2, inner) => {
let Value::Bytes(bytes) = inner.as_ref() else {
return Err(EngineError::InvalidArgument(format!(
"expected bytes inside bignum tag 2, got {inner:?}"
)));
};
let mut n = 0u128;
for b in bytes {
n = (n << 8) | (*b as u128);
}
Ok(i128::try_from(n).unwrap_or(i128::MAX))
}
Value::Tag(3, inner) => {
let Value::Bytes(bytes) = inner.as_ref() else {
return Err(EngineError::InvalidArgument(format!(
"expected bytes inside bignum tag 3, got {inner:?}"
)));
};
let mut n = 0u128;
for b in bytes {
n = (n << 8) | (*b as u128);
}
Ok(-1i128 - i128::try_from(n).unwrap_or(i128::MAX))
}
_ => Err(EngineError::InvalidArgument(format!(
"expected a CBOR integer, got {value:?}"
))),
}
}
fn bignum_overflows_i128(value: &Value) -> bool {
match value {
Value::Tag(2, inner) => {
let Value::Bytes(bytes) = inner.as_ref() else {
return false;
};
if bytes.len() > 16 {
return true;
}
if bytes.len() == 16 && bytes[0] >= 0x80 {
return true;
}
let mut n = 0u128;
for b in bytes {
n = (n << 8) | (*b as u128);
}
n > i128::MAX as u128
}
_ => false,
}
}
fn u128_to_cbor(v: u128) -> Value {
if let Ok(n) = u64::try_from(v) {
return Value::Integer(n.into());
}
let bytes = v.to_be_bytes();
let first_nonzero = bytes
.iter()
.position(|&b| b != 0)
.unwrap_or(bytes.len() - 1);
Value::Tag(2, Box::new(Value::Bytes(bytes[first_nonzero..].to_vec())))
}
fn i128_to_cbor(v: i128) -> Value {
if let Ok(n) = i64::try_from(v) {
Value::Integer(n.into())
} else if let Ok(n) = u64::try_from(v) {
Value::Integer(n.into())
} else {
crate::cbor_utils::cbor_serialize(&v)
}
}
#[cfg(test)]
#[path = "../../../tests/embedded/native/schema/mod_tests.rs"]
mod tests;