Skip to main content

radixdb_plugin_host/
loader.rs

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
79/// Discover, validate, load, and atomically construct one immutable registry.
80/// No registry is returned if any configured package or descriptor is invalid.
81pub 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    // Deliberately leak immediately. Even a rejected descriptor may have run
380    // constructors or returned pointers; PLUG-20 never invokes `dlclose`.
381    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/// Admit a statically linked descriptor through the same copy and graph gates
495/// as the production loader. The caller keeps all callback code loaded for the
496/// registry lifetime; this helper is available only to tests.
497#[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}