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/// 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}