pamoja_ffi/zenoh.rs
1//! The C ABI for Zenoh key expressions.
2//!
3//! These wrap [`pamoja_zenoh::keyexpr`] for callers that reach the SDK through
4//! the flat C boundary. A key expression is how a Zenoh network addresses data:
5//! a slash-separated path that may carry the `*` and `**` wildcards, so one
6//! subscriber can name a whole subtree of a fleet rather than each node in it.
7//!
8//! Only the naming rules cross. Running a Zenoh session needs the std-only
9//! zenoh stack, which stays behind the crate's `runtime` feature and out of the
10//! shipped libraries, so a caller who wants a live session uses the Rust crate.
11
12use std::ffi::c_char;
13use std::ptr;
14
15use pamoja_zenoh::keyexpr;
16
17use crate::{read_str, PamojaString};
18
19/// Reports whether a key expression is well formed.
20///
21/// # Arguments
22///
23/// * `key` - the expression to check, as null-terminated UTF-8.
24///
25/// # Returns
26///
27/// `true` when the expression is valid, or `false` if it is malformed or `key`
28/// is null.
29///
30/// # Safety
31///
32/// `key` must be a valid null-terminated UTF-8 string for the duration of the
33/// call, or null.
34#[no_mangle]
35pub unsafe extern "C" fn pamoja_keyexpr_is_valid(key: *const c_char) -> bool {
36 match read_str(key, "key") {
37 Some(key) => keyexpr::is_valid(key),
38 None => false,
39 }
40}
41
42/// Reports whether a key expression is already in its canonical form.
43///
44/// # Arguments
45///
46/// * `key` - the expression to check, as null-terminated UTF-8.
47///
48/// # Returns
49///
50/// `true` when the expression is canonical, or `false` if it is not or `key` is
51/// null.
52///
53/// # Safety
54///
55/// `key` must be a valid null-terminated UTF-8 string for the duration of the
56/// call, or null.
57#[no_mangle]
58pub unsafe extern "C" fn pamoja_keyexpr_is_canon(key: *const c_char) -> bool {
59 match read_str(key, "key") {
60 Some(key) => keyexpr::is_canon(key),
61 None => false,
62 }
63}
64
65/// Rewrites a key expression into its canonical form.
66///
67/// Two expressions that select the same data have one canonical form, so
68/// canonizing before comparing or routing avoids treating `a/**/**/b` and
69/// `a/**/b` as different.
70///
71/// # Arguments
72///
73/// * `key` - the expression to canonize, as null-terminated UTF-8.
74///
75/// # Returns
76///
77/// A string the caller must release with
78/// [`pamoja_string_free`](crate::pamoja_string_free), or null if the expression
79/// is malformed or `key` is null.
80///
81/// # Safety
82///
83/// `key` must be a valid null-terminated UTF-8 string for the duration of the
84/// call, or null.
85#[no_mangle]
86pub unsafe extern "C" fn pamoja_keyexpr_canonize(key: *const c_char) -> *mut PamojaString {
87 let Some(key) = read_str(key, "key") else {
88 return ptr::null_mut();
89 };
90 match keyexpr::canonize(key) {
91 Some(canonical) => PamojaString::into_raw(canonical),
92 None => {
93 crate::set_last_error(format!("`{key}` is not a valid key expression"));
94 ptr::null_mut()
95 }
96 }
97}
98
99/// Reports whether a pattern selects a key.
100///
101/// # Arguments
102///
103/// * `pattern` - the expression that may carry wildcards, as null-terminated
104/// UTF-8.
105/// * `key` - the concrete key to test against it, as null-terminated UTF-8.
106///
107/// # Returns
108///
109/// `true` when the pattern selects the key, or `false` if it does not or either
110/// argument is null.
111///
112/// # Safety
113///
114/// Both arguments must be valid null-terminated UTF-8 strings for the duration
115/// of the call, or null.
116#[no_mangle]
117pub unsafe extern "C" fn pamoja_keyexpr_matches(
118 pattern: *const c_char,
119 key: *const c_char,
120) -> bool {
121 let (Some(pattern), Some(key)) = (read_str(pattern, "pattern"), read_str(key, "key")) else {
122 return false;
123 };
124 keyexpr::matches(pattern, key)
125}
126
127#[cfg(test)]
128mod tests {
129 use std::ffi::CString;
130
131 use super::*;
132
133 #[test]
134 fn a_wildcard_pattern_selects_a_node_beneath_it() {
135 let pattern = CString::new("fleet/*/battery").expect("static");
136 let key = CString::new("fleet/n7/battery").expect("static");
137 assert!(unsafe { pamoja_keyexpr_matches(pattern.as_ptr(), key.as_ptr()) });
138 }
139
140 #[test]
141 fn a_redundant_double_wildcard_canonizes_away() {
142 let key = CString::new("fleet/**/**/battery").expect("static");
143 let canonical = unsafe { pamoja_keyexpr_canonize(key.as_ptr()) };
144 assert!(!canonical.is_null());
145 let text = unsafe { std::ffi::CStr::from_ptr(crate::pamoja_string_data(canonical)) };
146 assert_eq!(text.to_str().expect("utf-8"), "fleet/**/battery");
147 unsafe { crate::pamoja_string_free(canonical) };
148 }
149
150 #[test]
151 fn a_null_key_is_rejected_rather_than_dereferenced() {
152 assert!(!unsafe { pamoja_keyexpr_is_valid(ptr::null()) });
153 assert!(!unsafe { pamoja_keyexpr_is_canon(ptr::null()) });
154 assert!(unsafe { pamoja_keyexpr_canonize(ptr::null()) }.is_null());
155 assert!(!unsafe { pamoja_keyexpr_matches(ptr::null(), ptr::null()) });
156 }
157}