1use std::{
2 collections::{BTreeMap, BTreeSet},
3 ffi::CStr,
4 fs::{self, File},
5 io::{BufReader, Read},
6 os::unix::fs::{MetadataExt, PermissionsExt},
7 path::{Component, Path, PathBuf},
8 slice, str,
9 sync::{
10 atomic::{AtomicU64, Ordering},
11 Arc,
12 },
13};
14
15use libloading::Library;
16use radixdb_plugin_abi::*;
17use semver::Version;
18use sha2::{Digest, Sha256};
19use thiserror::Error;
20
21use crate::{
22 manifest::{
23 decode_lower_hex, parse_canonical_uuid, parse_glibc_version, MAX_LIBRARY_BYTES,
24 MAX_MANIFEST_BYTES, PLUGIN_MANIFEST_FILE,
25 },
26 ObjectId, PluginHostConfig, PluginPackageManifest, PluginRegistry, RegisteredBinding,
27 RegisteredExternalType, RegisteredFunction, RegisteredOperator, RegisteredOperatorClass,
28 RegisteredPackage, RegisteredPlannerSupport, RegisteredTypeRef,
29};
30
31static NEXT_REGISTRY_GENERATION: AtomicU64 = AtomicU64::new(1);
32static STARTUP_ATTEMPTS: AtomicU64 = AtomicU64::new(0);
33static STARTUP_FAILURES: AtomicU64 = AtomicU64::new(0);
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct PluginLoaderMetricsSnapshot {
37 pub startup_attempts: u64,
38 pub startup_failures: u64,
39}
40
41pub fn plugin_loader_metrics() -> PluginLoaderMetricsSnapshot {
42 PluginLoaderMetricsSnapshot {
43 startup_attempts: STARTUP_ATTEMPTS.load(Ordering::Relaxed),
44 startup_failures: STARTUP_FAILURES.load(Ordering::Relaxed),
45 }
46}
47
48#[derive(Debug, Error)]
49pub enum PluginHostError {
50 #[error("plugin host configuration: {0}")]
51 Configuration(String),
52 #[error("plugin package {path}: {reason}")]
53 Package { path: PathBuf, reason: String },
54 #[error("plugin registry: {0}")]
55 Registry(String),
56 #[error("plugin registry generation space exhausted")]
57 GenerationExhausted,
58}
59
60struct Candidate {
61 directory: PathBuf,
62 library: PathBuf,
63 library_bytes: u64,
64 package_id: ObjectId,
65 descriptor_fingerprint: [u8; 32],
66 manifest: PluginPackageManifest,
67}
68
69struct LoadedObjects {
70 package: RegisteredPackage,
71 capabilities: u64,
72 external_types: Vec<RegisteredExternalType>,
73 functions: Vec<RegisteredFunction>,
74 operators: Vec<RegisteredOperator>,
75 operator_classes: Vec<RegisteredOperatorClass>,
76 planner_support: Vec<RegisteredPlannerSupport>,
77}
78
79pub fn load_plugin_registry(
82 config: &PluginHostConfig,
83) -> Result<Arc<PluginRegistry>, PluginHostError> {
84 STARTUP_ATTEMPTS.fetch_add(1, Ordering::Relaxed);
85 let result = load_plugin_registry_inner(config);
86 if result.is_err() {
87 STARTUP_FAILURES.fetch_add(1, Ordering::Relaxed);
88 }
89 result
90}
91
92fn load_plugin_registry_inner(
93 config: &PluginHostConfig,
94) -> Result<Arc<PluginRegistry>, PluginHostError> {
95 if config.package_directories.is_empty() {
96 return Ok(Arc::new(PluginRegistry::empty()));
97 }
98 let mut candidates = Vec::with_capacity(config.package_directories.len());
99 let mut configured_paths = BTreeSet::new();
100 for directory in &config.package_directories {
101 let normalized = validate_allowlisted_directory(directory)?;
102 if !configured_paths.insert(normalized.clone()) {
103 return Err(PluginHostError::Configuration(format!(
104 "duplicate package directory {}",
105 normalized.display()
106 )));
107 }
108 candidates.push(read_candidate(normalized)?);
109 }
110
111 let (active, shadowed_versions) = select_active_versions(candidates)?;
112 let mut loaded = Vec::with_capacity(active.len());
113 let mut loaded_library_bytes = 0_u64;
114 for candidate in active {
115 loaded_library_bytes = loaded_library_bytes
116 .checked_add(candidate.library_bytes)
117 .ok_or_else(|| PluginHostError::Registry("library byte total overflow".into()))?;
118 loaded.push(load_candidate(candidate)?);
119 }
120 validate_loaded_graph(&loaded)?;
121
122 let generation = NEXT_REGISTRY_GENERATION
123 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
124 current.checked_add(1)
125 })
126 .map_err(|_| PluginHostError::GenerationExhausted)?;
127 Ok(Arc::new(build_registry(
128 generation,
129 loaded,
130 shadowed_versions,
131 loaded_library_bytes,
132 )))
133}
134
135fn validate_allowlisted_directory(path: &Path) -> Result<PathBuf, PluginHostError> {
136 if !path.is_absolute()
137 || path
138 .components()
139 .any(|component| !matches!(component, Component::RootDir | Component::Normal(_)))
140 {
141 return Err(PluginHostError::Configuration(format!(
142 "package path must be normalized and absolute: {}",
143 path.display()
144 )));
145 }
146 validate_secure_path(path, PathKind::Directory).map_err(|reason| {
147 PluginHostError::Configuration(format!("{}: {reason}", path.display()))
148 })?;
149 let canonical = fs::canonicalize(path)
150 .map_err(|error| PluginHostError::Configuration(format!("{}: {error}", path.display())))?;
151 if canonical != path {
152 return Err(PluginHostError::Configuration(format!(
153 "package path must not traverse aliases or symlinks: {}",
154 path.display()
155 )));
156 }
157 Ok(canonical)
158}
159
160fn read_candidate(directory: PathBuf) -> Result<Candidate, PluginHostError> {
161 let manifest_path = directory.join(PLUGIN_MANIFEST_FILE);
162 validate_secure_path(&manifest_path, PathKind::File)
163 .map_err(|reason| package_error(&directory, reason))?;
164 let manifest_metadata = fs::metadata(&manifest_path)
165 .map_err(|error| package_error(&directory, format!("manifest metadata: {error}")))?;
166 if manifest_metadata.len() > MAX_MANIFEST_BYTES {
167 return Err(package_error(
168 &directory,
169 format!("manifest exceeds {MAX_MANIFEST_BYTES} bytes"),
170 ));
171 }
172 let mut source = String::new();
173 BufReader::new(
174 File::open(&manifest_path)
175 .map_err(|error| package_error(&directory, format!("open manifest: {error}")))?,
176 )
177 .take(MAX_MANIFEST_BYTES + 1)
178 .read_to_string(&mut source)
179 .map_err(|error| package_error(&directory, format!("read manifest: {error}")))?;
180 let manifest: PluginPackageManifest = toml::from_str(&source)
181 .map_err(|error| package_error(&directory, format!("parse manifest: {error}")))?;
182 manifest
183 .validate_static_fields()
184 .map_err(|reason| package_error(&directory, reason))?;
185 validate_host_glibc(&manifest.maximum_required_glibc)?;
186 let raw_manifest: toml::Value = toml::from_str(&source)
187 .map_err(|error| package_error(&directory, format!("parse manifest: {error}")))?;
188 let raw_version = raw_manifest
189 .get("version")
190 .and_then(toml::Value::as_str)
191 .ok_or_else(|| package_error(&directory, "version must be a string"))?;
192 if raw_version != manifest.version.to_string() {
193 return Err(package_error(
194 &directory,
195 "version must use canonical SemVer spelling",
196 ));
197 }
198 let package_id = parse_canonical_uuid(&manifest.package_id)
199 .map_err(|reason| package_error(&directory, reason))?;
200 let descriptor_fingerprint =
201 decode_lower_hex::<32>(&manifest.descriptor_fingerprint, "descriptor_fingerprint")
202 .map_err(|reason| package_error(&directory, reason))?;
203 let expected_library_hash = decode_lower_hex::<32>(&manifest.library_sha256, "library_sha256")
204 .map_err(|reason| package_error(&directory, reason))?;
205
206 let library_directory = directory.join("lib");
207 validate_secure_path(&library_directory, PathKind::Directory)
208 .map_err(|reason| package_error(&directory, format!("library directory: {reason}")))?;
209 let library = directory.join(&manifest.library);
210 validate_secure_path(&library, PathKind::File)
211 .map_err(|reason| package_error(&directory, format!("library: {reason}")))?;
212 let canonical_library = fs::canonicalize(&library)
213 .map_err(|error| package_error(&directory, format!("canonicalize library: {error}")))?;
214 if canonical_library != library {
215 return Err(package_error(
216 &directory,
217 "library path must not traverse aliases or symlinks",
218 ));
219 }
220 let library_bytes = fs::metadata(&library)
221 .map_err(|error| package_error(&directory, format!("library metadata: {error}")))?
222 .len();
223 if library_bytes == 0 || library_bytes > MAX_LIBRARY_BYTES {
224 return Err(package_error(
225 &directory,
226 format!("library size must be 1..={MAX_LIBRARY_BYTES} bytes"),
227 ));
228 }
229 validate_elf(&library).map_err(|reason| package_error(&directory, reason))?;
230 let actual_library_hash =
231 sha256_file(&library).map_err(|reason| package_error(&directory, reason))?;
232 if actual_library_hash != expected_library_hash {
233 return Err(package_error(&directory, "library SHA-256 mismatch"));
234 }
235
236 Ok(Candidate {
237 directory,
238 library,
239 library_bytes,
240 package_id,
241 descriptor_fingerprint,
242 manifest,
243 })
244}
245
246#[derive(Clone, Copy)]
247enum PathKind {
248 File,
249 Directory,
250}
251
252fn validate_secure_path(path: &Path, kind: PathKind) -> Result<(), String> {
253 let metadata = fs::symlink_metadata(path).map_err(|error| error.to_string())?;
254 if metadata.file_type().is_symlink() {
255 return Err("symbolic links are forbidden".into());
256 }
257 match kind {
258 PathKind::File if !metadata.is_file() => return Err("not a regular file".into()),
259 PathKind::Directory if !metadata.is_dir() => return Err("not a directory".into()),
260 _ => {}
261 }
262 let effective_uid = unsafe { libc::geteuid() };
263 if metadata.uid() != 0 && metadata.uid() != effective_uid {
264 return Err(format!(
265 "owner uid {} is neither root nor server uid {effective_uid}",
266 metadata.uid()
267 ));
268 }
269 let mode = metadata.permissions().mode() & 0o777;
270 if mode & 0o022 != 0 {
271 return Err(format!("permissions {mode:04o} allow group/world writes"));
272 }
273 Ok(())
274}
275
276fn package_error(directory: &Path, reason: impl Into<String>) -> PluginHostError {
277 PluginHostError::Package {
278 path: directory.to_path_buf(),
279 reason: reason.into(),
280 }
281}
282
283fn validate_elf(path: &Path) -> Result<(), String> {
284 let mut header = [0_u8; 20];
285 File::open(path)
286 .map_err(|error| format!("open library: {error}"))?
287 .read_exact(&mut header)
288 .map_err(|error| format!("read ELF header: {error}"))?;
289 if &header[..4] != b"\x7fELF"
290 || header[4] != 2
291 || header[5] != 1
292 || u16::from_le_bytes([header[16], header[17]]) != 3
293 || u16::from_le_bytes([header[18], header[19]]) != 62
294 {
295 return Err("library is not an x86_64 little-endian ELF shared object".into());
296 }
297 Ok(())
298}
299
300fn sha256_file(path: &Path) -> Result<[u8; 32], String> {
301 let file = File::open(path).map_err(|error| format!("open library for hashing: {error}"))?;
302 let mut reader = BufReader::new(file);
303 let mut hash = Sha256::new();
304 let mut buffer = [0_u8; 64 * 1024];
305 loop {
306 let read = reader
307 .read(&mut buffer)
308 .map_err(|error| format!("hash library: {error}"))?;
309 if read == 0 {
310 break;
311 }
312 hash.update(&buffer[..read]);
313 }
314 Ok(hash.finalize().into())
315}
316
317fn select_active_versions(
318 candidates: Vec<Candidate>,
319) -> Result<(Vec<Candidate>, usize), PluginHostError> {
320 let mut grouped: BTreeMap<ObjectId, Vec<Candidate>> = BTreeMap::new();
321 for candidate in candidates {
322 grouped
323 .entry(candidate.package_id)
324 .or_default()
325 .push(candidate);
326 }
327 let mut active = Vec::with_capacity(grouped.len());
328 let mut shadowed = 0_usize;
329 for (package_id, mut versions) in grouped {
330 let names: BTreeSet<_> = versions
331 .iter()
332 .map(|candidate| candidate.manifest.name.as_str())
333 .collect();
334 if names.len() != 1 {
335 return Err(PluginHostError::Registry(format!(
336 "package identity {} is reused by different names",
337 format_id(package_id)
338 )));
339 }
340 versions.sort_by(|left, right| left.manifest.version.cmp(&right.manifest.version));
341 for pair in versions.windows(2) {
342 if pair[0].manifest.version == pair[1].manifest.version {
343 return Err(PluginHostError::Registry(format!(
344 "duplicate package identity {} version {}",
345 format_id(package_id),
346 pair[0].manifest.version
347 )));
348 }
349 }
350 shadowed = shadowed
351 .checked_add(versions.len().saturating_sub(1))
352 .ok_or_else(|| PluginHostError::Registry("shadowed version count overflow".into()))?;
353 active.push(versions.pop().expect("group is non-empty"));
354 }
355 let mut names = BTreeMap::<String, ObjectId>::new();
356 for candidate in &active {
357 if let Some(previous) = names.insert(candidate.manifest.name.clone(), candidate.package_id)
358 {
359 if previous != candidate.package_id {
360 return Err(PluginHostError::Registry(format!(
361 "package name {} is claimed by identities {} and {}",
362 candidate.manifest.name,
363 format_id(previous),
364 format_id(candidate.package_id)
365 )));
366 }
367 }
368 }
369 Ok((active, shadowed))
370}
371
372fn load_candidate(candidate: Candidate) -> Result<LoadedObjects, PluginHostError> {
373 let library = unsafe { Library::new(&candidate.library) }.map_err(|error| {
374 package_error(
375 &candidate.directory,
376 format!("load {}: {error}", candidate.library.display()),
377 )
378 })?;
379 let library: &'static Library = Box::leak(Box::new(library));
382 let entrypoint = unsafe { library.get::<RadixPluginEntrypointV1>(ENTRYPOINT_SYMBOL_V1) }
383 .map_err(|error| package_error(&candidate.directory, format!("entrypoint: {error}")))?;
384 let host = RadixHostApiV1 {
385 header: RadixAbiHeaderV1::new::<RadixHostApiV1>(0),
386 handle: 1,
387 max_external_value_bytes: RADIX_MAX_EXTERNAL_VALUE_BYTES,
388 max_batch_rows: 65_535,
389 max_planner_spans: RADIX_MAX_PLANNER_SPANS,
390 reserved: 0,
391 log: Some(host_log),
392 };
393 let mut status = RADIX_STATUS_INTERNAL_ERROR;
394 let descriptor = unsafe { entrypoint(&host, &mut status) };
395 if status != RADIX_STATUS_OK {
396 return Err(package_error(
397 &candidate.directory,
398 format!("entrypoint returned status {status}"),
399 ));
400 }
401 let descriptor = unsafe { descriptor.as_ref() }
402 .ok_or_else(|| package_error(&candidate.directory, "entrypoint returned null"))?;
403 validate_package_descriptor_shallow(descriptor).map_err(|error| {
404 package_error(
405 &candidate.directory,
406 format!("invalid package descriptor: {error:?}"),
407 )
408 })?;
409 let package_name = unsafe { copy_string(descriptor.package_name) }
410 .map_err(|reason| package_error(&candidate.directory, reason))?;
411 let package_version = unsafe { copy_string(descriptor.package_version) }
412 .map_err(|reason| package_error(&candidate.directory, reason))?;
413 let parsed_version = Version::parse(&package_version).map_err(|error| {
414 package_error(
415 &candidate.directory,
416 format!("descriptor package version: {error}"),
417 )
418 })?;
419 if descriptor.package_id != candidate.package_id
420 || package_name != candidate.manifest.name
421 || parsed_version != candidate.manifest.version
422 || package_version != parsed_version.to_string()
423 || descriptor.descriptor_fingerprint != candidate.descriptor_fingerprint
424 || descriptor.abi_min_minor != candidate.manifest.abi_min_minor
425 || descriptor.abi_max_minor != candidate.manifest.abi_max_minor
426 {
427 return Err(package_error(
428 &candidate.directory,
429 "manifest and embedded package descriptor differ",
430 ));
431 }
432 let abi_minor = negotiate_minor(
433 RADIX_ABI_MINOR,
434 RADIX_ABI_MINOR,
435 descriptor.abi_min_minor,
436 descriptor.abi_max_minor,
437 )
438 .map_err(|error| package_error(&candidate.directory, format!("ABI negotiation: {error:?}")))?;
439
440 let raw_types = unsafe { copy_table(descriptor.types, descriptor.type_count) };
441 let raw_functions = unsafe { copy_table(descriptor.functions, descriptor.function_count) };
442 let raw_operators = unsafe { copy_table(descriptor.operators, descriptor.operator_count) };
443 let raw_operator_classes =
444 unsafe { copy_table(descriptor.operator_classes, descriptor.operator_class_count) };
445 let raw_planner =
446 unsafe { copy_table(descriptor.planner_support, descriptor.planner_support_count) };
447
448 let external_types = raw_types
449 .iter()
450 .map(|raw| copy_external_type(candidate.package_id, raw))
451 .collect::<Result<Vec<_>, _>>()
452 .map_err(|reason| package_error(&candidate.directory, reason))?;
453 let functions = raw_functions
454 .iter()
455 .map(|raw| copy_function(candidate.package_id, raw))
456 .collect::<Result<Vec<_>, _>>()
457 .map_err(|reason| package_error(&candidate.directory, reason))?;
458 let operators = raw_operators
459 .iter()
460 .map(|raw| copy_operator(candidate.package_id, raw))
461 .collect::<Result<Vec<_>, _>>()
462 .map_err(|reason| package_error(&candidate.directory, reason))?;
463 let operator_classes = raw_operator_classes
464 .iter()
465 .map(|raw| copy_operator_class(candidate.package_id, raw))
466 .collect::<Result<Vec<_>, _>>()
467 .map_err(|reason| package_error(&candidate.directory, reason))?;
468 let planner_support = raw_planner
469 .iter()
470 .map(|raw| copy_planner_support(candidate.package_id, raw))
471 .collect::<Result<Vec<_>, _>>()
472 .map_err(|reason| package_error(&candidate.directory, reason))?;
473
474 Ok(LoadedObjects {
475 package: RegisteredPackage {
476 package_id: candidate.package_id,
477 name: package_name,
478 version: parsed_version,
479 abi_major: candidate.manifest.abi_major,
480 abi_min_minor: descriptor.abi_min_minor,
481 abi_max_minor: descriptor.abi_max_minor,
482 abi_minor,
483 descriptor_fingerprint: descriptor.descriptor_fingerprint,
484 },
485 capabilities: descriptor.header.flags,
486 external_types,
487 functions,
488 operators,
489 operator_classes,
490 planner_support,
491 })
492}
493
494#[cfg(any(test, feature = "test-hooks"))]
498#[doc(hidden)]
499pub unsafe fn registry_from_test_descriptor(
500 descriptor: &'static RadixPluginDescriptorV1,
501) -> Result<Arc<PluginRegistry>, PluginHostError> {
502 validate_package_descriptor_shallow(descriptor).map_err(|error| {
503 PluginHostError::Registry(format!("invalid package descriptor: {error:?}"))
504 })?;
505 let package_id = descriptor.package_id;
506 let package_name =
507 unsafe { copy_string(descriptor.package_name) }.map_err(PluginHostError::Registry)?;
508 let package_version =
509 unsafe { copy_string(descriptor.package_version) }.map_err(PluginHostError::Registry)?;
510 let parsed_version = Version::parse(&package_version)
511 .map_err(|error| PluginHostError::Registry(format!("package version: {error}")))?;
512 if package_version != parsed_version.to_string() {
513 return Err(PluginHostError::Registry(
514 "package version is not canonical SemVer".into(),
515 ));
516 }
517 let abi_minor = negotiate_minor(
518 RADIX_ABI_MINOR,
519 RADIX_ABI_MINOR,
520 descriptor.abi_min_minor,
521 descriptor.abi_max_minor,
522 )
523 .map_err(|error| PluginHostError::Registry(format!("ABI negotiation: {error:?}")))?;
524
525 let raw_types = unsafe { copy_table(descriptor.types, descriptor.type_count) };
526 let raw_functions = unsafe { copy_table(descriptor.functions, descriptor.function_count) };
527 let raw_operators = unsafe { copy_table(descriptor.operators, descriptor.operator_count) };
528 let raw_operator_classes =
529 unsafe { copy_table(descriptor.operator_classes, descriptor.operator_class_count) };
530 let raw_planner =
531 unsafe { copy_table(descriptor.planner_support, descriptor.planner_support_count) };
532 let loaded = LoadedObjects {
533 package: RegisteredPackage {
534 package_id,
535 name: package_name,
536 version: parsed_version,
537 abi_major: descriptor.header.abi_major,
538 abi_min_minor: descriptor.abi_min_minor,
539 abi_max_minor: descriptor.abi_max_minor,
540 abi_minor,
541 descriptor_fingerprint: descriptor.descriptor_fingerprint,
542 },
543 capabilities: descriptor.header.flags,
544 external_types: raw_types
545 .iter()
546 .map(|raw| copy_external_type(package_id, raw))
547 .collect::<Result<_, _>>()
548 .map_err(PluginHostError::Registry)?,
549 functions: raw_functions
550 .iter()
551 .map(|raw| copy_function(package_id, raw))
552 .collect::<Result<_, _>>()
553 .map_err(PluginHostError::Registry)?,
554 operators: raw_operators
555 .iter()
556 .map(|raw| copy_operator(package_id, raw))
557 .collect::<Result<_, _>>()
558 .map_err(PluginHostError::Registry)?,
559 operator_classes: raw_operator_classes
560 .iter()
561 .map(|raw| copy_operator_class(package_id, raw))
562 .collect::<Result<_, _>>()
563 .map_err(PluginHostError::Registry)?,
564 planner_support: raw_planner
565 .iter()
566 .map(|raw| copy_planner_support(package_id, raw))
567 .collect::<Result<_, _>>()
568 .map_err(PluginHostError::Registry)?,
569 };
570 validate_loaded_graph(std::slice::from_ref(&loaded))?;
571 Ok(Arc::new(build_registry(1, vec![loaded], 0, 0)))
572}
573
574unsafe extern "C" fn host_log(
575 _handle: u64,
576 level: u16,
577 reserved: u16,
578 message: RadixAbiStringV1,
579) -> RadixAbiStatusV1 {
580 if validate_log_record(level, reserved, message).is_err() {
581 return RADIX_STATUS_CONTRACT_VIOLATION;
582 }
583 let bytes = if message.len == 0 {
584 &[][..]
585 } else {
586 unsafe { slice::from_raw_parts(message.ptr, message.len as usize) }
587 };
588 let Ok(message) = str::from_utf8(bytes) else {
589 return RADIX_STATUS_CONTRACT_VIOLATION;
590 };
591 let level = match level {
592 RADIX_LOG_INFO => "INFO",
593 RADIX_LOG_WARN => "WARN",
594 RADIX_LOG_ERROR => "ERROR",
595 RADIX_LOG_DEBUG => "DEBUG",
596 _ => return RADIX_STATUS_CONTRACT_VIOLATION,
597 };
598 eprintln!("radixdb plugin [{level}] {message}");
599 RADIX_STATUS_OK
600}
601
602unsafe fn copy_table<T: Copy>(pointer: *const T, count: u32) -> Vec<T> {
603 if count == 0 {
604 Vec::new()
605 } else {
606 unsafe { slice::from_raw_parts(pointer, count as usize) }.to_vec()
607 }
608}
609
610unsafe fn copy_string(value: RadixAbiStringV1) -> Result<String, String> {
611 validate_slice(value, RADIX_MAX_LOCAL_ID_BYTES, 1)
612 .map_err(|error| format!("invalid string slice: {error:?}"))?;
613 let bytes = if value.len == 0 {
614 &[][..]
615 } else {
616 unsafe { slice::from_raw_parts(value.ptr, value.len as usize) }
617 };
618 let value = str::from_utf8(bytes).map_err(|_| "descriptor string is not UTF-8")?;
619 if value.as_bytes().contains(&0) {
620 return Err("descriptor string contains NUL".into());
621 }
622 Ok(value.to_owned())
623}
624
625fn copy_external_type(
626 package_id: ObjectId,
627 raw: &RadixAbiExternalTypeDescriptorV1,
628) -> Result<RegisteredExternalType, String> {
629 validate_external_type_descriptor(raw)
630 .map_err(|error| format!("invalid external type descriptor: {error:?}"))?;
631 let local_id = unsafe { copy_string(raw.local_id) }?;
632 validate_identity(package_id, &local_id, raw.object_id)?;
633 Ok(RegisteredExternalType {
634 package_id,
635 object_id: raw.object_id,
636 local_id,
637 display_name: unsafe { copy_string(raw.display_name) }?,
638 codec_version: raw.codec_version,
639 semantic_revision: raw.semantic_revision,
640 storage_kind: raw.storage_kind,
641 fixed_bytes: raw.fixed_bytes,
642 max_bytes: raw.max_bytes,
643 capabilities: raw.capabilities,
644 codec_fingerprint: raw.codec_fingerprint,
645 encode: raw.encode.expect("validated encode callback"),
646 decode: raw.decode.expect("validated decode callback"),
647 equality: raw.equality,
648 hash: raw.hash,
649 ordering: raw.ordering,
650 text_input: raw.text_input,
651 text_output: raw.text_output,
652 binary_input: raw.binary_input,
653 binary_output: raw.binary_output,
654 })
655}
656
657fn copy_function(
658 package_id: ObjectId,
659 raw: &RadixAbiScalarFunctionDescriptorV1,
660) -> Result<RegisteredFunction, String> {
661 validate_scalar_function_descriptor(raw)
662 .map_err(|error| format!("invalid function descriptor: {error:?}"))?;
663 let local_id = unsafe { copy_string(raw.local_id) }?;
664 validate_identity(package_id, &local_id, raw.object_id)?;
665 let arguments = unsafe { copy_table(raw.arguments, raw.argument_count) }
666 .iter()
667 .map(convert_type_ref)
668 .collect::<Result<Vec<_>, _>>()?;
669 Ok(RegisteredFunction {
670 package_id,
671 object_id: raw.object_id,
672 local_id,
673 display_name: unsafe { copy_string(raw.display_name) }?,
674 semantic_revision: raw.semantic_revision,
675 arguments,
676 result: convert_type_ref(&raw.result)?,
677 volatility: raw.volatility,
678 cancellation: raw.cancellation,
679 strict: raw.strict != 0,
680 parallel_safe: raw.parallel_safe != 0,
681 cost: raw.cost,
682 max_output_bytes: raw.max_output_bytes,
683 scalar: raw.scalar.expect("validated scalar callback"),
684 batch: raw.batch,
685 })
686}
687
688fn copy_operator(
689 package_id: ObjectId,
690 raw: &RadixAbiOperatorDescriptorV1,
691) -> Result<RegisteredOperator, String> {
692 validate_operator_descriptor(raw)
693 .map_err(|error| format!("invalid operator descriptor: {error:?}"))?;
694 let local_id = unsafe { copy_string(raw.local_id) }?;
695 validate_identity(package_id, &local_id, raw.object_id)?;
696 Ok(RegisteredOperator {
697 package_id,
698 object_id: raw.object_id,
699 local_id,
700 symbol: unsafe { copy_string(raw.symbol) }?,
701 semantic_revision: raw.semantic_revision,
702 left: (raw.left != RadixAbiTypeRefV1::ABSENT)
703 .then(|| convert_type_ref(&raw.left))
704 .transpose()?,
705 right: convert_type_ref(&raw.right)?,
706 result: convert_type_ref(&raw.result)?,
707 function_id: raw.function_id,
708 })
709}
710
711fn copy_operator_class(
712 package_id: ObjectId,
713 raw: &RadixAbiOperatorClassDescriptorV1,
714) -> Result<RegisteredOperatorClass, String> {
715 validate_operator_class_descriptor(raw)
716 .map_err(|error| format!("invalid operator class descriptor: {error:?}"))?;
717 let local_id = unsafe { copy_string(raw.local_id) }?;
718 validate_identity(package_id, &local_id, raw.object_id)?;
719 let strategies = unsafe { copy_table(raw.strategies, raw.strategy_count) };
720 let supports = unsafe { copy_table(raw.supports, raw.support_count) };
721 Ok(RegisteredOperatorClass {
722 package_id,
723 object_id: raw.object_id,
724 local_id,
725 semantic_revision: raw.semantic_revision,
726 access_method: raw.access_method,
727 input_type: convert_type_ref(&raw.input_type)?,
728 key_type: convert_type_ref(&raw.key_type)?,
729 key_codec_revision: raw.key_codec_revision,
730 strategies: convert_bindings(&strategies)?,
731 supports: convert_bindings(&supports)?,
732 fingerprint: raw.fingerprint,
733 encode_key: raw.encode_key.expect("validated key encoder"),
734 })
735}
736
737fn copy_planner_support(
738 package_id: ObjectId,
739 raw: &RadixAbiPlannerSupportDescriptorV1,
740) -> Result<RegisteredPlannerSupport, String> {
741 validate_planner_support_descriptor(raw)
742 .map_err(|error| format!("invalid planner support descriptor: {error:?}"))?;
743 let local_id = unsafe { copy_string(raw.local_id) }?;
744 validate_identity(package_id, &local_id, raw.object_id)?;
745 Ok(RegisteredPlannerSupport {
746 package_id,
747 object_id: raw.object_id,
748 local_id,
749 semantic_revision: raw.semantic_revision,
750 max_spans: raw.max_spans,
751 max_output_bytes: raw.max_output_bytes,
752 recheck_policy: raw.recheck_policy,
753 target_function_id: (raw.target_function_id != [0; 16]).then_some(raw.target_function_id),
754 target_operator_class_id: (raw.target_operator_class_id != [0; 16])
755 .then_some(raw.target_operator_class_id),
756 fingerprint: raw.fingerprint,
757 callback: raw.callback.expect("validated planner callback"),
758 })
759}
760
761fn convert_type_ref(raw: &RadixAbiTypeRefV1) -> Result<RegisteredTypeRef, String> {
762 validate_type_ref(raw).map_err(|error| format!("invalid type reference: {error:?}"))?;
763 match raw.kind {
764 RADIX_TYPE_REF_BUILTIN => Ok(RegisteredTypeRef::Builtin(raw.builtin_tag)),
765 RADIX_TYPE_REF_EXTERNAL => Ok(RegisteredTypeRef::External {
766 object_id: raw.object_id,
767 codec_version: raw.codec_version,
768 }),
769 _ => Err("invalid type reference kind".into()),
770 }
771}
772
773fn convert_bindings(raw: &[RadixAbiBindingEntryV1]) -> Result<Vec<RegisteredBinding>, String> {
774 let mut previous = 0_u16;
775 let mut output = Vec::with_capacity(raw.len());
776 for entry in raw {
777 if entry.slot == 0
778 || entry.slot <= previous
779 || entry.flags != 0
780 || entry.object_id == [0; 16]
781 {
782 return Err(
783 "binding table must have sorted unique nonzero slots and zero flags".into(),
784 );
785 }
786 previous = entry.slot;
787 output.push(RegisteredBinding {
788 slot: entry.slot,
789 object_id: entry.object_id,
790 });
791 }
792 Ok(output)
793}
794
795fn validate_identity(package_id: ObjectId, local_id: &str, actual: ObjectId) -> Result<(), String> {
796 let expected = crate::derive_object_id(package_id, local_id)?;
797 if actual != expected {
798 return Err(format!(
799 "object {} does not match derived identity {} for local id {local_id}",
800 format_id(actual),
801 format_id(expected)
802 ));
803 }
804 Ok(())
805}
806
807fn validate_loaded_graph(packages: &[LoadedObjects]) -> Result<(), PluginHostError> {
808 let mut objects = BTreeMap::<ObjectId, (&str, &'static str)>::new();
809 let mut types = BTreeMap::<ObjectId, &RegisteredExternalType>::new();
810 let mut functions = BTreeMap::<ObjectId, &RegisteredFunction>::new();
811 let mut operators = BTreeMap::<ObjectId, &RegisteredOperator>::new();
812 let mut operator_classes = BTreeMap::<ObjectId, &RegisteredOperatorClass>::new();
813 let mut planner_support = BTreeMap::<ObjectId, &RegisteredPlannerSupport>::new();
814 let mut function_overloads = BTreeSet::new();
815 let mut operator_overloads = BTreeSet::new();
816
817 for package in packages {
818 validate_capability_parity(package)?;
819 for value in &package.external_types {
820 insert_object(&mut objects, value.object_id, &value.local_id, "type")?;
821 types.insert(value.object_id, value);
822 }
823 for value in &package.functions {
824 insert_object(&mut objects, value.object_id, &value.local_id, "function")?;
825 if !function_overloads.insert((value.display_name.clone(), value.arguments.clone())) {
826 return Err(PluginHostError::Registry(format!(
827 "ambiguous function overload {}({:?})",
828 value.display_name, value.arguments
829 )));
830 }
831 functions.insert(value.object_id, value);
832 }
833 for value in &package.operators {
834 insert_object(&mut objects, value.object_id, &value.local_id, "operator")?;
835 if !operator_overloads.insert((value.symbol.clone(), value.left, value.right)) {
836 return Err(PluginHostError::Registry(format!(
837 "ambiguous operator overload {}",
838 value.symbol
839 )));
840 }
841 operators.insert(value.object_id, value);
842 }
843 for value in &package.operator_classes {
844 insert_object(
845 &mut objects,
846 value.object_id,
847 &value.local_id,
848 "operator class",
849 )?;
850 operator_classes.insert(value.object_id, value);
851 }
852 for value in &package.planner_support {
853 insert_object(
854 &mut objects,
855 value.object_id,
856 &value.local_id,
857 "planner support",
858 )?;
859 planner_support.insert(value.object_id, value);
860 }
861 }
862
863 for function in functions.values() {
864 for reference in function.arguments.iter().chain([&function.result]) {
865 validate_type_resolution(reference, &types)?;
866 }
867 }
868 for operator in operators.values() {
869 if let Some(left) = operator.left {
870 validate_type_resolution(&left, &types)?;
871 }
872 validate_type_resolution(&operator.right, &types)?;
873 validate_type_resolution(&operator.result, &types)?;
874 let function = functions.get(&operator.function_id).ok_or_else(|| {
875 PluginHostError::Registry(format!(
876 "operator {} references missing function {}",
877 operator.local_id,
878 format_id(operator.function_id)
879 ))
880 })?;
881 let expected_arguments: Vec<_> =
882 operator.left.into_iter().chain([operator.right]).collect();
883 if function.arguments != expected_arguments || function.result != operator.result {
884 return Err(PluginHostError::Registry(format!(
885 "operator {} and backing function signature differ",
886 operator.local_id
887 )));
888 }
889 }
890 for class in operator_classes.values() {
891 validate_type_resolution(&class.input_type, &types)?;
892 if !matches!(class.key_type, RegisteredTypeRef::Builtin(_)) {
893 return Err(PluginHostError::Registry(format!(
894 "operator class {} key type must be core-owned",
895 class.local_id
896 )));
897 }
898 let RegisteredTypeRef::External { object_id, .. } = class.input_type else {
899 return Err(PluginHostError::Registry(format!(
900 "operator class {} input type must be package-defined",
901 class.local_id
902 )));
903 };
904 let input = types.get(&object_id).expect("type resolution was checked");
905 match class.access_method {
906 radixdb_plugin_abi::RADIX_ACCESS_METHOD_BTREE if input.ordering.is_none() => {
907 return Err(PluginHostError::Registry(format!(
908 "B-tree operator class {} requires input ordering",
909 class.local_id
910 )));
911 }
912 radixdb_plugin_abi::RADIX_ACCESS_METHOD_HASH => {
913 if input.equality.is_none() || input.hash.is_none() {
914 return Err(PluginHostError::Registry(format!(
915 "hash operator class {} requires input equality and hash",
916 class.local_id
917 )));
918 }
919 if class.key_type
920 != RegisteredTypeRef::Builtin(radixdb_plugin_abi::RADIX_BUILTIN_BYTES)
921 {
922 return Err(PluginHostError::Registry(format!(
923 "hash operator class {} physical key must be BYTES",
924 class.local_id
925 )));
926 }
927 }
928 _ => {}
929 }
930 let expected_strategies: &[(u16, &str)] = match class.access_method {
931 radixdb_plugin_abi::RADIX_ACCESS_METHOD_BTREE => {
932 &[(1, "<"), (2, "<="), (3, "="), (4, ">="), (5, ">")]
933 }
934 radixdb_plugin_abi::RADIX_ACCESS_METHOD_HASH
935 | radixdb_plugin_abi::RADIX_ACCESS_METHOD_BITMAP => &[(1, "=")],
936 radixdb_plugin_abi::RADIX_ACCESS_METHOD_HNSW => {
937 return Err(PluginHostError::Registry(format!(
938 "external HNSW operator class {} requires planner support outside the v1.2 boundary",
939 class.local_id
940 )));
941 }
942 _ => unreachable!("access method was validated"),
943 };
944 if class.strategies.len() != expected_strategies.len() {
945 return Err(PluginHostError::Registry(format!(
946 "operator class {} has an incomplete strategy table",
947 class.local_id
948 )));
949 }
950 for ((expected_slot, expected_symbol), binding) in
951 expected_strategies.iter().zip(&class.strategies)
952 {
953 let Some(operator) = operators.get(&binding.object_id) else {
954 return Err(PluginHostError::Registry(format!(
955 "operator class {} references missing strategy {}",
956 class.local_id,
957 format_id(binding.object_id)
958 )));
959 };
960 if binding.slot != *expected_slot
961 || operator.symbol != *expected_symbol
962 || operator.left != Some(class.input_type)
963 || operator.right != class.input_type
964 || operator.result
965 != RegisteredTypeRef::Builtin(radixdb_plugin_abi::RADIX_BUILTIN_BOOLEAN)
966 || operator.package_id != class.package_id
967 {
968 return Err(PluginHostError::Registry(format!(
969 "operator class {} strategy slot {} is semantically incompatible",
970 class.local_id, expected_slot
971 )));
972 }
973 }
974 for binding in &class.supports {
975 if !planner_support.contains_key(&binding.object_id) {
976 return Err(PluginHostError::Registry(format!(
977 "operator class {} references missing planner support {}",
978 class.local_id,
979 format_id(binding.object_id)
980 )));
981 }
982 }
983 }
984 for support in planner_support.values() {
985 if support
986 .target_function_id
987 .is_some_and(|id| !functions.contains_key(&id))
988 || support
989 .target_operator_class_id
990 .is_some_and(|id| !operator_classes.contains_key(&id))
991 {
992 return Err(PluginHostError::Registry(format!(
993 "planner support {} has an unresolved target",
994 support.local_id
995 )));
996 }
997 }
998 Ok(())
999}
1000
1001fn validate_capability_parity(package: &LoadedObjects) -> Result<(), PluginHostError> {
1002 let flags = package.capabilities;
1003 let expected = [
1004 (
1005 RADIX_PACKAGE_CAP_EXTERNAL_TYPES,
1006 !package.external_types.is_empty(),
1007 ),
1008 (
1009 RADIX_PACKAGE_CAP_SCALAR_FUNCTIONS,
1010 !package.functions.is_empty(),
1011 ),
1012 (RADIX_PACKAGE_CAP_OPERATORS, !package.operators.is_empty()),
1013 (
1014 RADIX_PACKAGE_CAP_OPERATOR_CLASSES,
1015 !package.operator_classes.is_empty(),
1016 ),
1017 (
1018 RADIX_PACKAGE_CAP_PLANNER_SUPPORT,
1019 !package.planner_support.is_empty(),
1020 ),
1021 (
1022 RADIX_PACKAGE_CAP_BATCH_FUNCTIONS,
1023 package
1024 .functions
1025 .iter()
1026 .any(|function| function.batch.is_some()),
1027 ),
1028 ];
1029 if expected
1030 .iter()
1031 .any(|(flag, present)| (flags & *flag != 0) != *present)
1032 {
1033 return Err(PluginHostError::Registry(format!(
1034 "package {} capability flags do not match descriptor graph",
1035 package.package.name
1036 )));
1037 }
1038 Ok(())
1039}
1040
1041fn insert_object<'a>(
1042 objects: &mut BTreeMap<ObjectId, (&'a str, &'static str)>,
1043 id: ObjectId,
1044 local_id: &'a str,
1045 kind: &'static str,
1046) -> Result<(), PluginHostError> {
1047 if let Some((previous_id, previous_kind)) = objects.insert(id, (local_id, kind)) {
1048 return Err(PluginHostError::Registry(format!(
1049 "object identity collision: {previous_kind} {previous_id} and {kind} {local_id}"
1050 )));
1051 }
1052 Ok(())
1053}
1054
1055fn validate_type_resolution(
1056 reference: &RegisteredTypeRef,
1057 types: &BTreeMap<ObjectId, &RegisteredExternalType>,
1058) -> Result<(), PluginHostError> {
1059 let RegisteredTypeRef::External {
1060 object_id,
1061 codec_version,
1062 } = reference
1063 else {
1064 return Ok(());
1065 };
1066 let value = types.get(object_id).ok_or_else(|| {
1067 PluginHostError::Registry(format!(
1068 "unresolved external type {}",
1069 format_id(*object_id)
1070 ))
1071 })?;
1072 if value.codec_version != *codec_version {
1073 return Err(PluginHostError::Registry(format!(
1074 "external type {} requires codec {}, registry has {}",
1075 format_id(*object_id),
1076 codec_version,
1077 value.codec_version
1078 )));
1079 }
1080 Ok(())
1081}
1082
1083fn build_registry(
1084 generation: u64,
1085 loaded: Vec<LoadedObjects>,
1086 shadowed_versions: usize,
1087 loaded_library_bytes: u64,
1088) -> PluginRegistry {
1089 let mut registry = PluginRegistry {
1090 generation,
1091 packages: BTreeMap::new(),
1092 external_types: BTreeMap::new(),
1093 functions: BTreeMap::new(),
1094 operators: BTreeMap::new(),
1095 operator_classes: BTreeMap::new(),
1096 planner_support: BTreeMap::new(),
1097 shadowed_versions,
1098 loaded_library_bytes,
1099 };
1100 for package in loaded {
1101 let package_id = package.package.package_id;
1102 registry
1103 .packages
1104 .insert(package_id, Arc::new(package.package));
1105 for value in package.external_types {
1106 registry
1107 .external_types
1108 .insert(value.object_id, Arc::new(value));
1109 }
1110 for value in package.functions {
1111 registry.functions.insert(value.object_id, Arc::new(value));
1112 }
1113 for value in package.operators {
1114 registry.operators.insert(value.object_id, Arc::new(value));
1115 }
1116 for value in package.operator_classes {
1117 registry
1118 .operator_classes
1119 .insert(value.object_id, Arc::new(value));
1120 }
1121 for value in package.planner_support {
1122 registry
1123 .planner_support
1124 .insert(value.object_id, Arc::new(value));
1125 }
1126 }
1127 registry
1128}
1129
1130fn validate_host_glibc(required: &str) -> Result<(), PluginHostError> {
1131 let version = unsafe { CStr::from_ptr(libc::gnu_get_libc_version()) }
1132 .to_str()
1133 .map_err(|_| PluginHostError::Configuration("host glibc version is not UTF-8".into()))?;
1134 let mut components = version.split('.');
1135 let major = components
1136 .next()
1137 .and_then(|value| value.parse::<u32>().ok())
1138 .ok_or_else(|| PluginHostError::Configuration(format!("invalid host glibc {version}")))?;
1139 let minor = components
1140 .next()
1141 .and_then(|value| value.parse::<u32>().ok())
1142 .ok_or_else(|| PluginHostError::Configuration(format!("invalid host glibc {version}")))?;
1143 let required = parse_glibc_version(required).ok_or_else(|| {
1144 PluginHostError::Configuration("invalid package glibc requirement".into())
1145 })?;
1146 if (major, minor) < required {
1147 return Err(PluginHostError::Configuration(format!(
1148 "host glibc {version} is older than required {}.{}",
1149 required.0, required.1
1150 )));
1151 }
1152 Ok(())
1153}
1154
1155fn format_id(id: ObjectId) -> String {
1156 let mut output = String::with_capacity(32);
1157 for byte in id {
1158 use std::fmt::Write as _;
1159 write!(&mut output, "{byte:02x}").expect("writing to String cannot fail");
1160 }
1161 output
1162}
1163
1164#[cfg(test)]
1165mod tests {
1166 use super::*;
1167
1168 unsafe extern "C" fn codec(
1169 _context: *const RadixAbiCallContextV1,
1170 _input: *const RadixAbiValueV1,
1171 _output: *const RadixAbiResultBuilderV1,
1172 ) -> RadixAbiStatusV1 {
1173 RADIX_STATUS_OK
1174 }
1175
1176 unsafe extern "C" fn parse(
1177 _context: *const RadixAbiCallContextV1,
1178 _input: RadixAbiSliceV1,
1179 _output: *const RadixAbiResultBuilderV1,
1180 ) -> RadixAbiStatusV1 {
1181 RADIX_STATUS_OK
1182 }
1183
1184 unsafe extern "C" fn scalar(
1185 _context: *const RadixAbiCallContextV1,
1186 _arguments: *const RadixAbiValueV1,
1187 _argument_count: u32,
1188 _output: *const RadixAbiResultBuilderV1,
1189 ) -> RadixAbiStatusV1 {
1190 RADIX_STATUS_OK
1191 }
1192
1193 unsafe extern "C" fn key(
1194 _context: *const RadixAbiCallContextV1,
1195 _value: *const RadixAbiValueV1,
1196 _output: *const RadixAbiResultBuilderV1,
1197 ) -> RadixAbiStatusV1 {
1198 RADIX_STATUS_OK
1199 }
1200
1201 unsafe extern "C" fn equal(
1202 _context: *const RadixAbiCallContextV1,
1203 _left: *const RadixAbiValueV1,
1204 _right: *const RadixAbiValueV1,
1205 output: *mut u8,
1206 ) -> RadixAbiStatusV1 {
1207 if let Some(output) = unsafe { output.as_mut() } {
1208 *output = 1;
1209 RADIX_STATUS_OK
1210 } else {
1211 RADIX_STATUS_INVALID_ARGUMENT
1212 }
1213 }
1214
1215 unsafe extern "C" fn hash(
1216 _context: *const RadixAbiCallContextV1,
1217 _value: *const RadixAbiValueV1,
1218 _sink: *const RadixAbiHashSinkV1,
1219 ) -> RadixAbiStatusV1 {
1220 RADIX_STATUS_OK
1221 }
1222
1223 unsafe extern "C" fn support(
1224 _context: *const RadixAbiCallContextV1,
1225 _predicate: RadixAbiSliceV1,
1226 _output: *const RadixAbiResultBuilderV1,
1227 ) -> RadixAbiStatusV1 {
1228 RADIX_STATUS_OK
1229 }
1230
1231 fn complete_graph() -> LoadedObjects {
1232 let package_id = [7; 16];
1233 let type_id = crate::derive_object_id(package_id, "point").unwrap();
1234 let function_id = crate::derive_object_id(package_id, "point_equal").unwrap();
1235 let operator_id = crate::derive_object_id(package_id, "point_eq_operator").unwrap();
1236 let class_id = crate::derive_object_id(package_id, "point_btree").unwrap();
1237 let support_id = crate::derive_object_id(package_id, "point_ranges").unwrap();
1238 let point = RegisteredTypeRef::External {
1239 object_id: type_id,
1240 codec_version: 1,
1241 };
1242 LoadedObjects {
1243 package: RegisteredPackage {
1244 package_id,
1245 name: "spatial".into(),
1246 version: Version::new(1, 0, 0),
1247 abi_major: RADIX_ABI_MAJOR,
1248 abi_min_minor: RADIX_ABI_MINOR,
1249 abi_max_minor: RADIX_ABI_MINOR,
1250 abi_minor: 0,
1251 descriptor_fingerprint: [1; 32],
1252 },
1253 capabilities: RADIX_PACKAGE_CAP_EXTERNAL_TYPES
1254 | RADIX_PACKAGE_CAP_SCALAR_FUNCTIONS
1255 | RADIX_PACKAGE_CAP_OPERATORS
1256 | RADIX_PACKAGE_CAP_OPERATOR_CLASSES
1257 | RADIX_PACKAGE_CAP_PLANNER_SUPPORT,
1258 external_types: vec![RegisteredExternalType {
1259 package_id,
1260 object_id: type_id,
1261 local_id: "point".into(),
1262 display_name: "point".into(),
1263 codec_version: 1,
1264 semantic_revision: 1,
1265 storage_kind: RADIX_EXTERNAL_STORAGE_FIXED,
1266 fixed_bytes: 16,
1267 max_bytes: 16,
1268 capabilities: RADIX_TYPE_CAP_EQUALITY | RADIX_TYPE_CAP_HASH,
1269 codec_fingerprint: [2; 32],
1270 encode: codec,
1271 decode: parse,
1272 equality: Some(equal),
1273 hash: Some(hash),
1274 ordering: None,
1275 text_input: None,
1276 text_output: None,
1277 binary_input: None,
1278 binary_output: None,
1279 }],
1280 functions: vec![RegisteredFunction {
1281 package_id,
1282 object_id: function_id,
1283 local_id: "point_equal".into(),
1284 display_name: "point_equal".into(),
1285 semantic_revision: 1,
1286 arguments: vec![point, point],
1287 result: RegisteredTypeRef::Builtin(RADIX_BUILTIN_BOOLEAN),
1288 volatility: RADIX_VOLATILITY_IMMUTABLE,
1289 cancellation: RADIX_CANCELLATION_BOUNDED,
1290 strict: true,
1291 parallel_safe: true,
1292 cost: 1,
1293 max_output_bytes: 1,
1294 scalar,
1295 batch: None,
1296 }],
1297 operators: vec![RegisteredOperator {
1298 package_id,
1299 object_id: operator_id,
1300 local_id: "point_eq_operator".into(),
1301 symbol: "=".into(),
1302 semantic_revision: 1,
1303 left: Some(point),
1304 right: point,
1305 result: RegisteredTypeRef::Builtin(RADIX_BUILTIN_BOOLEAN),
1306 function_id,
1307 }],
1308 operator_classes: vec![RegisteredOperatorClass {
1309 package_id,
1310 object_id: class_id,
1311 local_id: "point_btree".into(),
1312 semantic_revision: 1,
1313 access_method: RADIX_ACCESS_METHOD_HASH,
1314 input_type: point,
1315 key_type: RegisteredTypeRef::Builtin(RADIX_BUILTIN_BYTES),
1316 key_codec_revision: 1,
1317 strategies: vec![RegisteredBinding {
1318 slot: 1,
1319 object_id: operator_id,
1320 }],
1321 supports: vec![RegisteredBinding {
1322 slot: 1,
1323 object_id: support_id,
1324 }],
1325 fingerprint: [3; 32],
1326 encode_key: key,
1327 }],
1328 planner_support: vec![RegisteredPlannerSupport {
1329 package_id,
1330 object_id: support_id,
1331 local_id: "point_ranges".into(),
1332 semantic_revision: 1,
1333 max_spans: 8,
1334 max_output_bytes: 4096,
1335 recheck_policy: RADIX_RECHECK_ALWAYS,
1336 target_function_id: None,
1337 target_operator_class_id: Some(class_id),
1338 fingerprint: [4; 32],
1339 callback: support,
1340 }],
1341 }
1342 }
1343
1344 #[test]
1345 fn complete_descriptor_graph_resolves_every_identity_and_signature() {
1346 validate_loaded_graph(&[complete_graph()]).unwrap();
1347 }
1348
1349 #[test]
1350 fn unresolved_reference_and_batch_capability_fail_closed() {
1351 let mut unresolved = complete_graph();
1352 unresolved.functions[0].arguments[0] = RegisteredTypeRef::External {
1353 object_id: [99; 16],
1354 codec_version: 1,
1355 };
1356 assert!(validate_loaded_graph(&[unresolved])
1357 .unwrap_err()
1358 .to_string()
1359 .contains("unresolved external type"));
1360
1361 let mut false_batch = complete_graph();
1362 false_batch.capabilities |= RADIX_PACKAGE_CAP_BATCH_FUNCTIONS;
1363 assert!(validate_loaded_graph(&[false_batch])
1364 .unwrap_err()
1365 .to_string()
1366 .contains("capability flags"));
1367 }
1368}