1use std::{cmp::Ordering, fmt::Debug};
2
3use radixdb_plugin_abi as abi;
4use sha2::{Digest, Sha256};
5
6use crate::{
7 BoundedBytes, BoundedText, CodecReader, CodecWriter, PluginError, PluginResult, RadixType,
8};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct TypeTestReport {
12 pub corpus_values: usize,
13 pub codec_vectors: Vec<Vec<u8>>,
14 pub hash_vectors: Vec<[u8; 32]>,
15}
16
17pub trait OperatorClassKey {
20 fn key_compare(&self, other: &Self) -> Ordering;
21}
22
23macro_rules! ordered_key {
24 ($($type:ty),+ $(,)?) => {
25 $(
26 impl OperatorClassKey for $type {
27 fn key_compare(&self, other: &Self) -> Ordering {
28 self.cmp(other)
29 }
30 }
31 )+
32 };
33}
34
35ordered_key!(i8, i16, i32, i64, u8, u16, u32, u64, bool);
36
37impl OperatorClassKey for f32 {
38 fn key_compare(&self, other: &Self) -> Ordering {
39 self.total_cmp(other)
40 }
41}
42
43impl OperatorClassKey for f64 {
44 fn key_compare(&self, other: &Self) -> Ordering {
45 self.total_cmp(other)
46 }
47}
48
49impl<const MAX: usize> OperatorClassKey for BoundedBytes<MAX> {
50 fn key_compare(&self, other: &Self) -> Ordering {
51 self.as_slice().cmp(other.as_slice())
52 }
53}
54
55impl<const MAX: usize> OperatorClassKey for BoundedText<MAX> {
56 fn key_compare(&self, other: &Self) -> Ordering {
57 self.as_str().cmp(other.as_str())
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct TestValue {
63 type_ref: abi::RadixAbiTypeRefV1,
64 bytes: Vec<u8>,
65 is_null: bool,
66}
67
68impl TestValue {
69 pub fn integer(value: i64) -> Self {
70 Self {
71 type_ref: abi::RadixAbiTypeRefV1::builtin(abi::RADIX_BUILTIN_INTEGER),
72 bytes: value.to_le_bytes().to_vec(),
73 is_null: false,
74 }
75 }
76
77 pub fn float(value: f64) -> Self {
78 Self {
79 type_ref: abi::RadixAbiTypeRefV1::builtin(abi::RADIX_BUILTIN_FLOAT),
80 bytes: value.to_bits().to_le_bytes().to_vec(),
81 is_null: false,
82 }
83 }
84
85 pub fn boolean(value: bool) -> Self {
86 Self {
87 type_ref: abi::RadixAbiTypeRefV1::builtin(abi::RADIX_BUILTIN_BOOLEAN),
88 bytes: vec![u8::from(value)],
89 is_null: false,
90 }
91 }
92
93 pub fn external<T: RadixType>(
94 package: &'static abi::RadixPluginDescriptorV1,
95 value: &T,
96 ) -> PluginResult<Self> {
97 let descriptor = find_type(package, T::LOCAL_ID)?;
98 if descriptor.codec_version != T::CODEC_VERSION {
99 return Err(PluginError::invalid_input(
100 "test type codec differs from package descriptor",
101 ));
102 }
103 Ok(Self {
104 type_ref: abi::RadixAbiTypeRefV1::external(
105 descriptor.object_id,
106 descriptor.codec_version,
107 ),
108 bytes: encode(value)?,
109 is_null: false,
110 })
111 }
112
113 pub fn null_like(value: &Self) -> Self {
114 Self {
115 type_ref: value.type_ref,
116 bytes: Vec::new(),
117 is_null: true,
118 }
119 }
120
121 pub fn type_ref(&self) -> abi::RadixAbiTypeRefV1 {
122 self.type_ref
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct TestCallOptions {
128 pub cancelled: bool,
129 pub deadline_expired: bool,
130 pub max_output_bytes: u32,
131 pub max_work_units: u32,
132}
133
134impl Default for TestCallOptions {
135 fn default() -> Self {
136 Self {
137 cancelled: false,
138 deadline_expired: false,
139 max_output_bytes: 1024 * 1024,
140 max_work_units: 1024 * 1024,
141 }
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct TestOutput {
147 pub is_null: bool,
148 pub bytes: Vec<u8>,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct TestDiagnostic {
153 pub category: u32,
154 pub status: abi::RadixAbiStatusV1,
155 pub detail: String,
156 pub field: String,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct TestCallReport {
161 pub status: abi::RadixAbiStatusV1,
162 pub outputs: Vec<TestOutput>,
163 pub diagnostics: Vec<TestDiagnostic>,
164 pub work_charged: u32,
165 pub finished: bool,
166}
167
168pub fn validate_descriptor_graph(
169 package: &'static abi::RadixPluginDescriptorV1,
170) -> PluginResult<()> {
171 abi::validate_package_descriptor_shallow(package).map_err(validation_error)?;
172 unsafe {
175 for item in descriptor_slice(package.types, package.type_count) {
176 abi::validate_external_type_descriptor(item).map_err(validation_error)?;
177 }
178 for item in descriptor_slice(package.functions, package.function_count) {
179 abi::validate_scalar_function_descriptor(item).map_err(validation_error)?;
180 for argument in descriptor_slice(item.arguments, item.argument_count) {
181 abi::validate_type_ref(argument).map_err(validation_error)?;
182 }
183 }
184 for item in descriptor_slice(package.operators, package.operator_count) {
185 abi::validate_operator_descriptor(item).map_err(validation_error)?;
186 }
187 for item in descriptor_slice(package.operator_classes, package.operator_class_count) {
188 abi::validate_operator_class_descriptor(item).map_err(validation_error)?;
189 }
190 for item in descriptor_slice(package.planner_support, package.planner_support_count) {
191 abi::validate_planner_support_descriptor(item).map_err(validation_error)?;
192 }
193 }
194 Ok(())
195}
196
197pub fn invoke_scalar(
198 package: &'static abi::RadixPluginDescriptorV1,
199 local_id: &str,
200 arguments: &[TestValue],
201 options: TestCallOptions,
202) -> PluginResult<TestCallReport> {
203 validate_descriptor_graph(package)?;
204 let function = find_function(package, local_id)?;
205 if arguments.len() != function.argument_count as usize {
206 return Err(PluginError::invalid_input(
207 "test scalar argument count mismatch",
208 ));
209 }
210 let expected = unsafe { descriptor_slice(function.arguments, function.argument_count) };
211 for (argument, expected) in arguments.iter().zip(expected) {
212 if argument.type_ref != *expected {
213 return Err(PluginError::invalid_input(
214 "test scalar argument type mismatch",
215 ));
216 }
217 }
218 let raw_arguments = arguments.iter().map(raw_value).collect::<Vec<_>>();
219 let callback = function
220 .scalar
221 .ok_or_else(|| PluginError::internal("scalar descriptor has no callback"))?;
222 let mut state = TestHostState::new(options);
223 let diagnostic_sink = state.diagnostic_sink();
224 let context = state.call_context(&diagnostic_sink, function.max_output_bytes);
225 let result_builder = state.result_builder(function.max_output_bytes, 1);
226 let argument_pointer = if raw_arguments.is_empty() {
227 std::ptr::null()
228 } else {
229 raw_arguments.as_ptr()
230 };
231 let status = unsafe {
232 callback(
233 &context,
234 argument_pointer,
235 raw_arguments.len() as u32,
236 &result_builder,
237 )
238 };
239 Ok(state.report(status))
240}
241
242pub fn invoke_batch(
243 package: &'static abi::RadixPluginDescriptorV1,
244 local_id: &str,
245 rows: &[Vec<TestValue>],
246 options: TestCallOptions,
247) -> PluginResult<TestCallReport> {
248 validate_descriptor_graph(package)?;
249 let function = find_function(package, local_id)?;
250 let callback = function
251 .batch
252 .ok_or_else(|| PluginError::invalid_input("function has no batch adapter"))?;
253 if rows.len() > u32::MAX as usize
254 || rows
255 .iter()
256 .any(|row| row.len() != function.argument_count as usize)
257 {
258 return Err(PluginError::invalid_input("test batch shape mismatch"));
259 }
260 let expected = unsafe { descriptor_slice(function.arguments, function.argument_count) };
261 for row in rows {
262 for (value, expected) in row.iter().zip(expected) {
263 if value.type_ref != *expected {
264 return Err(PluginError::invalid_input("test batch type mismatch"));
265 }
266 }
267 }
268 let columns = expected
269 .iter()
270 .enumerate()
271 .map(|(index, type_ref)| {
272 let values = rows.iter().map(|row| &row[index]).collect::<Vec<_>>();
273 OwnedColumn::new(package, *type_ref, &values)
274 })
275 .collect::<PluginResult<Vec<_>>>()?;
276 let raw_columns = columns
277 .iter()
278 .map(|column| column.as_abi(rows.len() as u32))
279 .collect::<Vec<_>>();
280 let batch = abi::RadixAbiBatchViewV1 {
281 header: abi::RadixAbiHeaderV1::new::<abi::RadixAbiBatchViewV1>(0),
282 row_count: rows.len() as u32,
283 column_count: raw_columns.len() as u32,
284 columns: if raw_columns.is_empty() {
285 std::ptr::null()
286 } else {
287 raw_columns.as_ptr()
288 },
289 };
290 unsafe { abi::validate_batch_columns(&batch) }.map_err(validation_error)?;
291 let mut state = TestHostState::new(options);
292 let diagnostic_sink = state.diagnostic_sink();
293 let batch_output_bytes = function.max_output_bytes.saturating_mul(rows.len() as u32);
294 let context = state.call_context(&diagnostic_sink, batch_output_bytes);
295 let result_builder = state.result_builder(batch_output_bytes, rows.len() as u32);
296 let status = unsafe { callback(&context, &batch, &result_builder) };
297 Ok(state.report(status))
298}
299
300pub fn invoke_planner(
301 package: &'static abi::RadixPluginDescriptorV1,
302 local_id: &str,
303 predicate: &[u8],
304 options: TestCallOptions,
305) -> PluginResult<TestCallReport> {
306 validate_descriptor_graph(package)?;
307 let support = find_planner_support(package, local_id)?;
308 let callback = support
309 .callback
310 .ok_or_else(|| PluginError::internal("planner descriptor has no callback"))?;
311 let mut state = TestHostState::new(options);
312 let diagnostic_sink = state.diagnostic_sink();
313 let context = state.call_context(&diagnostic_sink, support.max_output_bytes);
314 let result_builder = state.result_builder(
315 support.max_output_bytes,
316 support.max_spans.saturating_add(1),
317 );
318 let predicate = abi::RadixAbiSliceV1 {
319 ptr: if predicate.is_empty() {
320 std::ptr::null()
321 } else {
322 predicate.as_ptr()
323 },
324 len: predicate.len() as u32,
325 reserved: 0,
326 };
327 let status = unsafe { callback(&context, predicate, &result_builder) };
328 Ok(state.report(status))
329}
330
331pub fn planner_recheck_policy(
332 package: &'static abi::RadixPluginDescriptorV1,
333 local_id: &str,
334) -> PluginResult<u16> {
335 validate_descriptor_graph(package)?;
336 Ok(find_planner_support(package, local_id)?.recheck_policy)
337}
338
339pub fn decode_candidate_spans(report: &TestCallReport) -> PluginResult<Vec<crate::CandidateSpan>> {
340 report
341 .outputs
342 .iter()
343 .filter(|item| item.bytes.first() == Some(&1))
344 .map(|item| {
345 if item.is_null || item.bytes.len() < 12 || item.bytes[..4] != [1, 0, 0, 0] {
346 return Err(PluginError::invalid_input(
347 "planner output is not a candidate span",
348 ));
349 }
350 let start_len =
351 u32::from_le_bytes(item.bytes[4..8].try_into().expect("fixed width")) as usize;
352 let end_len =
353 u32::from_le_bytes(item.bytes[8..12].try_into().expect("fixed width")) as usize;
354 let split = 12_usize
355 .checked_add(start_len)
356 .ok_or_else(|| PluginError::invalid_input("candidate span length overflow"))?;
357 let end = split
358 .checked_add(end_len)
359 .ok_or_else(|| PluginError::invalid_input("candidate span length overflow"))?;
360 if end != item.bytes.len() {
361 return Err(PluginError::invalid_input(
362 "candidate span has invalid lengths",
363 ));
364 }
365 Ok(crate::CandidateSpan {
366 start: item.bytes[12..split].to_vec(),
367 end: item.bytes[split..end].to_vec(),
368 })
369 })
370 .collect()
371}
372
373pub fn check_type<T>() -> PluginResult<TypeTestReport>
374where
375 T: RadixType + Debug,
376{
377 let corpus = T::test_corpus();
378 if corpus.is_empty() {
379 return Err(PluginError::invalid_input(
380 "type test corpus must not be empty",
381 ));
382 }
383 let mut codec_vectors = Vec::with_capacity(corpus.len());
384 for value in &corpus {
385 let bytes = encode(value)?;
386 let decoded = decode::<T>(&bytes)?;
387 let second = encode(&decoded)?;
388 if bytes != second {
389 return Err(PluginError::domain(
390 "codec is not canonical after roundtrip",
391 ));
392 }
393 codec_vectors.push(bytes);
394 }
395
396 let mut hashes = vec![None; corpus.len()];
397 for (index, value) in corpus.iter().enumerate() {
398 let mut components = Vec::new();
399 let mut sink = crate::HashSink::for_testing(&mut components);
400 if let Some(result) = T::semantic_hash(value, &mut sink) {
401 result?;
402 hashes[index] = Some(hash_components(&components));
403 }
404 }
405
406 if T::CAPABILITIES & radixdb_plugin_abi::RADIX_TYPE_CAP_EQUALITY != 0 {
407 for (left_index, left) in corpus.iter().enumerate() {
408 if T::semantic_equal(left, left) != Some(true) {
409 return Err(PluginError::domain("equality is not reflexive"));
410 }
411 for (right_index, right) in corpus.iter().enumerate() {
412 let lr = T::semantic_equal(left, right).ok_or_else(|| {
413 PluginError::internal("equality capability has no safe callback")
414 })?;
415 let rl = T::semantic_equal(right, left).ok_or_else(|| {
416 PluginError::internal("equality capability has no safe callback")
417 })?;
418 if lr != rl {
419 return Err(PluginError::domain("equality is not symmetric"));
420 }
421 if lr && hashes[left_index] != hashes[right_index] {
422 return Err(PluginError::domain(
423 "equal values produce different semantic hash components",
424 ));
425 }
426 for third in &corpus {
427 if lr
428 && T::semantic_equal(right, third) == Some(true)
429 && T::semantic_equal(left, third) != Some(true)
430 {
431 return Err(PluginError::domain("equality is not transitive"));
432 }
433 }
434 }
435 }
436 }
437
438 if T::CAPABILITIES & radixdb_plugin_abi::RADIX_TYPE_CAP_ORDERING != 0 {
439 for left in &corpus {
440 if T::semantic_compare(left, left) != Some(Ordering::Equal) {
441 return Err(PluginError::domain("ordering is not reflexive"));
442 }
443 for right in &corpus {
444 let lr = T::semantic_compare(left, right)
445 .ok_or_else(|| PluginError::internal("missing ordering callback"))?;
446 let rl = T::semantic_compare(right, left)
447 .ok_or_else(|| PluginError::internal("missing ordering callback"))?;
448 if lr != rl.reverse() {
449 return Err(PluginError::domain("ordering is not antisymmetric"));
450 }
451 if T::CAPABILITIES & radixdb_plugin_abi::RADIX_TYPE_CAP_EQUALITY != 0
452 && (lr == Ordering::Equal) != (T::semantic_equal(left, right) == Some(true))
453 {
454 return Err(PluginError::domain(
455 "ordering equality disagrees with equality callback",
456 ));
457 }
458 for third in &corpus {
459 if lr != Ordering::Greater
460 && T::semantic_compare(right, third) != Some(Ordering::Greater)
461 && T::semantic_compare(left, third) == Some(Ordering::Greater)
462 {
463 return Err(PluginError::domain("ordering is not transitive"));
464 }
465 }
466 }
467 }
468 }
469
470 Ok(TypeTestReport {
471 corpus_values: corpus.len(),
472 codec_vectors,
473 hash_vectors: hashes.into_iter().flatten().collect(),
474 })
475}
476
477pub fn check_btree_operator_class<T, K>(encode_key: fn(T) -> PluginResult<K>) -> PluginResult<()>
480where
481 T: RadixType + Debug,
482 K: OperatorClassKey,
483{
484 let report = check_type::<T>()?;
485 if T::CAPABILITIES & abi::RADIX_TYPE_CAP_EQUALITY == 0
486 || T::CAPABILITIES & abi::RADIX_TYPE_CAP_ORDERING == 0
487 {
488 return Err(PluginError::domain(
489 "B-tree operator class requires equality and total ordering",
490 ));
491 }
492 let input = T::test_corpus()
493 .iter()
494 .map(encode)
495 .collect::<PluginResult<Vec<_>>>()?;
496 if input.len() != report.corpus_values {
497 return Err(PluginError::internal(
498 "operator-class corpus changed between law checks",
499 ));
500 }
501 let keys = input
502 .iter()
503 .map(|bytes| decode::<T>(bytes).and_then(encode_key))
504 .collect::<PluginResult<Vec<_>>>()?;
505 for (left_index, left_bytes) in input.iter().enumerate() {
506 for (right_index, right_bytes) in input.iter().enumerate() {
507 let left = decode::<T>(left_bytes)?;
508 let right = decode::<T>(right_bytes)?;
509 let semantic_order = T::semantic_compare(&left, &right)
510 .ok_or_else(|| PluginError::internal("missing ordering callback"))?;
511 let semantic_equal = T::semantic_equal(&left, &right)
512 .ok_or_else(|| PluginError::internal("missing equality callback"))?;
513 let key_order = keys[left_index].key_compare(&keys[right_index]);
514 if key_order != semantic_order || (key_order == Ordering::Equal) != semantic_equal {
515 return Err(PluginError::domain(
516 "B-tree key encoder does not preserve semantic equality and total order",
517 ));
518 }
519 }
520 }
521 Ok(())
522}
523
524pub fn check_hash_operator_class<T>() -> PluginResult<()>
527where
528 T: RadixType + Debug,
529{
530 let report = check_type::<T>()?;
531 if T::CAPABILITIES & abi::RADIX_TYPE_CAP_EQUALITY == 0
532 || T::CAPABILITIES & abi::RADIX_TYPE_CAP_HASH == 0
533 || report.hash_vectors.len() != report.corpus_values
534 {
535 return Err(PluginError::domain(
536 "hash operator class requires equality and semantic hash components",
537 ));
538 }
539 Ok(())
540}
541
542pub fn check_bitmap_operator_class<T, K>(encode_key: fn(T) -> PluginResult<K>) -> PluginResult<()>
545where
546 T: RadixType + Debug,
547 K: OperatorClassKey,
548{
549 let _ = check_type::<T>()?;
550 if T::CAPABILITIES & abi::RADIX_TYPE_CAP_EQUALITY == 0 {
551 return Err(PluginError::domain(
552 "bitmap operator class requires equality",
553 ));
554 }
555 let input = T::test_corpus()
556 .iter()
557 .map(encode)
558 .collect::<PluginResult<Vec<_>>>()?;
559 let keys = input
560 .iter()
561 .map(|bytes| decode::<T>(bytes).and_then(encode_key))
562 .collect::<PluginResult<Vec<_>>>()?;
563 for (left_index, left_bytes) in input.iter().enumerate() {
564 for (right_index, right_bytes) in input.iter().enumerate() {
565 let left = decode::<T>(left_bytes)?;
566 let right = decode::<T>(right_bytes)?;
567 let semantic_equal = T::semantic_equal(&left, &right)
568 .ok_or_else(|| PluginError::internal("missing equality callback"))?;
569 let key_equal = keys[left_index].key_compare(&keys[right_index]) == Ordering::Equal;
570 if key_equal != semantic_equal {
571 return Err(PluginError::domain(
572 "bitmap key encoder does not preserve semantic equality",
573 ));
574 }
575 }
576 }
577 Ok(())
578}
579
580pub fn check_operator_class_strategies<T>(
583 package: &'static abi::RadixPluginDescriptorV1,
584 local_id: &str,
585) -> PluginResult<()>
586where
587 T: RadixType + Debug,
588{
589 validate_descriptor_graph(package)?;
590 let class = find_operator_class(package, local_id)?;
591 let external_type = find_type(package, T::LOCAL_ID)?;
592 let expected_type =
593 abi::RadixAbiTypeRefV1::external(external_type.object_id, external_type.codec_version);
594 if class.input_type != expected_type {
595 return Err(PluginError::domain(
596 "operator-class input differs from its tested external type",
597 ));
598 }
599 let strategies = unsafe { descriptor_slice(class.strategies, class.strategy_count) };
600 let corpus = T::test_corpus();
601 let values = corpus
602 .iter()
603 .map(|value| TestValue::external(package, value))
604 .collect::<PluginResult<Vec<_>>>()?;
605 let encoded = corpus
606 .iter()
607 .map(encode)
608 .collect::<PluginResult<Vec<_>>>()?;
609 for (left_index, left_bytes) in encoded.iter().enumerate() {
610 for (right_index, right_bytes) in encoded.iter().enumerate() {
611 let left = decode::<T>(left_bytes)?;
612 let right = decode::<T>(right_bytes)?;
613 let equal = T::semantic_equal(&left, &right)
614 .ok_or_else(|| PluginError::internal("missing equality callback"))?;
615 let order = T::semantic_compare(&left, &right);
616 for strategy in strategies {
617 let expected = match (class.access_method, strategy.slot) {
618 (abi::RADIX_ACCESS_METHOD_BTREE, 1) => order == Some(Ordering::Less),
619 (abi::RADIX_ACCESS_METHOD_BTREE, 2) => {
620 order.is_some_and(|value| value != Ordering::Greater)
621 }
622 (abi::RADIX_ACCESS_METHOD_BTREE, 3)
623 | (abi::RADIX_ACCESS_METHOD_HASH, 1)
624 | (abi::RADIX_ACCESS_METHOD_BITMAP, 1) => equal,
625 (abi::RADIX_ACCESS_METHOD_BTREE, 4) => {
626 order.is_some_and(|value| value != Ordering::Less)
627 }
628 (abi::RADIX_ACCESS_METHOD_BTREE, 5) => order == Some(Ordering::Greater),
629 _ => {
630 return Err(PluginError::domain(
631 "operator class has an unsupported strategy slot",
632 ));
633 }
634 };
635 let actual = invoke_boolean_operator_by_id(
636 package,
637 strategy.object_id,
638 &values[left_index],
639 &values[right_index],
640 )?;
641 if actual != expected {
642 return Err(PluginError::domain(
643 "operator-class strategy disagrees with type semantics",
644 ));
645 }
646 }
647 }
648 }
649 Ok(())
650}
651
652pub fn golden_vectors<T: RadixType + Debug>() -> PluginResult<Vec<Vec<u8>>> {
653 Ok(check_type::<T>()?.codec_vectors)
654}
655
656pub fn fuzz_malformed_external_bytes<T: RadixType>(inputs: &[&[u8]]) -> usize {
657 inputs
658 .iter()
659 .filter(|bytes| decode::<T>(bytes).is_err())
660 .count()
661}
662
663fn encode<T: RadixType>(value: &T) -> PluginResult<Vec<u8>> {
664 let mut output = CodecWriter::new(T::MAX_BYTES as usize);
665 value.encode(&mut output)?;
666 let bytes = output.into_bytes();
667 if T::STORAGE_KIND == abi::RADIX_EXTERNAL_STORAGE_FIXED
668 && bytes.len() != T::FIXED_BYTES as usize
669 {
670 return Err(PluginError::domain(
671 "fixed codec corpus value has the wrong width",
672 ));
673 }
674 Ok(bytes)
675}
676
677fn decode<T: RadixType>(bytes: &[u8]) -> PluginResult<T> {
678 if T::STORAGE_KIND == abi::RADIX_EXTERNAL_STORAGE_FIXED
679 && bytes.len() != T::FIXED_BYTES as usize
680 {
681 return Err(PluginError::invalid_input(
682 "fixed codec input has the wrong width",
683 ));
684 }
685 let mut input = CodecReader::new(bytes);
686 let value = T::decode(&mut input)?;
687 input.finish()?;
688 Ok(value)
689}
690
691pub(crate) fn hash_components(components: &[(u16, Vec<u8>)]) -> [u8; 32] {
692 let mut digest = Sha256::new();
693 for (kind, bytes) in components {
694 digest.update(kind.to_le_bytes());
695 digest.update((bytes.len() as u32).to_le_bytes());
696 digest.update(bytes);
697 }
698 digest.finalize().into()
699}
700
701fn validation_error(error: abi::RadixAbiValidationError) -> PluginError {
702 PluginError::invalid_input(format!("invalid generated ABI descriptor: {error:?}"))
703}
704
705unsafe fn descriptor_slice<'a, T>(pointer: *const T, count: u32) -> &'a [T] {
706 if count == 0 {
707 &[]
708 } else {
709 unsafe { std::slice::from_raw_parts(pointer, count as usize) }
711 }
712}
713
714fn abi_text(value: abi::RadixAbiStringV1) -> PluginResult<&'static str> {
715 if value.len == 0 || value.ptr.is_null() {
716 return Err(PluginError::invalid_input(
717 "empty generated descriptor name",
718 ));
719 }
720 let bytes = unsafe { std::slice::from_raw_parts(value.ptr, value.len as usize) };
723 std::str::from_utf8(bytes)
724 .map_err(|_| PluginError::invalid_input("generated descriptor name is not UTF-8"))
725}
726
727fn find_type(
728 package: &'static abi::RadixPluginDescriptorV1,
729 local_id: &str,
730) -> PluginResult<&'static abi::RadixAbiExternalTypeDescriptorV1> {
731 validate_descriptor_graph(package)?;
732 let types = unsafe { descriptor_slice(package.types, package.type_count) };
733 types
734 .iter()
735 .find(|descriptor| abi_text(descriptor.local_id) == Ok(local_id))
736 .ok_or_else(|| PluginError::invalid_input("unknown test external type"))
737}
738
739fn find_function(
740 package: &'static abi::RadixPluginDescriptorV1,
741 local_id: &str,
742) -> PluginResult<&'static abi::RadixAbiScalarFunctionDescriptorV1> {
743 let functions = unsafe { descriptor_slice(package.functions, package.function_count) };
744 functions
745 .iter()
746 .find(|descriptor| abi_text(descriptor.local_id) == Ok(local_id))
747 .ok_or_else(|| PluginError::invalid_input("unknown test scalar function"))
748}
749
750fn find_function_by_id(
751 package: &'static abi::RadixPluginDescriptorV1,
752 object_id: [u8; 16],
753) -> PluginResult<&'static abi::RadixAbiScalarFunctionDescriptorV1> {
754 let functions = unsafe { descriptor_slice(package.functions, package.function_count) };
755 functions
756 .iter()
757 .find(|descriptor| descriptor.object_id == object_id)
758 .ok_or_else(|| PluginError::invalid_input("unknown test scalar function identity"))
759}
760
761fn find_operator_by_id(
762 package: &'static abi::RadixPluginDescriptorV1,
763 object_id: [u8; 16],
764) -> PluginResult<&'static abi::RadixAbiOperatorDescriptorV1> {
765 let operators = unsafe { descriptor_slice(package.operators, package.operator_count) };
766 operators
767 .iter()
768 .find(|descriptor| descriptor.object_id == object_id)
769 .ok_or_else(|| PluginError::invalid_input("unknown test operator identity"))
770}
771
772fn find_operator_class(
773 package: &'static abi::RadixPluginDescriptorV1,
774 local_id: &str,
775) -> PluginResult<&'static abi::RadixAbiOperatorClassDescriptorV1> {
776 let classes =
777 unsafe { descriptor_slice(package.operator_classes, package.operator_class_count) };
778 classes
779 .iter()
780 .find(|descriptor| abi_text(descriptor.local_id) == Ok(local_id))
781 .ok_or_else(|| PluginError::invalid_input("unknown test operator class"))
782}
783
784fn invoke_boolean_operator_by_id(
785 package: &'static abi::RadixPluginDescriptorV1,
786 object_id: [u8; 16],
787 left: &TestValue,
788 right: &TestValue,
789) -> PluginResult<bool> {
790 let operator = find_operator_by_id(package, object_id)?;
791 let function = find_function_by_id(package, operator.function_id)?;
792 let report = invoke_scalar(
793 package,
794 abi_text(function.local_id)?,
795 &[left.clone(), right.clone()],
796 TestCallOptions::default(),
797 )?;
798 if report.status != abi::RADIX_STATUS_OK
799 || !report.finished
800 || report.outputs.len() != 1
801 || report.outputs[0].is_null
802 {
803 return Err(PluginError::domain(
804 "operator-class strategy did not return one BOOLEAN result",
805 ));
806 }
807 match report.outputs[0].bytes.as_slice() {
808 [0] => Ok(false),
809 [1] => Ok(true),
810 _ => Err(PluginError::domain(
811 "operator-class strategy returned a malformed BOOLEAN",
812 )),
813 }
814}
815
816fn find_planner_support(
817 package: &'static abi::RadixPluginDescriptorV1,
818 local_id: &str,
819) -> PluginResult<&'static abi::RadixAbiPlannerSupportDescriptorV1> {
820 let supports =
821 unsafe { descriptor_slice(package.planner_support, package.planner_support_count) };
822 supports
823 .iter()
824 .find(|descriptor| abi_text(descriptor.local_id) == Ok(local_id))
825 .ok_or_else(|| PluginError::invalid_input("unknown test planner support"))
826}
827
828fn raw_value(value: &TestValue) -> abi::RadixAbiValueV1 {
829 let mut inline_bytes = [0; 16];
830 let fixed_builtin = value.type_ref.kind == abi::RADIX_TYPE_REF_BUILTIN
831 && matches!(
832 value.type_ref.builtin_tag,
833 abi::RADIX_BUILTIN_INTEGER | abi::RADIX_BUILTIN_FLOAT | abi::RADIX_BUILTIN_BOOLEAN
834 );
835 if fixed_builtin && !value.is_null {
836 inline_bytes[..value.bytes.len()].copy_from_slice(&value.bytes);
837 }
838 abi::RadixAbiValueV1 {
839 type_ref: value.type_ref,
840 flags: if value.is_null {
841 abi::RADIX_VALUE_FLAG_NULL
842 } else {
843 0
844 },
845 reserved: 0,
846 inline_bytes,
847 borrowed_bytes: if fixed_builtin || value.is_null {
848 abi::RadixAbiSliceV1::EMPTY
849 } else {
850 abi::RadixAbiSliceV1 {
851 ptr: value.bytes.as_ptr(),
852 len: value.bytes.len() as u32,
853 reserved: 0,
854 }
855 },
856 }
857}
858
859enum ColumnStorage {
860 Aligned(Vec<u64>),
861 Bytes(Vec<u8>),
862}
863
864impl ColumnStorage {
865 fn bytes(&self) -> (*const u8, usize) {
866 match self {
867 Self::Aligned(words) => (words.as_ptr().cast(), words.len() * 8),
868 Self::Bytes(bytes) => (bytes.as_ptr(), bytes.len()),
869 }
870 }
871}
872
873struct OwnedColumn {
874 type_ref: abi::RadixAbiTypeRefV1,
875 layout: u16,
876 element_width: u16,
877 alignment: u16,
878 stride: u32,
879 null_bitmap: Vec<u8>,
880 storage: ColumnStorage,
881 offsets: Vec<u32>,
882}
883
884impl OwnedColumn {
885 fn new(
886 package: &'static abi::RadixPluginDescriptorV1,
887 type_ref: abi::RadixAbiTypeRefV1,
888 values: &[&TestValue],
889 ) -> PluginResult<Self> {
890 let fixed_width = if type_ref.kind == abi::RADIX_TYPE_REF_BUILTIN {
891 match type_ref.builtin_tag {
892 abi::RADIX_BUILTIN_INTEGER | abi::RADIX_BUILTIN_FLOAT => Some(8_usize),
893 abi::RADIX_BUILTIN_BOOLEAN => Some(1_usize),
894 _ => None,
895 }
896 } else {
897 let types = unsafe { descriptor_slice(package.types, package.type_count) };
898 types
899 .iter()
900 .find(|descriptor| descriptor.object_id == type_ref.object_id)
901 .and_then(|descriptor| {
902 (descriptor.storage_kind == abi::RADIX_EXTERNAL_STORAGE_FIXED)
903 .then_some(descriptor.fixed_bytes as usize)
904 })
905 };
906 let mut null_bitmap = vec![0_u8; values.len().div_ceil(8)];
907 for (index, value) in values.iter().enumerate() {
908 if value.is_null {
909 null_bitmap[index / 8] |= 1 << (index % 8);
910 }
911 }
912 if null_bitmap.iter().all(|byte| *byte == 0) {
913 null_bitmap.clear();
914 }
915
916 if let Some(width) = fixed_width {
917 if width == 0 || width > u16::MAX as usize {
918 return Err(PluginError::limit_exceeded(
919 "fixed test column width is outside ABI bounds",
920 ));
921 }
922 if width == 1 {
923 let data = values
924 .iter()
925 .map(|value| {
926 if value.is_null {
927 Ok(0)
928 } else if value.bytes.len() == 1 {
929 Ok(value.bytes[0])
930 } else {
931 Err(PluginError::invalid_input(
932 "fixed test value has wrong width",
933 ))
934 }
935 })
936 .collect::<PluginResult<Vec<_>>>()?;
937 return Ok(Self {
938 type_ref,
939 layout: abi::RADIX_COLUMN_LAYOUT_FIXED,
940 element_width: 1,
941 alignment: 1,
942 stride: 1,
943 null_bitmap,
944 storage: ColumnStorage::Bytes(data),
945 offsets: Vec::new(),
946 });
947 }
948 let stride = width.div_ceil(8) * 8;
949 let total = stride
950 .checked_mul(values.len())
951 .ok_or_else(|| PluginError::limit_exceeded("test column size overflow"))?;
952 let mut words = vec![0_u64; total / 8];
953 let bytes =
956 unsafe { std::slice::from_raw_parts_mut(words.as_mut_ptr().cast::<u8>(), total) };
957 for (row, value) in values.iter().enumerate() {
958 if !value.is_null {
959 if value.bytes.len() != width {
960 return Err(PluginError::invalid_input(
961 "fixed test value has wrong width",
962 ));
963 }
964 let start = row * stride;
965 bytes[start..start + width].copy_from_slice(&value.bytes);
966 }
967 }
968 return Ok(Self {
969 type_ref,
970 layout: abi::RADIX_COLUMN_LAYOUT_FIXED,
971 element_width: width as u16,
972 alignment: 8,
973 stride: stride as u32,
974 null_bitmap,
975 storage: ColumnStorage::Aligned(words),
976 offsets: Vec::new(),
977 });
978 }
979
980 let mut data = Vec::new();
981 let mut offsets = Vec::with_capacity(values.len() + 1);
982 offsets.push(0);
983 for value in values {
984 if !value.is_null {
985 data.extend_from_slice(&value.bytes);
986 }
987 offsets.push(
988 u32::try_from(data.len())
989 .map_err(|_| PluginError::limit_exceeded("test column exceeds ABI bounds"))?,
990 );
991 }
992 Ok(Self {
993 type_ref,
994 layout: abi::RADIX_COLUMN_LAYOUT_VARIABLE,
995 element_width: 0,
996 alignment: 1,
997 stride: 0,
998 null_bitmap,
999 storage: ColumnStorage::Bytes(data),
1000 offsets,
1001 })
1002 }
1003
1004 fn as_abi(&self, row_count: u32) -> abi::RadixAbiColumnViewV1 {
1005 let (data, data_len) = self.storage.bytes();
1006 abi::RadixAbiColumnViewV1 {
1007 header: abi::RadixAbiHeaderV1::new::<abi::RadixAbiColumnViewV1>(0),
1008 type_ref: self.type_ref,
1009 row_count,
1010 layout: self.layout,
1011 element_width: self.element_width,
1012 alignment: self.alignment,
1013 reserved_u16: 0,
1014 stride: self.stride,
1015 null_bitmap: abi::RadixAbiSliceV1 {
1016 ptr: if self.null_bitmap.is_empty() {
1017 std::ptr::null()
1018 } else {
1019 self.null_bitmap.as_ptr()
1020 },
1021 len: self.null_bitmap.len() as u32,
1022 reserved: 0,
1023 },
1024 data: abi::RadixAbiSliceV1 {
1025 ptr: if data_len == 0 {
1026 std::ptr::null()
1027 } else {
1028 data
1029 },
1030 len: data_len as u32,
1031 reserved: 0,
1032 },
1033 offsets: abi::RadixAbiU32SliceV1 {
1034 ptr: if self.offsets.is_empty() {
1035 std::ptr::null()
1036 } else {
1037 self.offsets.as_ptr()
1038 },
1039 len: self.offsets.len() as u32,
1040 reserved: 0,
1041 },
1042 }
1043 }
1044}
1045
1046struct TestHostState {
1047 options: TestCallOptions,
1048 staged: Vec<TestOutput>,
1049 committed: Vec<TestOutput>,
1050 diagnostics: Vec<TestDiagnostic>,
1051 work_charged: u32,
1052 finished: bool,
1053}
1054
1055impl TestHostState {
1056 fn new(options: TestCallOptions) -> Self {
1057 Self {
1058 options,
1059 staged: Vec::new(),
1060 committed: Vec::new(),
1061 diagnostics: Vec::new(),
1062 work_charged: 0,
1063 finished: false,
1064 }
1065 }
1066
1067 fn diagnostic_sink(&mut self) -> abi::RadixAbiDiagnosticSinkV1 {
1068 let handle = self as *mut Self as usize as u64;
1069 abi::RadixAbiDiagnosticSinkV1 {
1070 header: abi::RadixAbiHeaderV1::new::<abi::RadixAbiDiagnosticSinkV1>(0),
1071 handle,
1072 max_detail_bytes: abi::RADIX_MAX_DIAGNOSTIC_BYTES,
1073 reserved: 0,
1074 write: Some(test_diagnostic),
1075 }
1076 }
1077
1078 fn call_context(
1079 &mut self,
1080 diagnostic_sink: &abi::RadixAbiDiagnosticSinkV1,
1081 declared_output_bytes: u32,
1082 ) -> abi::RadixAbiCallContextV1 {
1083 let handle = self as *mut Self as usize as u64;
1084 abi::RadixAbiCallContextV1 {
1085 header: abi::RadixAbiHeaderV1::new::<abi::RadixAbiCallContextV1>(0),
1086 handle,
1087 deadline_unix_ns: if self.options.deadline_expired {
1088 1
1089 } else {
1090 u64::MAX
1091 },
1092 max_output_bytes: self.options.max_output_bytes.min(declared_output_bytes),
1093 max_work_units: self.options.max_work_units,
1094 check_cancelled: Some(test_cancelled),
1095 charge_work: Some(test_charge_work),
1096 diagnostics: diagnostic_sink,
1097 }
1098 }
1099
1100 fn result_builder(
1101 &mut self,
1102 declared_output_bytes: u32,
1103 max_items: u32,
1104 ) -> abi::RadixAbiResultBuilderV1 {
1105 let handle = self as *mut Self as usize as u64;
1106 abi::RadixAbiResultBuilderV1 {
1107 header: abi::RadixAbiHeaderV1::new::<abi::RadixAbiResultBuilderV1>(0),
1108 handle,
1109 max_bytes: self.options.max_output_bytes.min(declared_output_bytes),
1110 max_items,
1111 write: Some(test_write),
1112 finish: Some(test_finish),
1113 }
1114 }
1115
1116 fn report(self, status: abi::RadixAbiStatusV1) -> TestCallReport {
1117 TestCallReport {
1118 status,
1119 outputs: self.committed,
1120 diagnostics: self.diagnostics,
1121 work_charged: self.work_charged,
1122 finished: self.finished,
1123 }
1124 }
1125}
1126
1127unsafe fn state(handle: u64) -> &'static mut TestHostState {
1128 unsafe { &mut *(handle as usize as *mut TestHostState) }
1131}
1132
1133unsafe extern "C" fn test_cancelled(handle: u64) -> abi::RadixAbiStatusV1 {
1134 let options = unsafe { state(handle) }.options;
1135 if options.cancelled || options.deadline_expired {
1136 abi::RADIX_STATUS_CANCELLED
1137 } else {
1138 abi::RADIX_STATUS_OK
1139 }
1140}
1141
1142unsafe extern "C" fn test_charge_work(handle: u64, units: u32) -> abi::RadixAbiStatusV1 {
1143 let state = unsafe { state(handle) };
1144 let Some(total) = state.work_charged.checked_add(units) else {
1145 return abi::RADIX_STATUS_LIMIT_EXCEEDED;
1146 };
1147 if total > state.options.max_work_units {
1148 abi::RADIX_STATUS_LIMIT_EXCEEDED
1149 } else {
1150 state.work_charged = total;
1151 abi::RADIX_STATUS_OK
1152 }
1153}
1154
1155unsafe extern "C" fn test_write(
1156 handle: u64,
1157 flags: u32,
1158 reserved: u32,
1159 bytes: abi::RadixAbiSliceV1,
1160) -> abi::RadixAbiStatusV1 {
1161 let state = unsafe { state(handle) };
1162 if abi::validate_result_item(flags, reserved, bytes, state.options.max_output_bytes).is_err() {
1163 return abi::RADIX_STATUS_CONTRACT_VIOLATION;
1164 }
1165 let bytes = if bytes.len == 0 {
1166 Vec::new()
1167 } else {
1168 unsafe { std::slice::from_raw_parts(bytes.ptr, bytes.len as usize) }.to_vec()
1170 };
1171 state.staged.push(TestOutput {
1172 is_null: flags & abi::RADIX_RESULT_ITEM_FLAG_NULL != 0,
1173 bytes,
1174 });
1175 abi::RADIX_STATUS_OK
1176}
1177
1178unsafe extern "C" fn test_finish(handle: u64) -> abi::RadixAbiStatusV1 {
1179 let state = unsafe { state(handle) };
1180 if state.finished {
1181 return abi::RADIX_STATUS_CONTRACT_VIOLATION;
1182 }
1183 state.finished = true;
1184 state.committed = std::mem::take(&mut state.staged);
1185 abi::RADIX_STATUS_OK
1186}
1187
1188unsafe extern "C" fn test_diagnostic(
1189 handle: u64,
1190 diagnostic: *const abi::RadixAbiDiagnosticV1,
1191) -> abi::RadixAbiStatusV1 {
1192 let Some(diagnostic) = (unsafe { diagnostic.as_ref() }) else {
1193 return abi::RADIX_STATUS_INVALID_ARGUMENT;
1194 };
1195 if abi::validate_diagnostic(diagnostic).is_err() {
1196 return abi::RADIX_STATUS_CONTRACT_VIOLATION;
1197 }
1198 let copy = |value: abi::RadixAbiStringV1| {
1199 if value.len == 0 {
1200 String::new()
1201 } else {
1202 String::from_utf8_lossy(unsafe {
1204 std::slice::from_raw_parts(value.ptr, value.len as usize)
1205 })
1206 .into_owned()
1207 }
1208 };
1209 unsafe { state(handle) }.diagnostics.push(TestDiagnostic {
1210 category: diagnostic.category,
1211 status: diagnostic.status,
1212 detail: copy(diagnostic.detail),
1213 field: copy(diagnostic.field),
1214 });
1215 abi::RADIX_STATUS_OK
1216}