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/// Joins two key expressions with a `/` and canonizes the result.
100///
101/// # Arguments
102///
103/// * `prefix` - the leading expression, as null-terminated UTF-8.
104/// * `suffix` - the expression to place beneath it, as null-terminated UTF-8.
105///
106/// # Returns
107///
108/// A string the caller must release with
109/// [`pamoja_string_free`](crate::pamoja_string_free), or null if either side is
110/// empty or null, or the joined expression is malformed.
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_join(
118 prefix: *const c_char,
119 suffix: *const c_char,
120) -> *mut PamojaString {
121 let (Some(prefix), Some(suffix)) = (read_str(prefix, "prefix"), read_str(suffix, "suffix"))
122 else {
123 return ptr::null_mut();
124 };
125 match keyexpr::join(prefix, suffix) {
126 Some(joined) => PamojaString::into_raw(joined),
127 None => {
128 crate::set_last_error(format!(
129 "`{prefix}` and `{suffix}` do not join into a valid key expression"
130 ));
131 ptr::null_mut()
132 }
133 }
134}
135
136/// Reports whether a pattern selects a key.
137///
138/// A chunk that starts with `@` is verbatim: no wildcard selects it.
139///
140/// # Arguments
141///
142/// * `pattern` - the expression that may carry wildcards, as null-terminated
143/// UTF-8.
144/// * `key` - the concrete key to test against it, as null-terminated UTF-8.
145///
146/// # Returns
147///
148/// `true` when the pattern selects the key, or `false` if it does not or either
149/// argument is null.
150///
151/// # Safety
152///
153/// Both arguments must be valid null-terminated UTF-8 strings for the duration
154/// of the call, or null.
155#[no_mangle]
156pub unsafe extern "C" fn pamoja_keyexpr_matches(
157 pattern: *const c_char,
158 key: *const c_char,
159) -> bool {
160 let (Some(pattern), Some(key)) = (read_str(pattern, "pattern"), read_str(key, "key")) else {
161 return false;
162 };
163 keyexpr::matches(pattern, key)
164}
165
166/// Reports whether two key expressions share at least one key.
167///
168/// This is the relation Zenoh routes by: a publication on one reaches a
169/// subscriber on the other exactly when the two intersect.
170///
171/// # Arguments
172///
173/// * `a` - one expression, as null-terminated UTF-8.
174/// * `b` - the other expression, as null-terminated UTF-8.
175///
176/// # Returns
177///
178/// `true` when some key is selected by both, or `false` if none is, either
179/// expression is malformed, or either argument is null.
180///
181/// # Safety
182///
183/// Both arguments must be valid null-terminated UTF-8 strings for the duration
184/// of the call, or null.
185#[no_mangle]
186pub unsafe extern "C" fn pamoja_keyexpr_intersects(a: *const c_char, b: *const c_char) -> bool {
187 let (Some(a), Some(b)) = (read_str(a, "a"), read_str(b, "b")) else {
188 return false;
189 };
190 keyexpr::intersects(a, b)
191}
192
193/// Reports whether one key expression selects every key another one selects.
194///
195/// # Arguments
196///
197/// * `a` - the expression that may be the wider one, as null-terminated UTF-8.
198/// * `b` - the expression tested for being covered by `a`, as null-terminated
199/// UTF-8.
200///
201/// # Returns
202///
203/// `true` when every key `b` selects is also selected by `a`, or `false` if
204/// not, either expression is malformed, or either argument is null.
205///
206/// # Safety
207///
208/// Both arguments must be valid null-terminated UTF-8 strings for the duration
209/// of the call, or null.
210#[no_mangle]
211pub unsafe extern "C" fn pamoja_keyexpr_includes(a: *const c_char, b: *const c_char) -> bool {
212 let (Some(a), Some(b)) = (read_str(a, "a"), read_str(b, "b")) else {
213 return false;
214 };
215 keyexpr::includes(a, b)
216}
217
218#[cfg(test)]
219mod tests {
220 use std::ffi::CString;
221
222 use super::*;
223
224 #[test]
225 fn a_wildcard_pattern_selects_a_node_beneath_it() {
226 let pattern = CString::new("fleet/*/battery").expect("static");
227 let key = CString::new("fleet/n7/battery").expect("static");
228 assert!(unsafe { pamoja_keyexpr_matches(pattern.as_ptr(), key.as_ptr()) });
229 }
230
231 #[test]
232 fn a_redundant_double_wildcard_canonizes_away() {
233 let key = CString::new("fleet/**/**/battery").expect("static");
234 let canonical = unsafe { pamoja_keyexpr_canonize(key.as_ptr()) };
235 assert!(!canonical.is_null());
236 let text = unsafe { std::ffi::CStr::from_ptr(crate::pamoja_string_data(canonical)) };
237 assert_eq!(text.to_str().expect("utf-8"), "fleet/**/battery");
238 unsafe { crate::pamoja_string_free(canonical) };
239 }
240
241 #[test]
242 fn a_null_key_is_rejected_rather_than_dereferenced() {
243 assert!(!unsafe { pamoja_keyexpr_is_valid(ptr::null()) });
244 assert!(!unsafe { pamoja_keyexpr_is_canon(ptr::null()) });
245 assert!(unsafe { pamoja_keyexpr_canonize(ptr::null()) }.is_null());
246 assert!(!unsafe { pamoja_keyexpr_matches(ptr::null(), ptr::null()) });
247 assert!(unsafe { pamoja_keyexpr_join(ptr::null(), ptr::null()) }.is_null());
248 assert!(!unsafe { pamoja_keyexpr_intersects(ptr::null(), ptr::null()) });
249 assert!(!unsafe { pamoja_keyexpr_includes(ptr::null(), ptr::null()) });
250 }
251
252 #[test]
253 fn a_key_joins_beneath_a_prefix_in_canonical_form() {
254 let prefix = CString::new("fleet/**").expect("static");
255 let suffix = CString::new("*").expect("static");
256 let joined = unsafe { pamoja_keyexpr_join(prefix.as_ptr(), suffix.as_ptr()) };
257 assert!(!joined.is_null());
258 let text = unsafe { std::ffi::CStr::from_ptr(crate::pamoja_string_data(joined)) };
259 assert_eq!(text.to_str().expect("utf-8"), "fleet/*/**");
260 unsafe { crate::pamoja_string_free(joined) };
261
262 let empty = CString::new("").expect("static");
263 assert!(unsafe { pamoja_keyexpr_join(prefix.as_ptr(), empty.as_ptr()) }.is_null());
264 }
265
266 #[test]
267 fn two_patterns_relate_as_the_keys_they_select() {
268 let battery = CString::new("fleet/*/battery").expect("static");
269 let node = CString::new("fleet/n7/**").expect("static");
270 let fleet = CString::new("fleet/**").expect("static");
271 let sealed = CString::new("fleet/@v1/**").expect("static");
272 assert!(unsafe { pamoja_keyexpr_intersects(battery.as_ptr(), node.as_ptr()) });
273 assert!(unsafe { pamoja_keyexpr_includes(fleet.as_ptr(), battery.as_ptr()) });
274 assert!(!unsafe { pamoja_keyexpr_includes(battery.as_ptr(), fleet.as_ptr()) });
275 assert!(!unsafe { pamoja_keyexpr_intersects(fleet.as_ptr(), sealed.as_ptr()) });
276 }
277}