Skip to main content

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}