use super::*;
use crate::candid_codec as codec;
use candid as upstream;
fn limits() -> Limits {
Limits {
bytes: 1_000_000,
header: 100_000,
types: 1024,
work: 10_000_000,
allocation: 4_000_000,
}
}
fn check<T>(value: T)
where
T: Encode
+ for<'a> Decode<'a>
+ upstream::CandidType
+ for<'a> serde::Deserialize<'a>
+ PartialEq
+ std::fmt::Debug,
{
let bytes = upstream::encode_one(&value).unwrap();
assert_eq!(decode_one::<T>(&bytes, limits()).unwrap(), value);
let own = codec::encode_one(&value, 1_000_000).unwrap();
assert_eq!(decode_one::<T>(&own, limits()).unwrap(), value);
for n in 0..bytes.len() {
assert!(
decode_one::<T>(&bytes[..n], limits()).is_err(),
"accepted prefix {n}"
);
}
let mut trailing = bytes;
trailing.push(0);
assert_eq!(decode_one::<T>(&trailing, limits()), Err(Error::Trailing));
}
fn conversion<S, T>(source: S)
where
S: upstream::CandidType,
T: Encode
+ for<'a> Decode<'a>
+ upstream::CandidType
+ for<'a> serde::Deserialize<'a>
+ PartialEq
+ std::fmt::Debug,
{
let bytes = upstream::encode_one(source).unwrap();
let expected =
upstream::decode_args_with_config::<(T,)>(&bytes, &upstream::DecoderConfig::new())
.unwrap()
.0;
assert_eq!(decode_one::<T>(&bytes, limits()).unwrap(), expected);
}
#[test]
fn scalar_and_container_differential() {
check(());
check(true);
check(false);
check(u8::MAX);
check(u16::MAX);
check(u32::MAX);
check(u64::MAX);
check(i8::MIN);
check(i16::MIN);
check(i32::MIN);
check(i64::MIN);
check(-0.0f32);
check(f64::INFINITY);
check("héllo 🌍".to_owned());
check(vec![
Some((1u64, "x".to_owned())),
None,
Some((u64::MAX, "".to_owned())),
]);
check(vec![(); 100]);
check(Vec::<String>::new());
check(Some(None::<bool>));
}
#[test]
fn optional_fallback_and_promotion() {
conversion::<_, Option<u64>>(42u64);
conversion::<_, Option<u64>>(true);
conversion::<_, Option<u64>>(Some(true));
conversion::<_, Option<u64>>(None::<bool>);
conversion::<_, Option<u64>>(());
conversion::<_, Option<Option<u64>>>(Some(42u64));
conversion::<_, Option<Option<u64>>>(42u64);
conversion::<_, Option<Option<u64>>>(Some(true));
conversion::<_, Option<()>>(());
}
#[test]
fn record_extension_and_missing_optional_fields() {
conversion::<_, (u64, Option<String>)>((42u64,));
conversion::<_, (u64, Option<String>)>((42u64, Some("hello".to_owned()), vec![true, false]));
conversion::<_, (u64, ())>((42u64,));
conversion::<_, (u64, Option<String>)>((42u64, true, "extra".to_owned()));
}
#[test]
fn malformed_framing_and_values_fail_closed() {
for bytes in [
b"".as_slice(),
b"DIDL",
b"DIDL\0\x01\x7e\x02",
b"DIDL\0\x01\x71\x01\xff",
b"DIDL\x01\x6d\x05\x01\x7f",
b"DIDL\x01\x6c\x02\x01\x7f\x01\x7f\x01\x00",
] {
assert!(decode_one::<Option<bool>>(bytes, limits()).is_err());
}
assert!(decode_one::<()>(b"DIDL\x01\x6d\x05\x01\x7f", limits()).is_err());
assert_eq!(
decode_one::<u64>(&upstream::encode_one(true).unwrap(), limits()),
Err(Error::Type)
);
}
#[test]
fn caller_budgets_are_enforced() {
let bytes = upstream::encode_one(vec!["hello".to_owned(); 10]).unwrap();
let mut budget = limits();
budget.bytes = bytes.len() - 1;
assert_eq!(decode_one::<Vec<String>>(&bytes, budget), Err(Error::Limit));
let mut budget = limits();
budget.header = 4;
assert_eq!(decode_one::<Vec<String>>(&bytes, budget), Err(Error::Limit));
let mut budget = limits();
budget.types = 0;
assert_eq!(decode_one::<Vec<String>>(&bytes, budget), Err(Error::Limit));
let mut budget = limits();
budget.work = 10;
assert_eq!(decode_one::<Vec<String>>(&bytes, budget), Err(Error::Limit));
let mut budget = limits();
budget.allocation = 1;
assert_eq!(decode_one::<Vec<String>>(&bytes, budget), Err(Error::Limit));
let bomb = b"DIDL\x01\x6d\x7f\x01\x00\xff\xff\xff\xff\x0f";
assert_eq!(decode_one::<Vec<()>>(bomb, limits()), Err(Error::Limit));
}
#[test]
fn arbitrary_byte_mutations_do_not_panic() {
let original = upstream::encode_one(vec![Some((1u64, "hello".to_owned()))]).unwrap();
for position in 0..original.len() {
for replacement in [0, 1, 0x7f, 0x80, 0xff] {
let mut bytes = original.clone();
bytes[position] = replacement;
let _ = decode_one::<Vec<Option<(u64, String)>>>(&bytes, limits());
}
}
}
#[test]
fn nesting_boundary_including_skipped_and_unused_types() {
for depth in [16u8, 17] {
let mut bytes = b"DIDL".to_vec();
bytes.push(depth);
for index in 0..depth {
bytes.extend_from_slice(&[0x6d, if index + 1 == depth { 0x7f } else { index + 1 }]);
}
bytes.extend_from_slice(&[1, 0x7f]);
assert_eq!(
decode_one::<()>(&bytes, limits()),
if depth == 16 {
Ok(())
} else {
Err(Error::Limit)
}
);
bytes.truncate(bytes.len() - 2);
bytes.extend_from_slice(&[1, 0]);
bytes.extend(std::iter::repeat_n(1, usize::from(depth)));
assert_eq!(
decode_one::<Option<bool>>(&bytes, limits()),
if depth == 16 {
Ok(None)
} else {
Err(Error::Limit)
}
);
}
for depth in [16, 17] {
let mut bytes = b"DIDL\x01\x6e\x00\x01\x00".to_vec();
bytes.extend(std::iter::repeat_n(1, depth - 1));
bytes.push(0);
assert_eq!(
decode_one::<Option<bool>>(&bytes, limits()),
if depth == 16 {
Ok(None)
} else {
Err(Error::Limit)
}
);
}
}
#[test]
fn accepted_mutations_match_upstream_values() {
type Value = Vec<Option<(u64, String)>>;
let source = upstream::encode_one(vec![Some((1u64, "hello".to_owned()))]).unwrap();
for position in 0..source.len() {
for replacement in [0, 1, 2, 0x7f, 0x80, 0xff] {
let mut bytes = source.clone();
bytes[position] = replacement;
if let Ok(actual) = decode_one::<Value>(&bytes, limits()) {
let reference = upstream::decode_args_with_config::<(Value,)>(
&bytes,
&upstream::DecoderConfig::new(),
)
.unwrap();
assert_eq!(actual, reference.0, "mutation {position}/{replacement}");
}
}
}
}
#[test]
fn floating_point_payloads_preserve_bits() {
for bits in [0u64, 1, 1 << 63, 0x7ff8000000000001, u64::MAX] {
let bytes = upstream::encode_one(f64::from_bits(bits)).unwrap();
assert_eq!(decode_one::<f64>(&bytes, limits()).unwrap().to_bits(), bits);
}
}
#[test]
fn typed_tuple_requires_tuple_wire_layout() {
let mut bytes = b"DIDL\x01\x6c\x02\x00\x78\x02\x7f\x01\x00".to_vec();
bytes.extend_from_slice(&42u64.to_le_bytes());
type Pair = (u64, Option<String>);
assert!(
upstream::decode_args_with_config::<(Pair,)>(&bytes, &upstream::DecoderConfig::new())
.is_err()
);
assert_eq!(decode_one::<Pair>(&bytes, limits()), Err(Error::Type));
}
#[test]
#[ignore = "Explicit native throughput experiment; not a correctness or IC instruction gate."]
fn native_text_throughput() {
use std::{hint::black_box, time::Instant};
let reference_limits = crate::tests::reference::DecodeLimits {
reply_bytes: 1_000_000,
header_bytes: 100_000,
type_entries: 1024,
decoding_quota: 10_000_000,
skipping_quota: 10_000_000,
};
println!("payload_bytes,iterations,sample,implementation,operation,total_ns");
for length in [0, 32, 4096, 65_536] {
let text = "x".repeat(length);
let bytes = upstream::encode_one(&text).unwrap();
assert_eq!(codec::encode_one(&text, 1_000_000).unwrap(), bytes);
assert_eq!(decode_one::<String>(&bytes, limits()).unwrap(), text);
let iterations = (16_777_216 / length.max(1)).clamp(256, 10_000);
for sample in 0..5 {
for implementation in if sample % 2 == 0 { [0, 1] } else { [1, 0] } {
let start = Instant::now();
for _ in 0..iterations {
if implementation == 0 {
black_box(codec::encode_one(black_box(&text), 1_000_000).unwrap());
} else {
black_box(upstream::encode_one(black_box(&text)).unwrap());
}
}
println!(
"{length},{iterations},{sample},{implementation},encode,{}",
start.elapsed().as_nanos()
);
let start = Instant::now();
for _ in 0..iterations {
if implementation == 0 {
black_box(decode_one::<String>(black_box(&bytes), limits()).unwrap());
} else {
black_box(
crate::tests::reference::decode_one::<String>(
black_box(&bytes),
reference_limits,
)
.unwrap(),
);
}
}
println!(
"{length},{iterations},{sample},{implementation},decode,{}",
start.elapsed().as_nanos()
);
}
}
}
}
#[test]
fn result_variants_and_optional_fallback() {
check(std::result::Result::<u64, String>::Ok(42));
check(std::result::Result::<u64, String>::Err("denied".to_owned()));
check(vec![Ok(vec![1u64, 2]), Err("bad".to_owned())]);
conversion::<_, Option<std::result::Result<u64, String>>>(Ok::<u64, bool>(42));
conversion::<_, Option<std::result::Result<u64, String>>>(Err::<u64, bool>(true));
conversion::<_, Option<std::result::Result<Vec<u64>, String>>>(Ok::<Vec<bool>, String>(vec![]));
}