1#![allow(clippy::missing_safety_doc)]
44#![expect(
45 clippy::undocumented_unsafe_blocks,
46 reason = "module-wide FFI safety contract documented in the # Safety preamble above"
47)]
48#![expect(
49 clippy::multiple_unsafe_ops_per_block,
50 reason = "FFI entry points routinely deref + write to multiple out-parameter fields under the same caller contract; splitting per-op would obscure the single boundary-cross"
51)]
52
53use std::ffi::{c_char, c_int, CStr, CString};
54use std::mem::ManuallyDrop;
55use std::sync::Arc;
56
57use bytes::Bytes;
58use serde::{Deserialize, Serialize};
59use tokio::runtime::Runtime;
60
61use crate::adapter::net::identity::{
62 EntityId, PermissionToken, TokenCache, TokenError as CoreTokenError, TokenScope,
63};
64use crate::adapter::net::{
65 ChannelConfig as InnerChannelConfig, ChannelConfigRegistry, ChannelHash, ChannelId,
66 ChannelName as InnerChannelName, ChannelPublisher, EntityKeypair, MeshNode, MeshNodeConfig,
67 OnFailure as InnerOnFailure, PublishConfig as InnerPublishConfig,
68 PublishReport as InnerPublishReport, Reliability, Stream as CoreStream, StreamConfig,
69 StreamError, Visibility as InnerVisibility, DEFAULT_STREAM_WINDOW_BYTES,
70};
71use crate::adapter::net::{SubnetId, SubnetPolicy, SubnetRule};
72use crate::adapter::Adapter;
73use crate::error::AdapterError;
74
75use super::handle_guard::{HandleGuard, FFI_HANDLE_FREE_DEADLINE};
76use super::NetError;
77
78pub(crate) const NET_ERR_MESH_INIT: c_int = -110;
84pub(crate) const NET_ERR_MESH_HANDSHAKE: c_int = -111;
85pub(crate) const NET_ERR_MESH_BACKPRESSURE: c_int = -112;
86pub(crate) const NET_ERR_MESH_NOT_CONNECTED: c_int = -113;
87pub(crate) const NET_ERR_MESH_TRANSPORT: c_int = -114;
88pub(crate) const NET_ERR_CHANNEL: c_int = -115;
89pub(crate) const NET_ERR_CHANNEL_AUTH: c_int = -116;
90
91pub(crate) const NET_ERR_IDENTITY: c_int = -120;
96pub(crate) const NET_ERR_TOKEN_INVALID_FORMAT: c_int = -121;
97pub(crate) const NET_ERR_TOKEN_INVALID_SIGNATURE: c_int = -122;
98pub(crate) const NET_ERR_TOKEN_EXPIRED: c_int = -123;
99pub(crate) const NET_ERR_TOKEN_NOT_YET_VALID: c_int = -124;
100pub(crate) const NET_ERR_TOKEN_DELEGATION_EXHAUSTED: c_int = -125;
101pub(crate) const NET_ERR_TOKEN_DELEGATION_NOT_ALLOWED: c_int = -126;
102pub(crate) const NET_ERR_TOKEN_NOT_AUTHORIZED: c_int = -127;
103
104#[cfg(feature = "nat-traversal")]
117pub(crate) const NET_ERR_TRAVERSAL_REFLEX_TIMEOUT: c_int = -130;
118#[cfg(feature = "nat-traversal")]
119pub(crate) const NET_ERR_TRAVERSAL_PEER_NOT_REACHABLE: c_int = -131;
120#[cfg(feature = "nat-traversal")]
121pub(crate) const NET_ERR_TRAVERSAL_TRANSPORT: c_int = -132;
122#[cfg(feature = "nat-traversal")]
123pub(crate) const NET_ERR_TRAVERSAL_RENDEZVOUS_NO_RELAY: c_int = -133;
124#[cfg(feature = "nat-traversal")]
125pub(crate) const NET_ERR_TRAVERSAL_RENDEZVOUS_REJECTED: c_int = -134;
126#[cfg(feature = "nat-traversal")]
127pub(crate) const NET_ERR_TRAVERSAL_PUNCH_FAILED: c_int = -135;
128#[cfg(feature = "nat-traversal")]
129pub(crate) const NET_ERR_TRAVERSAL_PORT_MAP_UNAVAILABLE: c_int = -136;
130pub(crate) const NET_ERR_TRAVERSAL_UNSUPPORTED: c_int = -137;
136
137#[cfg(feature = "nat-traversal")]
138fn traversal_err_to_code(e: &crate::adapter::net::traversal::TraversalError) -> c_int {
139 use crate::adapter::net::traversal::TraversalError;
140 match e {
141 TraversalError::ReflexTimeout => NET_ERR_TRAVERSAL_REFLEX_TIMEOUT,
142 TraversalError::PeerNotReachable => NET_ERR_TRAVERSAL_PEER_NOT_REACHABLE,
143 TraversalError::Transport(_) => NET_ERR_TRAVERSAL_TRANSPORT,
144 TraversalError::RendezvousNoRelay => NET_ERR_TRAVERSAL_RENDEZVOUS_NO_RELAY,
145 TraversalError::RendezvousRejected(_) => NET_ERR_TRAVERSAL_RENDEZVOUS_REJECTED,
146 TraversalError::PunchFailed => NET_ERR_TRAVERSAL_PUNCH_FAILED,
147 TraversalError::PortMapUnavailable => NET_ERR_TRAVERSAL_PORT_MAP_UNAVAILABLE,
148 TraversalError::Unsupported => NET_ERR_TRAVERSAL_UNSUPPORTED,
149 }
150}
151
152#[cfg(feature = "nat-traversal")]
156fn nat_class_to_str(class: crate::adapter::net::traversal::classify::NatClass) -> &'static str {
157 use crate::adapter::net::traversal::classify::NatClass;
158 match class {
159 NatClass::Open => "open",
160 NatClass::Cone => "cone",
161 NatClass::Symmetric => "symmetric",
162 NatClass::Unknown => "unknown",
163 }
164}
165
166fn token_err_to_code(e: &CoreTokenError) -> c_int {
167 match e {
168 CoreTokenError::InvalidFormat => NET_ERR_TOKEN_INVALID_FORMAT,
169 CoreTokenError::InvalidSignature => NET_ERR_TOKEN_INVALID_SIGNATURE,
170 CoreTokenError::Expired => NET_ERR_TOKEN_EXPIRED,
171 CoreTokenError::NotYetValid => NET_ERR_TOKEN_NOT_YET_VALID,
172 CoreTokenError::DelegationExhausted => NET_ERR_TOKEN_DELEGATION_EXHAUSTED,
173 CoreTokenError::DelegationNotAllowed => NET_ERR_TOKEN_DELEGATION_NOT_ALLOWED,
174 CoreTokenError::NotAuthorized => NET_ERR_TOKEN_NOT_AUTHORIZED,
175 CoreTokenError::ReadOnly => NET_ERR_IDENTITY,
180 CoreTokenError::ZeroTtl => NET_ERR_TOKEN_INVALID_FORMAT,
187 }
188}
189
190fn runtime() -> &'static Arc<Runtime> {
206 use std::sync::OnceLock;
207 static RT: OnceLock<Arc<Runtime>> = OnceLock::new();
208 RT.get_or_init(|| {
209 match tokio::runtime::Builder::new_multi_thread()
210 .enable_all()
211 .build()
212 {
213 Ok(rt) => Arc::new(rt),
214 Err(e) => {
215 eprintln!(
216 "FATAL: mesh FFI tokio runtime build failure ({e:?}); aborting to avoid panic across the FFI boundary"
217 );
218 std::process::abort();
219 }
220 }
221 })
222}
223
224pub(super) fn block_on<F: std::future::Future>(future: F) -> F::Output {
244 if tokio::runtime::Handle::try_current().is_ok() {
245 eprintln!(
246 "FATAL: mesh FFI called from inside a tokio runtime context; \
247 aborting to avoid runtime-in-runtime panic across the FFI boundary"
248 );
249 std::process::abort();
250 }
251 runtime().block_on(future)
252}
253
254#[inline]
277pub(super) unsafe fn c_str_to_string(p: *const c_char) -> Option<String> {
278 if p.is_null() {
279 return None;
280 }
281 CStr::from_ptr(p).to_str().ok().map(str::to_owned)
282}
283
284fn write_json_out<T: Serialize>(
290 value: &T,
291 out_ptr: *mut *mut c_char,
292 out_len: *mut usize,
293) -> c_int {
294 if out_ptr.is_null() || out_len.is_null() {
295 return NetError::NullPointer.into();
296 }
297 let Ok(s) = serde_json::to_string(value) else {
298 return NetError::Unknown.into();
299 };
300 let len = s.len();
301 let Ok(cs) = CString::new(s) else {
302 return NetError::Unknown.into();
303 };
304 unsafe {
305 *out_ptr = cs.into_raw();
306 *out_len = len;
307 }
308 0
309}
310
311pub(super) fn write_string_out(s: String, out_ptr: *mut *mut c_char, out_len: *mut usize) -> c_int {
312 if out_ptr.is_null() || out_len.is_null() {
313 return NetError::NullPointer.into();
314 }
315 let len = s.len();
316 let Ok(cs) = CString::new(s) else {
317 return NetError::Unknown.into();
318 };
319 unsafe {
320 *out_ptr = cs.into_raw();
321 *out_len = len;
322 }
323 0
324}
325
326fn adapter_err_to_code(err: &AdapterError) -> c_int {
327 match err {
328 AdapterError::Connection(_) => NET_ERR_MESH_HANDSHAKE,
329 _ => NET_ERR_MESH_TRANSPORT,
330 }
331}
332
333fn stream_err_to_code(err: &StreamError) -> c_int {
334 match err {
335 StreamError::Backpressure => NET_ERR_MESH_BACKPRESSURE,
336 StreamError::NotConnected => NET_ERR_MESH_NOT_CONNECTED,
337 StreamError::Transport(_) => NET_ERR_MESH_TRANSPORT,
338 }
339}
340
341#[derive(Deserialize)]
346struct SubnetPolicyJson {
347 #[serde(default)]
348 rules: Vec<SubnetRuleJson>,
349}
350
351#[derive(Deserialize)]
352struct SubnetRuleJson {
353 tag_prefix: String,
354 level: u32,
355 #[serde(default)]
356 values: std::collections::HashMap<String, u32>,
357}
358
359fn u8_from_u32(value: u32) -> Option<u8> {
360 if value > 255 {
361 None
362 } else {
363 Some(value as u8)
364 }
365}
366
367fn subnet_id_from_json(levels: Vec<u32>) -> Option<SubnetId> {
368 if levels.is_empty() || levels.len() > 4 {
369 return None;
370 }
371 let mut bytes = [0u8; 4];
372 for (i, raw) in levels.iter().enumerate() {
373 bytes[i] = u8_from_u32(*raw)?;
374 }
375 Some(SubnetId::new(&bytes[..levels.len()]))
376}
377
378fn subnet_policy_from_json(p: SubnetPolicyJson) -> Option<SubnetPolicy> {
379 let mut policy = SubnetPolicy::new();
380 for rule_json in p.rules {
381 let level = u8_from_u32(rule_json.level)?;
382 if level > 3 {
383 return None;
384 }
385 let mut rule = SubnetRule::new(rule_json.tag_prefix, level);
386 for (tag_value, raw_val) in rule_json.values {
387 let v = u8_from_u32(raw_val)?;
388 if v == 0 {
394 return None;
395 }
396 rule = rule.map(tag_value, v);
397 }
398 policy = policy.add_rule(rule);
399 }
400 Some(policy)
401}
402
403#[derive(Deserialize)]
404struct MeshNewConfig {
405 bind_addr: String,
406 psk_hex: String,
408 heartbeat_ms: Option<u64>,
409 session_timeout_ms: Option<u64>,
410 num_shards: Option<u16>,
411 capability_gc_interval_ms: Option<u64>,
414 require_signed_capabilities: Option<bool>,
417 subnet: Option<Vec<u32>>,
419 subnet_policy: Option<SubnetPolicyJson>,
421 identity_seed_hex: Option<String>,
426 #[serde(default)]
432 reflex_override: Option<String>,
433 #[serde(default)]
437 try_port_mapping: bool,
438}
439
440pub struct MeshNodeHandle {
453 inner: ManuallyDrop<Arc<MeshNode>>,
454 channel_configs: ManuallyDrop<Arc<ChannelConfigRegistry>>,
455 guard: HandleGuard,
456}
457
458#[unsafe(no_mangle)]
473pub unsafe extern "C" fn net_mesh_new(
474 config_json: *const c_char,
475 out_handle: *mut *mut MeshNodeHandle,
476) -> c_int {
477 if config_json.is_null() || out_handle.is_null() {
478 return NetError::NullPointer.into();
479 }
480 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
481 return NetError::InvalidUtf8.into();
482 };
483 let cfg: MeshNewConfig = match serde_json::from_str(&s) {
484 Ok(v) => v,
485 Err(_) => return NetError::InvalidJson.into(),
486 };
487 let bind_addr: std::net::SocketAddr = match cfg.bind_addr.parse() {
488 Ok(a) => a,
489 Err(_) => return NET_ERR_MESH_INIT,
490 };
491 let psk_bytes = match hex::decode(&cfg.psk_hex) {
492 Ok(b) => b,
493 Err(_) => return NET_ERR_MESH_INIT,
494 };
495 if psk_bytes.len() != 32 {
496 return NET_ERR_MESH_INIT;
497 }
498 let mut psk = [0u8; 32];
499 psk.copy_from_slice(&psk_bytes);
500
501 let mut node_cfg = MeshNodeConfig::new(bind_addr, psk);
502 if let Some(ms) = cfg.heartbeat_ms {
510 if ms == 0 {
511 return NetError::InvalidJson.into();
512 }
513 node_cfg = node_cfg.with_heartbeat_interval(std::time::Duration::from_millis(ms));
514 }
515 if let Some(ms) = cfg.session_timeout_ms {
516 if ms == 0 {
517 return NetError::InvalidJson.into();
518 }
519 node_cfg = node_cfg.with_session_timeout(std::time::Duration::from_millis(ms));
520 }
521 if let Some(n) = cfg.num_shards {
522 node_cfg = node_cfg.with_num_shards(n);
523 }
524 if let Some(ms) = cfg.capability_gc_interval_ms {
525 node_cfg = node_cfg.with_capability_gc_interval(std::time::Duration::from_millis(ms));
526 }
527 if let Some(b) = cfg.require_signed_capabilities {
528 node_cfg = node_cfg.with_require_signed_capabilities(b);
529 }
530 if let Some(levels) = cfg.subnet {
531 let Some(id) = subnet_id_from_json(levels) else {
532 return NET_ERR_MESH_INIT;
533 };
534 node_cfg = node_cfg.with_subnet(id);
535 }
536 if let Some(policy_js) = cfg.subnet_policy {
537 let Some(policy) = subnet_policy_from_json(policy_js) else {
538 return NET_ERR_MESH_INIT;
539 };
540 node_cfg = node_cfg.with_subnet_policy(Arc::new(policy));
541 }
542 #[cfg(feature = "nat-traversal")]
543 if let Some(external_str) = cfg.reflex_override.as_deref() {
544 let Ok(external) = external_str.parse::<std::net::SocketAddr>() else {
545 return NET_ERR_MESH_INIT;
546 };
547 node_cfg = node_cfg.with_reflex_override(external);
548 }
549 #[cfg(not(feature = "nat-traversal"))]
553 let _ = cfg.reflex_override;
554 #[cfg(feature = "port-mapping")]
555 if cfg.try_port_mapping {
556 node_cfg = node_cfg.with_try_port_mapping(true);
557 }
558 #[cfg(not(feature = "port-mapping"))]
560 let _ = cfg.try_port_mapping;
561
562 let identity = match cfg.identity_seed_hex {
563 Some(seed_hex) => {
564 let bytes = match hex::decode(&seed_hex) {
565 Ok(b) => b,
566 Err(_) => return NET_ERR_MESH_INIT,
567 };
568 if bytes.len() != 32 {
569 return NET_ERR_MESH_INIT;
570 }
571 let mut arr = [0u8; 32];
572 arr.copy_from_slice(&bytes);
573 EntityKeypair::from_bytes(arr)
574 }
575 None => EntityKeypair::generate(),
576 };
577 let result = block_on(async move { MeshNode::new(identity, node_cfg).await });
578 match result {
579 Ok(mut node) => {
580 let channel_configs = Arc::new(ChannelConfigRegistry::new());
581 node.set_channel_configs(channel_configs.clone());
582 node.set_token_cache(Arc::new(TokenCache::new()));
586 let handle = Box::new(MeshNodeHandle {
587 inner: ManuallyDrop::new(Arc::new(node)),
588 channel_configs: ManuallyDrop::new(channel_configs),
589 guard: HandleGuard::new(),
590 });
591 unsafe {
592 *out_handle = Box::into_raw(handle);
593 }
594 0
595 }
596 Err(_) => NET_ERR_MESH_INIT,
597 }
598}
599
600#[unsafe(no_mangle)]
601pub unsafe extern "C" fn net_mesh_free(handle: *mut MeshNodeHandle) {
602 if handle.is_null() {
603 return;
604 }
605 let h: &MeshNodeHandle = unsafe { &*handle };
610 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
611 unsafe {
613 let mh = &mut *handle;
614 let inner = ManuallyDrop::take(&mut mh.inner);
615 let configs = ManuallyDrop::take(&mut mh.channel_configs);
616 drop(inner);
617 drop(configs);
618 }
619 } else {
620 tracing::warn!(
621 "net_mesh_free: in-flight ops did not drain within deadline; \
622 leaking inner to avoid use-after-free"
623 );
624 }
625}
626
627#[cfg(feature = "cortex")]
636pub(super) fn mesh_node_arc(h: &MeshNodeHandle) -> Arc<MeshNode> {
637 Arc::clone(&h.inner)
638}
639
640#[unsafe(no_mangle)]
648pub unsafe extern "C" fn net_mesh_arc_clone(handle: *mut MeshNodeHandle) -> *mut Arc<MeshNode> {
649 if handle.is_null() {
650 return std::ptr::null_mut();
651 }
652 let h = unsafe { &*handle };
653 let _op = match h.guard.try_enter() {
655 Some(op) => op,
656 None => return std::ptr::null_mut(),
657 };
658 let cloned: Arc<MeshNode> = Arc::clone(&h.inner);
659 Box::into_raw(Box::new(cloned))
660}
661
662#[unsafe(no_mangle)]
669pub unsafe extern "C" fn net_mesh_channel_configs_arc_clone(
670 handle: *mut MeshNodeHandle,
671) -> *mut Arc<ChannelConfigRegistry> {
672 if handle.is_null() {
673 return std::ptr::null_mut();
674 }
675 let h = unsafe { &*handle };
676 let _op = match h.guard.try_enter() {
678 Some(op) => op,
679 None => return std::ptr::null_mut(),
680 };
681 let cloned: Arc<ChannelConfigRegistry> = Arc::clone(&h.channel_configs);
682 Box::into_raw(Box::new(cloned))
683}
684
685#[unsafe(no_mangle)]
688pub unsafe extern "C" fn net_mesh_arc_free(p: *mut Arc<MeshNode>) {
689 if p.is_null() {
690 return;
691 }
692 unsafe {
693 drop(Box::from_raw(p));
694 }
695}
696
697#[unsafe(no_mangle)]
700pub unsafe extern "C" fn net_mesh_channel_configs_arc_free(p: *mut Arc<ChannelConfigRegistry>) {
701 if p.is_null() {
702 return;
703 }
704 unsafe {
705 drop(Box::from_raw(p));
706 }
707}
708
709#[unsafe(no_mangle)]
712pub unsafe extern "C" fn net_mesh_public_key_hex(
713 handle: *mut MeshNodeHandle,
714 out_ptr: *mut *mut c_char,
715 out_len: *mut usize,
716) -> c_int {
717 if handle.is_null() || out_ptr.is_null() || out_len.is_null() {
718 return NetError::NullPointer.into();
719 }
720 let h = unsafe { &*handle };
721 let _op = match h.guard.try_enter() {
722 Some(op) => op,
723 None => return NetError::ShuttingDown.into(),
724 };
725 let s = hex::encode(h.inner.public_key());
726 write_string_out(s, out_ptr, out_len)
727}
728
729#[unsafe(no_mangle)]
730pub unsafe extern "C" fn net_mesh_node_id(handle: *mut MeshNodeHandle) -> u64 {
731 if handle.is_null() {
732 return 0;
733 }
734 let h = unsafe { &*handle };
735 let _op = match h.guard.try_enter() {
737 Some(op) => op,
738 None => return 0,
739 };
740 h.inner.node_id()
741}
742
743#[unsafe(no_mangle)]
747pub unsafe extern "C" fn net_mesh_entity_id(handle: *mut MeshNodeHandle, out: *mut u8) -> c_int {
748 if handle.is_null() || out.is_null() {
749 return NetError::NullPointer.into();
750 }
751 let h = unsafe { &*handle };
752 let _op = match h.guard.try_enter() {
753 Some(op) => op,
754 None => return NetError::ShuttingDown.into(),
755 };
756 let bytes = h.inner.entity_id().as_bytes();
757 unsafe {
758 std::ptr::copy_nonoverlapping(bytes.as_ptr(), out, 32);
759 }
760 0
761}
762
763#[unsafe(no_mangle)]
765pub unsafe extern "C" fn net_mesh_connect(
766 handle: *mut MeshNodeHandle,
767 peer_addr: *const c_char,
768 peer_pubkey_hex: *const c_char,
769 peer_node_id: u64,
770) -> c_int {
771 if handle.is_null() || peer_addr.is_null() || peer_pubkey_hex.is_null() {
772 return NetError::NullPointer.into();
773 }
774 let h = unsafe { &*handle };
775 let _op = match h.guard.try_enter() {
776 Some(op) => op,
777 None => return NetError::ShuttingDown.into(),
778 };
779 let Some(addr_s) = (unsafe { c_str_to_string(peer_addr) }) else {
780 return NetError::InvalidUtf8.into();
781 };
782 let addr: std::net::SocketAddr = match addr_s.parse() {
783 Ok(a) => a,
784 Err(_) => return NET_ERR_MESH_HANDSHAKE,
785 };
786 let Some(pk_s) = (unsafe { c_str_to_string(peer_pubkey_hex) }) else {
787 return NetError::InvalidUtf8.into();
788 };
789 let pk_bytes = match hex::decode(pk_s) {
790 Ok(b) => b,
791 Err(_) => return NET_ERR_MESH_HANDSHAKE,
792 };
793 if pk_bytes.len() != 32 {
794 return NET_ERR_MESH_HANDSHAKE;
795 }
796 let mut pk = [0u8; 32];
797 pk.copy_from_slice(&pk_bytes);
798
799 let node = h.inner.clone();
800 match block_on(async move { node.connect(addr, &pk, peer_node_id).await }) {
801 Ok(_) => 0,
802 Err(e) => adapter_err_to_code(&e),
803 }
804}
805
806#[unsafe(no_mangle)]
809pub unsafe extern "C" fn net_mesh_accept(
810 handle: *mut MeshNodeHandle,
811 peer_node_id: u64,
812 out_addr: *mut *mut c_char,
813 out_len: *mut usize,
814) -> c_int {
815 if handle.is_null() || out_addr.is_null() || out_len.is_null() {
816 return NetError::NullPointer.into();
817 }
818 let h = unsafe { &*handle };
819 let _op = match h.guard.try_enter() {
820 Some(op) => op,
821 None => return NetError::ShuttingDown.into(),
822 };
823 let node = h.inner.clone();
824 match block_on(async move { node.accept(peer_node_id).await }) {
825 Ok((addr, _)) => write_string_out(addr.to_string(), out_addr, out_len),
826 Err(e) => adapter_err_to_code(&e),
827 }
828}
829
830#[unsafe(no_mangle)]
831pub unsafe extern "C" fn net_mesh_start(handle: *mut MeshNodeHandle) -> c_int {
832 if handle.is_null() {
833 return NetError::NullPointer.into();
834 }
835 let h = unsafe { &*handle };
836 let _op = match h.guard.try_enter() {
837 Some(op) => op,
838 None => return NetError::ShuttingDown.into(),
839 };
840 let node = h.inner.clone();
841 block_on(async move { node.start() });
844 0
845}
846
847#[unsafe(no_mangle)]
859pub unsafe extern "C" fn net_mesh_shutdown(handle: *mut MeshNodeHandle) -> c_int {
860 if handle.is_null() {
861 return NetError::NullPointer.into();
862 }
863 let h = unsafe { &*handle };
864 let _op = match h.guard.try_enter() {
865 Some(op) => op,
866 None => return NetError::ShuttingDown.into(),
867 };
868 match block_on(async { h.inner.shutdown().await }) {
869 Ok(()) => 0,
870 Err(e) => adapter_err_to_code(&e),
871 }
872}
873
874#[cfg(feature = "nat-traversal")]
896#[unsafe(no_mangle)]
897pub unsafe extern "C" fn net_mesh_nat_type(
898 handle: *mut MeshNodeHandle,
899 out_str: *mut *mut c_char,
900 out_len: *mut usize,
901) -> c_int {
902 if handle.is_null() || out_str.is_null() || out_len.is_null() {
903 return NetError::NullPointer.into();
904 }
905 let h = unsafe { &*handle };
906 let _op = match h.guard.try_enter() {
907 Some(op) => op,
908 None => return NetError::ShuttingDown.into(),
909 };
910 write_string_out(
911 nat_class_to_str(h.inner.nat_class()).to_string(),
912 out_str,
913 out_len,
914 )
915}
916
917#[cfg(feature = "nat-traversal")]
922#[unsafe(no_mangle)]
923pub unsafe extern "C" fn net_mesh_reflex_addr(
924 handle: *mut MeshNodeHandle,
925 out_str: *mut *mut c_char,
926 out_len: *mut usize,
927) -> c_int {
928 if handle.is_null() || out_str.is_null() || out_len.is_null() {
929 return NetError::NullPointer.into();
930 }
931 let h = unsafe { &*handle };
932 let _op = match h.guard.try_enter() {
933 Some(op) => op,
934 None => return NetError::ShuttingDown.into(),
935 };
936 let s = h
937 .inner
938 .reflex_addr()
939 .map(|a| a.to_string())
940 .unwrap_or_default();
941 write_string_out(s, out_str, out_len)
942}
943
944#[cfg(feature = "nat-traversal")]
948#[unsafe(no_mangle)]
949pub unsafe extern "C" fn net_mesh_peer_nat_type(
950 handle: *mut MeshNodeHandle,
951 peer_node_id: u64,
952 out_str: *mut *mut c_char,
953 out_len: *mut usize,
954) -> c_int {
955 if handle.is_null() || out_str.is_null() || out_len.is_null() {
956 return NetError::NullPointer.into();
957 }
958 let h = unsafe { &*handle };
959 let _op = match h.guard.try_enter() {
960 Some(op) => op,
961 None => return NetError::ShuttingDown.into(),
962 };
963 write_string_out(
964 nat_class_to_str(h.inner.peer_nat_class(peer_node_id)).to_string(),
965 out_str,
966 out_len,
967 )
968}
969
970#[cfg(feature = "nat-traversal")]
979#[unsafe(no_mangle)]
980pub unsafe extern "C" fn net_mesh_probe_reflex(
981 handle: *mut MeshNodeHandle,
982 peer_node_id: u64,
983 out_str: *mut *mut c_char,
984 out_len: *mut usize,
985) -> c_int {
986 if handle.is_null() || out_str.is_null() || out_len.is_null() {
987 return NetError::NullPointer.into();
988 }
989 let h = unsafe { &*handle };
990 let _op = match h.guard.try_enter() {
991 Some(op) => op,
992 None => return NetError::ShuttingDown.into(),
993 };
994 let node = h.inner.clone();
995 match block_on(async move { node.probe_reflex(peer_node_id).await }) {
996 Ok(addr) => write_string_out(addr.to_string(), out_str, out_len),
997 Err(e) => traversal_err_to_code(&e),
998 }
999}
1000
1001#[cfg(feature = "nat-traversal")]
1006#[unsafe(no_mangle)]
1007pub unsafe extern "C" fn net_mesh_reclassify_nat(handle: *mut MeshNodeHandle) -> c_int {
1008 if handle.is_null() {
1009 return NetError::NullPointer.into();
1010 }
1011 let h = unsafe { &*handle };
1012 let _op = match h.guard.try_enter() {
1013 Some(op) => op,
1014 None => return NetError::ShuttingDown.into(),
1015 };
1016 let node = h.inner.clone();
1017 block_on(async move { node.reclassify_nat().await });
1018 0
1019}
1020
1021#[cfg(feature = "nat-traversal")]
1026#[unsafe(no_mangle)]
1027pub unsafe extern "C" fn net_mesh_traversal_stats(
1028 handle: *mut MeshNodeHandle,
1029 out_punches_attempted: *mut u64,
1030 out_punches_succeeded: *mut u64,
1031 out_relay_fallbacks: *mut u64,
1032) -> c_int {
1033 if handle.is_null() {
1034 return NetError::NullPointer.into();
1035 }
1036 let h = unsafe { &*handle };
1037 let _op = match h.guard.try_enter() {
1038 Some(op) => op,
1039 None => return NetError::ShuttingDown.into(),
1040 };
1041 let snap = h.inner.traversal_stats();
1042 unsafe {
1043 if !out_punches_attempted.is_null() {
1044 *out_punches_attempted = snap.punches_attempted;
1045 }
1046 if !out_punches_succeeded.is_null() {
1047 *out_punches_succeeded = snap.punches_succeeded;
1048 }
1049 if !out_relay_fallbacks.is_null() {
1050 *out_relay_fallbacks = snap.relay_fallbacks;
1051 }
1052 }
1053 0
1054}
1055
1056#[cfg(feature = "nat-traversal")]
1068#[unsafe(no_mangle)]
1069pub unsafe extern "C" fn net_mesh_connect_direct(
1070 handle: *mut MeshNodeHandle,
1071 peer_node_id: u64,
1072 peer_pubkey_hex: *const c_char,
1073 coordinator: u64,
1074) -> c_int {
1075 if handle.is_null() || peer_pubkey_hex.is_null() {
1076 return NetError::NullPointer.into();
1077 }
1078 let h = unsafe { &*handle };
1079 let _op = match h.guard.try_enter() {
1080 Some(op) => op,
1081 None => return NetError::ShuttingDown.into(),
1082 };
1083 let Some(pk_s) = (unsafe { c_str_to_string(peer_pubkey_hex) }) else {
1084 return NetError::InvalidUtf8.into();
1085 };
1086 let pk_bytes = match hex::decode(pk_s) {
1087 Ok(b) => b,
1088 Err(_) => return NET_ERR_MESH_HANDSHAKE,
1089 };
1090 if pk_bytes.len() != 32 {
1091 return NET_ERR_MESH_HANDSHAKE;
1092 }
1093 let mut pk = [0u8; 32];
1094 pk.copy_from_slice(&pk_bytes);
1095
1096 let node = h.inner.clone();
1097 match block_on(async move { node.connect_direct(peer_node_id, &pk, coordinator).await }) {
1098 Ok(_) => 0,
1099 Err(e) => traversal_err_to_code(&e),
1100 }
1101}
1102
1103#[cfg(feature = "nat-traversal")]
1111#[unsafe(no_mangle)]
1112pub unsafe extern "C" fn net_mesh_set_reflex_override(
1113 handle: *mut MeshNodeHandle,
1114 external: *const c_char,
1115) -> c_int {
1116 if handle.is_null() || external.is_null() {
1117 return NetError::NullPointer.into();
1118 }
1119 let h = unsafe { &*handle };
1120 let _op = match h.guard.try_enter() {
1121 Some(op) => op,
1122 None => return NetError::ShuttingDown.into(),
1123 };
1124 let Some(s) = (unsafe { c_str_to_string(external) }) else {
1125 return NetError::InvalidUtf8.into();
1126 };
1127 let Ok(addr) = s.parse::<std::net::SocketAddr>() else {
1128 return NET_ERR_MESH_INIT;
1129 };
1130 h.inner.set_reflex_override(addr);
1131 0
1132}
1133
1134#[cfg(feature = "nat-traversal")]
1142#[unsafe(no_mangle)]
1143pub unsafe extern "C" fn net_mesh_clear_reflex_override(handle: *mut MeshNodeHandle) -> c_int {
1144 if handle.is_null() {
1145 return NetError::NullPointer.into();
1146 }
1147 let h = unsafe { &*handle };
1148 let _op = match h.guard.try_enter() {
1149 Some(op) => op,
1150 None => return NetError::ShuttingDown.into(),
1151 };
1152 h.inner.clear_reflex_override();
1153 0
1154}
1155
1156#[cfg(not(feature = "nat-traversal"))]
1179#[unsafe(no_mangle)]
1180pub unsafe extern "C" fn net_mesh_nat_type(
1181 _handle: *mut MeshNodeHandle,
1182 _out_str: *mut *mut c_char,
1183 _out_len: *mut usize,
1184) -> c_int {
1185 NET_ERR_TRAVERSAL_UNSUPPORTED
1186}
1187
1188#[cfg(not(feature = "nat-traversal"))]
1189#[unsafe(no_mangle)]
1190pub unsafe extern "C" fn net_mesh_reflex_addr(
1191 _handle: *mut MeshNodeHandle,
1192 _out_str: *mut *mut c_char,
1193 _out_len: *mut usize,
1194) -> c_int {
1195 NET_ERR_TRAVERSAL_UNSUPPORTED
1196}
1197
1198#[cfg(not(feature = "nat-traversal"))]
1199#[unsafe(no_mangle)]
1200pub unsafe extern "C" fn net_mesh_peer_nat_type(
1201 _handle: *mut MeshNodeHandle,
1202 _peer_node_id: u64,
1203 _out_str: *mut *mut c_char,
1204 _out_len: *mut usize,
1205) -> c_int {
1206 NET_ERR_TRAVERSAL_UNSUPPORTED
1207}
1208
1209#[cfg(not(feature = "nat-traversal"))]
1210#[unsafe(no_mangle)]
1211pub unsafe extern "C" fn net_mesh_probe_reflex(
1212 _handle: *mut MeshNodeHandle,
1213 _peer_node_id: u64,
1214 _out_str: *mut *mut c_char,
1215 _out_len: *mut usize,
1216) -> c_int {
1217 NET_ERR_TRAVERSAL_UNSUPPORTED
1218}
1219
1220#[cfg(not(feature = "nat-traversal"))]
1221#[unsafe(no_mangle)]
1222pub unsafe extern "C" fn net_mesh_reclassify_nat(_handle: *mut MeshNodeHandle) -> c_int {
1223 NET_ERR_TRAVERSAL_UNSUPPORTED
1224}
1225
1226#[cfg(not(feature = "nat-traversal"))]
1227#[unsafe(no_mangle)]
1228pub unsafe extern "C" fn net_mesh_traversal_stats(
1229 _handle: *mut MeshNodeHandle,
1230 _out_punches_attempted: *mut u64,
1231 _out_punches_succeeded: *mut u64,
1232 _out_relay_fallbacks: *mut u64,
1233) -> c_int {
1234 NET_ERR_TRAVERSAL_UNSUPPORTED
1235}
1236
1237#[cfg(not(feature = "nat-traversal"))]
1238#[unsafe(no_mangle)]
1239pub unsafe extern "C" fn net_mesh_connect_direct(
1240 _handle: *mut MeshNodeHandle,
1241 _peer_node_id: u64,
1242 _peer_pubkey_hex: *const c_char,
1243 _coordinator: u64,
1244) -> c_int {
1245 NET_ERR_TRAVERSAL_UNSUPPORTED
1246}
1247
1248#[cfg(not(feature = "nat-traversal"))]
1249#[unsafe(no_mangle)]
1250pub unsafe extern "C" fn net_mesh_set_reflex_override(
1251 _handle: *mut MeshNodeHandle,
1252 _external: *const c_char,
1253) -> c_int {
1254 NET_ERR_TRAVERSAL_UNSUPPORTED
1255}
1256
1257#[cfg(not(feature = "nat-traversal"))]
1258#[unsafe(no_mangle)]
1259pub unsafe extern "C" fn net_mesh_clear_reflex_override(_handle: *mut MeshNodeHandle) -> c_int {
1260 NET_ERR_TRAVERSAL_UNSUPPORTED
1261}
1262
1263#[derive(Deserialize, Default)]
1268struct StreamOpenConfig {
1269 reliability: Option<String>,
1271 window_bytes: Option<u32>,
1274 fairness_weight: Option<u8>,
1275}
1276
1277pub struct MeshStreamHandle {
1292 stream: ManuallyDrop<CoreStream>,
1293 _node: ManuallyDrop<Arc<MeshNode>>,
1296 guard: HandleGuard,
1297}
1298
1299#[unsafe(no_mangle)]
1300pub unsafe extern "C" fn net_mesh_open_stream(
1301 handle: *mut MeshNodeHandle,
1302 peer_node_id: u64,
1303 stream_id: u64,
1304 config_json: *const c_char,
1305 out_stream: *mut *mut MeshStreamHandle,
1306) -> c_int {
1307 if handle.is_null() || out_stream.is_null() {
1308 return NetError::NullPointer.into();
1309 }
1310 let h = unsafe { &*handle };
1311 let _op = match h.guard.try_enter() {
1312 Some(op) => op,
1313 None => return NetError::ShuttingDown.into(),
1314 };
1315 let cfg_json: StreamOpenConfig = if config_json.is_null() {
1316 StreamOpenConfig::default()
1317 } else {
1318 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1319 return NetError::InvalidUtf8.into();
1320 };
1321 match serde_json::from_str(&s) {
1322 Ok(v) => v,
1323 Err(_) => return NetError::InvalidJson.into(),
1324 }
1325 };
1326 let reliability = match cfg_json.reliability.as_deref() {
1327 None | Some("fire_and_forget") => Reliability::FireAndForget,
1328 Some("reliable") => Reliability::Reliable,
1329 Some(_) => return NET_ERR_MESH_TRANSPORT,
1330 };
1331 let window = cfg_json.window_bytes.unwrap_or(DEFAULT_STREAM_WINDOW_BYTES);
1332 let weight = cfg_json.fairness_weight.unwrap_or(1);
1333 let cfg = StreamConfig::new()
1334 .with_reliability(reliability)
1335 .with_window_bytes(window)
1336 .with_fairness_weight(weight);
1337 match h.inner.open_stream(peer_node_id, stream_id, cfg) {
1338 Ok(stream) => {
1339 let node_clone: Arc<MeshNode> = Arc::clone(&h.inner);
1340 let sh = Box::new(MeshStreamHandle {
1341 stream: ManuallyDrop::new(stream),
1342 _node: ManuallyDrop::new(node_clone),
1343 guard: HandleGuard::new(),
1344 });
1345 unsafe {
1346 *out_stream = Box::into_raw(sh);
1347 }
1348 0
1349 }
1350 Err(e) => adapter_err_to_code(&e),
1351 }
1352}
1353
1354#[unsafe(no_mangle)]
1355pub unsafe extern "C" fn net_mesh_stream_free(handle: *mut MeshStreamHandle) {
1356 if handle.is_null() {
1357 return;
1358 }
1359 let h: &MeshStreamHandle = unsafe { &*handle };
1361 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
1362 unsafe {
1364 let _stream = ManuallyDrop::take(&mut (*handle).stream);
1368 let node = ManuallyDrop::take(&mut (*handle)._node);
1369 drop(node);
1370 }
1371 } else {
1372 tracing::warn!(
1373 "net_mesh_stream_free: in-flight ops did not drain within deadline; \
1374 leaking inner to avoid use-after-free"
1375 );
1376 }
1377}
1378
1379unsafe fn collect_payloads(
1389 payloads: *const *const u8,
1390 lens: *const usize,
1391 count: usize,
1392) -> Option<Vec<Bytes>> {
1393 let mut out = Vec::with_capacity(count);
1394 for i in 0..count {
1395 let ptr = *payloads.add(i);
1396 let len = *lens.add(i);
1397 if ptr.is_null() {
1398 if len == 0 {
1399 out.push(Bytes::new());
1400 continue;
1401 }
1402 return None;
1403 }
1404 if len > isize::MAX as usize {
1408 return None;
1409 }
1410 let slice = std::slice::from_raw_parts(ptr, len);
1411 out.push(Bytes::copy_from_slice(slice));
1412 }
1413 Some(out)
1414}
1415
1416#[inline]
1424fn handles_match(sh: &MeshStreamHandle, nh: &MeshNodeHandle) -> bool {
1425 Arc::ptr_eq(&sh._node, &nh.inner)
1426}
1427
1428#[unsafe(no_mangle)]
1429pub unsafe extern "C" fn net_mesh_send(
1430 handle: *mut MeshStreamHandle,
1431 payloads: *const *const u8,
1432 lens: *const usize,
1433 count: usize,
1434 node_handle: *mut MeshNodeHandle,
1435) -> c_int {
1436 if handle.is_null() || node_handle.is_null() {
1437 return NetError::NullPointer.into();
1438 }
1439 if count > 0 && (payloads.is_null() || lens.is_null()) {
1440 return NetError::NullPointer.into();
1441 }
1442 let sh = unsafe { &*handle };
1443 let nh = unsafe { &*node_handle };
1444 let _sh_op = match sh.guard.try_enter() {
1447 Some(op) => op,
1448 None => return NetError::ShuttingDown.into(),
1449 };
1450 let _nh_op = match nh.guard.try_enter() {
1451 Some(op) => op,
1452 None => return NetError::ShuttingDown.into(),
1453 };
1454 if !handles_match(sh, nh) {
1455 return NetError::MismatchedHandles.into();
1456 }
1457 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1458 Some(v) => v,
1459 None => return NetError::NullPointer.into(),
1460 };
1461 let node = nh.inner.clone();
1462 let stream = sh.stream.clone();
1463 match block_on(async move { node.send_on_stream(&stream, &payloads).await }) {
1464 Ok(()) => 0,
1465 Err(e) => stream_err_to_code(&e),
1466 }
1467}
1468
1469#[unsafe(no_mangle)]
1470pub unsafe extern "C" fn net_mesh_send_with_retry(
1471 handle: *mut MeshStreamHandle,
1472 payloads: *const *const u8,
1473 lens: *const usize,
1474 count: usize,
1475 max_retries: u32,
1476 node_handle: *mut MeshNodeHandle,
1477) -> c_int {
1478 if handle.is_null() || node_handle.is_null() {
1479 return NetError::NullPointer.into();
1480 }
1481 if count > 0 && (payloads.is_null() || lens.is_null()) {
1482 return NetError::NullPointer.into();
1483 }
1484 let sh = unsafe { &*handle };
1485 let nh = unsafe { &*node_handle };
1486 let _sh_op = match sh.guard.try_enter() {
1489 Some(op) => op,
1490 None => return NetError::ShuttingDown.into(),
1491 };
1492 let _nh_op = match nh.guard.try_enter() {
1493 Some(op) => op,
1494 None => return NetError::ShuttingDown.into(),
1495 };
1496 if !handles_match(sh, nh) {
1497 return NetError::MismatchedHandles.into();
1498 }
1499 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1500 Some(v) => v,
1501 None => return NetError::NullPointer.into(),
1502 };
1503 let node = nh.inner.clone();
1504 let stream = sh.stream.clone();
1505 match block_on(async move {
1506 node.send_with_retry(&stream, &payloads, max_retries as usize)
1507 .await
1508 }) {
1509 Ok(()) => 0,
1510 Err(e) => stream_err_to_code(&e),
1511 }
1512}
1513
1514#[unsafe(no_mangle)]
1515pub unsafe extern "C" fn net_mesh_send_blocking(
1516 handle: *mut MeshStreamHandle,
1517 payloads: *const *const u8,
1518 lens: *const usize,
1519 count: usize,
1520 node_handle: *mut MeshNodeHandle,
1521) -> c_int {
1522 if handle.is_null() || node_handle.is_null() {
1523 return NetError::NullPointer.into();
1524 }
1525 if count > 0 && (payloads.is_null() || lens.is_null()) {
1526 return NetError::NullPointer.into();
1527 }
1528 let sh = unsafe { &*handle };
1529 let nh = unsafe { &*node_handle };
1530 let _sh_op = match sh.guard.try_enter() {
1533 Some(op) => op,
1534 None => return NetError::ShuttingDown.into(),
1535 };
1536 let _nh_op = match nh.guard.try_enter() {
1537 Some(op) => op,
1538 None => return NetError::ShuttingDown.into(),
1539 };
1540 if !handles_match(sh, nh) {
1541 return NetError::MismatchedHandles.into();
1542 }
1543 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1544 Some(v) => v,
1545 None => return NetError::NullPointer.into(),
1546 };
1547 let node = nh.inner.clone();
1548 let stream = sh.stream.clone();
1549 match block_on(async move { node.send_blocking(&stream, &payloads).await }) {
1550 Ok(()) => 0,
1551 Err(e) => stream_err_to_code(&e),
1552 }
1553}
1554
1555#[derive(Serialize)]
1556struct StreamStatsJson {
1557 tx_seq: u64,
1558 rx_seq: u64,
1559 inbound_pending: u64,
1560 last_activity_ns: u64,
1561 active: bool,
1562 backpressure_events: u64,
1563 tx_credit_remaining: u32,
1564 tx_window: u32,
1565 credit_grants_received: u64,
1566 credit_grants_sent: u64,
1567}
1568
1569#[unsafe(no_mangle)]
1570pub unsafe extern "C" fn net_mesh_stream_stats(
1571 node_handle: *mut MeshNodeHandle,
1572 peer_node_id: u64,
1573 stream_id: u64,
1574 out_json: *mut *mut c_char,
1575 out_len: *mut usize,
1576) -> c_int {
1577 if node_handle.is_null() || out_json.is_null() || out_len.is_null() {
1578 return NetError::NullPointer.into();
1579 }
1580 let h = unsafe { &*node_handle };
1581 let _op = match h.guard.try_enter() {
1582 Some(op) => op,
1583 None => return NetError::ShuttingDown.into(),
1584 };
1585 match h.inner.stream_stats(peer_node_id, stream_id) {
1586 Some(s) => {
1587 let js = StreamStatsJson {
1588 tx_seq: s.tx_seq,
1589 rx_seq: s.rx_seq,
1590 inbound_pending: s.inbound_pending,
1591 last_activity_ns: s.last_activity_ns,
1592 active: s.active,
1593 backpressure_events: s.backpressure_events,
1594 tx_credit_remaining: s.tx_credit_remaining,
1595 tx_window: s.tx_window,
1596 credit_grants_received: s.credit_grants_received,
1597 credit_grants_sent: s.credit_grants_sent,
1598 };
1599 write_json_out(&js, out_json, out_len)
1600 }
1601 None => {
1602 write_string_out("null".to_string(), out_json, out_len)
1605 }
1606 }
1607}
1608
1609#[derive(Serialize)]
1614struct RecvEventJson {
1615 id: String,
1616 payload_b64: String,
1618 insertion_ts: u64,
1619 shard_id: u16,
1620}
1621
1622#[unsafe(no_mangle)]
1623pub unsafe extern "C" fn net_mesh_recv_shard(
1624 handle: *mut MeshNodeHandle,
1625 shard_id: u16,
1626 limit: u32,
1627 out_json: *mut *mut c_char,
1628 out_len: *mut usize,
1629) -> c_int {
1630 if handle.is_null() || out_json.is_null() || out_len.is_null() {
1631 return NetError::NullPointer.into();
1632 }
1633 let h = unsafe { &*handle };
1634 let _op = match h.guard.try_enter() {
1635 Some(op) => op,
1636 None => return NetError::ShuttingDown.into(),
1637 };
1638 let node = h.inner.clone();
1639 let result = block_on(async move { node.poll_shard(shard_id, None, limit as usize).await });
1640 let result = match result {
1641 Ok(r) => r,
1642 Err(e) => return adapter_err_to_code(&e),
1643 };
1644 let events: Vec<RecvEventJson> = result
1645 .events
1646 .into_iter()
1647 .map(|e| RecvEventJson {
1648 id: e.id,
1649 payload_b64: encode_b64(&e.raw),
1650 insertion_ts: e.insertion_ts,
1651 shard_id: e.shard_id,
1652 })
1653 .collect();
1654 write_json_out(&events, out_json, out_len)
1655}
1656
1657fn encode_b64(bytes: &[u8]) -> String {
1658 const ALPH: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1661 let mut s = String::with_capacity(bytes.len().div_ceil(3) * 4);
1662 let mut i = 0;
1663 while i + 3 <= bytes.len() {
1664 let chunk = &bytes[i..i + 3];
1665 s.push(ALPH[(chunk[0] >> 2) as usize] as char);
1666 s.push(ALPH[(((chunk[0] & 0b11) << 4) | (chunk[1] >> 4)) as usize] as char);
1667 s.push(ALPH[(((chunk[1] & 0b1111) << 2) | (chunk[2] >> 6)) as usize] as char);
1668 s.push(ALPH[(chunk[2] & 0b111111) as usize] as char);
1669 i += 3;
1670 }
1671 let rem = bytes.len() - i;
1672 if rem == 1 {
1673 let b = bytes[i];
1674 s.push(ALPH[(b >> 2) as usize] as char);
1675 s.push(ALPH[((b & 0b11) << 4) as usize] as char);
1676 s.push('=');
1677 s.push('=');
1678 } else if rem == 2 {
1679 let b0 = bytes[i];
1680 let b1 = bytes[i + 1];
1681 s.push(ALPH[(b0 >> 2) as usize] as char);
1682 s.push(ALPH[(((b0 & 0b11) << 4) | (b1 >> 4)) as usize] as char);
1683 s.push(ALPH[((b1 & 0b1111) << 2) as usize] as char);
1684 s.push('=');
1685 }
1686 s
1687}
1688
1689#[derive(Deserialize)]
1694struct ChannelConfigInput {
1695 name: String,
1696 visibility: Option<String>,
1697 reliable: Option<bool>,
1698 require_token: Option<bool>,
1699 priority: Option<u8>,
1700 max_rate_pps: Option<u32>,
1701 publish_caps: Option<CapabilityFilterJson>,
1705 subscribe_caps: Option<CapabilityFilterJson>,
1709}
1710
1711fn parse_visibility(s: &str) -> Option<InnerVisibility> {
1712 match s {
1713 "subnet-local" => Some(InnerVisibility::SubnetLocal),
1714 "parent-visible" => Some(InnerVisibility::ParentVisible),
1715 "exported" => Some(InnerVisibility::Exported),
1716 "global" => Some(InnerVisibility::Global),
1717 _ => None,
1718 }
1719}
1720
1721#[unsafe(no_mangle)]
1722pub unsafe extern "C" fn net_mesh_register_channel(
1723 handle: *mut MeshNodeHandle,
1724 config_json: *const c_char,
1725) -> c_int {
1726 if handle.is_null() || config_json.is_null() {
1727 return NetError::NullPointer.into();
1728 }
1729 let h = unsafe { &*handle };
1730 let _op = match h.guard.try_enter() {
1731 Some(op) => op,
1732 None => return NetError::ShuttingDown.into(),
1733 };
1734 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1735 return NetError::InvalidUtf8.into();
1736 };
1737 let input: ChannelConfigInput = match serde_json::from_str(&s) {
1738 Ok(v) => v,
1739 Err(_) => return NetError::InvalidJson.into(),
1740 };
1741 let name = match InnerChannelName::new(&input.name) {
1742 Ok(n) => n,
1743 Err(_) => return NET_ERR_CHANNEL,
1744 };
1745 let mut cfg = InnerChannelConfig::new(ChannelId::new(name));
1746 if let Some(v) = input.visibility {
1747 let Some(vis) = parse_visibility(&v) else {
1748 return NET_ERR_CHANNEL;
1749 };
1750 cfg = cfg.with_visibility(vis);
1751 }
1752 if let Some(r) = input.reliable {
1753 cfg = cfg.with_reliable(r);
1754 }
1755 if let Some(t) = input.require_token {
1756 cfg = cfg.with_require_token(t);
1757 }
1758 if let Some(p) = input.priority {
1759 cfg = cfg.with_priority(p);
1760 }
1761 if let Some(pps) = input.max_rate_pps {
1762 cfg = cfg.with_rate_limit(pps);
1763 }
1764 if let Some(filter_json) = input.publish_caps {
1765 cfg = cfg.with_publish_caps(capability_filter_from_json(filter_json));
1766 }
1767 if let Some(filter_json) = input.subscribe_caps {
1768 cfg = cfg.with_subscribe_caps(capability_filter_from_json(filter_json));
1769 }
1770 h.channel_configs.insert(cfg);
1771 0
1772}
1773
1774#[unsafe(no_mangle)]
1775pub unsafe extern "C" fn net_mesh_subscribe_channel(
1776 handle: *mut MeshNodeHandle,
1777 publisher_node_id: u64,
1778 channel: *const c_char,
1779) -> c_int {
1780 subscribe_or_unsubscribe(handle, publisher_node_id, channel, true)
1781}
1782
1783#[unsafe(no_mangle)]
1784pub unsafe extern "C" fn net_mesh_unsubscribe_channel(
1785 handle: *mut MeshNodeHandle,
1786 publisher_node_id: u64,
1787 channel: *const c_char,
1788) -> c_int {
1789 subscribe_or_unsubscribe(handle, publisher_node_id, channel, false)
1790}
1791
1792#[unsafe(no_mangle)]
1799pub unsafe extern "C" fn net_mesh_subscribe_channel_with_token(
1800 handle: *mut MeshNodeHandle,
1801 publisher_node_id: u64,
1802 channel: *const c_char,
1803 token: *const u8,
1804 token_len: usize,
1805) -> c_int {
1806 if handle.is_null() || channel.is_null() || token.is_null() {
1807 return NetError::NullPointer.into();
1808 }
1809 let h = unsafe { &*handle };
1810 let _op = match h.guard.try_enter() {
1811 Some(op) => op,
1812 None => return NetError::ShuttingDown.into(),
1813 };
1814 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
1815 return NetError::InvalidUtf8.into();
1816 };
1817 let name = match InnerChannelName::new(&s) {
1818 Ok(n) => n,
1819 Err(_) => return NET_ERR_CHANNEL,
1820 };
1821 if token_len > isize::MAX as usize {
1823 return NetError::InvalidJson.into();
1824 }
1825 let slice = unsafe { std::slice::from_raw_parts(token, token_len) };
1826 let parsed = match PermissionToken::from_bytes(slice) {
1827 Ok(t) => t,
1828 Err(e) => return token_err_to_code(&e),
1829 };
1830 let node = h.inner.clone();
1831 match block_on(async move {
1832 node.subscribe_channel_with_token(publisher_node_id, name, parsed)
1833 .await
1834 }) {
1835 Ok(()) => 0,
1836 Err(e) => adapter_err_to_channel_code(&e),
1837 }
1838}
1839
1840fn subscribe_or_unsubscribe(
1841 handle: *mut MeshNodeHandle,
1842 publisher_node_id: u64,
1843 channel: *const c_char,
1844 subscribe: bool,
1845) -> c_int {
1846 if handle.is_null() || channel.is_null() {
1847 return NetError::NullPointer.into();
1848 }
1849 let h = unsafe { &*handle };
1850 let _op = match h.guard.try_enter() {
1851 Some(op) => op,
1852 None => return NetError::ShuttingDown.into(),
1853 };
1854 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
1855 return NetError::InvalidUtf8.into();
1856 };
1857 let name = match InnerChannelName::new(&s) {
1858 Ok(n) => n,
1859 Err(_) => return NET_ERR_CHANNEL,
1860 };
1861 let node = h.inner.clone();
1862 let outcome = if subscribe {
1863 block_on(async move { node.subscribe_channel(publisher_node_id, name).await })
1864 } else {
1865 block_on(async move { node.unsubscribe_channel(publisher_node_id, name).await })
1866 };
1867 match outcome {
1868 Ok(()) => 0,
1869 Err(e) => adapter_err_to_channel_code(&e),
1870 }
1871}
1872
1873fn adapter_err_to_channel_code(err: &AdapterError) -> c_int {
1874 if let AdapterError::Connection(msg) = err {
1875 let prefix = "membership request rejected: ";
1876 if let Some(tail) = msg.strip_prefix(prefix) {
1877 if tail.trim() == "Some(Unauthorized)" {
1878 return NET_ERR_CHANNEL_AUTH;
1879 }
1880 }
1881 }
1882 NET_ERR_CHANNEL
1883}
1884
1885#[derive(Deserialize, Default)]
1886struct PublishConfigInput {
1887 reliability: Option<String>,
1888 on_failure: Option<String>,
1889 max_inflight: Option<u32>,
1890}
1891
1892#[derive(Serialize)]
1893struct PublishReportJson {
1894 attempted: u32,
1895 delivered: u32,
1896 errors: Vec<PublishFailureJson>,
1897}
1898
1899#[derive(Serialize)]
1900struct PublishFailureJson {
1901 node_id: u64,
1902 message: String,
1903}
1904
1905fn to_publish_report_json(r: InnerPublishReport) -> PublishReportJson {
1906 PublishReportJson {
1907 attempted: r.attempted as u32,
1908 delivered: r.delivered as u32,
1909 errors: r
1910 .errors
1911 .into_iter()
1912 .map(|(id, e)| PublishFailureJson {
1913 node_id: id,
1914 message: format!("{}", e),
1915 })
1916 .collect(),
1917 }
1918}
1919
1920#[unsafe(no_mangle)]
1921pub unsafe extern "C" fn net_mesh_publish(
1922 handle: *mut MeshNodeHandle,
1923 channel: *const c_char,
1924 payload: *const u8,
1925 len: usize,
1926 config_json: *const c_char,
1927 out_json: *mut *mut c_char,
1928 out_len: *mut usize,
1929) -> c_int {
1930 if handle.is_null() || channel.is_null() || out_json.is_null() || out_len.is_null() {
1931 return NetError::NullPointer.into();
1932 }
1933 let h = unsafe { &*handle };
1934 let _op = match h.guard.try_enter() {
1935 Some(op) => op,
1936 None => return NetError::ShuttingDown.into(),
1937 };
1938 let Some(ch) = (unsafe { c_str_to_string(channel) }) else {
1939 return NetError::InvalidUtf8.into();
1940 };
1941 let name = match InnerChannelName::new(&ch) {
1942 Ok(n) => n,
1943 Err(_) => return NET_ERR_CHANNEL,
1944 };
1945 let cfg_in: PublishConfigInput = if config_json.is_null() {
1946 PublishConfigInput::default()
1947 } else {
1948 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1949 return NetError::InvalidUtf8.into();
1950 };
1951 match serde_json::from_str(&s) {
1952 Ok(v) => v,
1953 Err(_) => return NetError::InvalidJson.into(),
1954 }
1955 };
1956 let reliability = match cfg_in.reliability.as_deref() {
1957 None | Some("fire_and_forget") => Reliability::FireAndForget,
1958 Some("reliable") => Reliability::Reliable,
1959 Some(_) => return NET_ERR_CHANNEL,
1960 };
1961 let on_failure = match cfg_in.on_failure.as_deref() {
1962 None | Some("best_effort") => InnerOnFailure::BestEffort,
1963 Some("fail_fast") => InnerOnFailure::FailFast,
1964 Some("collect") => InnerOnFailure::Collect,
1965 Some(_) => return NET_ERR_CHANNEL,
1966 };
1967 let max_inflight = cfg_in.max_inflight.unwrap_or(32) as usize;
1968 let publish_cfg = InnerPublishConfig {
1969 reliability,
1970 on_failure,
1971 max_inflight,
1972 };
1973 let publisher = ChannelPublisher::new(name, publish_cfg);
1974
1975 let bytes = if len == 0 {
1977 Bytes::new()
1978 } else if payload.is_null() {
1979 return NetError::NullPointer.into();
1980 } else if len > isize::MAX as usize {
1981 return NetError::InvalidJson.into();
1983 } else {
1984 Bytes::copy_from_slice(unsafe { std::slice::from_raw_parts(payload, len) })
1985 };
1986
1987 let node = h.inner.clone();
1988 match block_on(async move { node.publish(&publisher, bytes).await }) {
1989 Ok(report) => {
1990 let js = to_publish_report_json(report);
1991 write_json_out(&js, out_json, out_len)
1992 }
1993 Err(e) => adapter_err_to_channel_code(&e),
1994 }
1995}
1996
1997pub struct IdentityHandle {
2011 keypair: ManuallyDrop<Arc<EntityKeypair>>,
2012 cache: ManuallyDrop<Arc<TokenCache>>,
2013 guard: HandleGuard,
2014}
2015
2016fn alloc_bytes(src: &[u8], out_ptr: *mut *mut u8, out_len: *mut usize) -> c_int {
2030 if out_ptr.is_null() || out_len.is_null() {
2031 return NetError::NullPointer.into();
2032 }
2033 let len = src.len();
2034 if len == 0 {
2035 unsafe {
2036 *out_ptr = std::ptr::null_mut();
2037 *out_len = 0;
2038 }
2039 return 0;
2040 }
2041 let layout = match std::alloc::Layout::array::<u8>(len) {
2050 Ok(l) => l,
2051 Err(_) => return NET_ERR_IDENTITY,
2057 };
2058 let ptr = unsafe { std::alloc::alloc(layout) };
2059 if ptr.is_null() {
2060 std::alloc::handle_alloc_error(layout);
2061 }
2062 unsafe {
2063 std::ptr::copy_nonoverlapping(src.as_ptr(), ptr, len);
2064 *out_ptr = ptr;
2065 *out_len = len;
2066 }
2067 0
2068}
2069
2070#[unsafe(no_mangle)]
2085pub unsafe extern "C" fn net_free_bytes(ptr: *mut u8, len: usize) {
2086 if ptr.is_null() || len == 0 {
2087 return;
2088 }
2089 let layout = match std::alloc::Layout::array::<u8>(len) {
2095 Ok(l) => l,
2096 Err(_) => return,
2097 };
2098 unsafe {
2099 std::alloc::dealloc(ptr, layout);
2100 }
2101}
2102
2103fn entity_id_from_bytes(bytes: *const u8, len: usize) -> Option<EntityId> {
2104 if bytes.is_null() || len != 32 {
2105 return None;
2106 }
2107 let slice = unsafe { std::slice::from_raw_parts(bytes, 32) };
2108 let mut arr = [0u8; 32];
2109 arr.copy_from_slice(slice);
2110 Some(EntityId::from_bytes(arr))
2111}
2112
2113fn parse_scope_list(raw: &str) -> Option<TokenScope> {
2114 let values: Vec<String> = serde_json::from_str(raw).ok()?;
2118 let mut acc = TokenScope::NONE;
2119 for s in &values {
2120 acc = acc.union(match s.as_str() {
2121 "publish" => TokenScope::PUBLISH,
2122 "subscribe" => TokenScope::SUBSCRIBE,
2123 "admin" => TokenScope::ADMIN,
2124 "delegate" => TokenScope::DELEGATE,
2125 _ => return None,
2126 });
2127 }
2128 Some(acc)
2129}
2130
2131fn scope_to_strings(scope: TokenScope) -> Vec<&'static str> {
2132 let mut out = Vec::new();
2133 if scope.contains(TokenScope::PUBLISH) {
2134 out.push("publish");
2135 }
2136 if scope.contains(TokenScope::SUBSCRIBE) {
2137 out.push("subscribe");
2138 }
2139 if scope.contains(TokenScope::ADMIN) {
2140 out.push("admin");
2141 }
2142 if scope.contains(TokenScope::DELEGATE) {
2143 out.push("delegate");
2144 }
2145 out
2146}
2147
2148fn channel_name_to_hash(channel: &str) -> Option<ChannelHash> {
2149 InnerChannelName::new(channel).ok().map(|n| n.hash())
2150}
2151
2152#[unsafe(no_mangle)]
2155pub unsafe extern "C" fn net_identity_generate(out_handle: *mut *mut IdentityHandle) -> c_int {
2156 if out_handle.is_null() {
2157 return NetError::NullPointer.into();
2158 }
2159 let handle = Box::new(IdentityHandle {
2160 keypair: ManuallyDrop::new(Arc::new(EntityKeypair::generate())),
2161 cache: ManuallyDrop::new(Arc::new(TokenCache::new())),
2162 guard: HandleGuard::new(),
2163 });
2164 unsafe {
2165 *out_handle = Box::into_raw(handle);
2166 }
2167 0
2168}
2169
2170#[unsafe(no_mangle)]
2174pub unsafe extern "C" fn net_identity_from_seed(
2175 seed: *const u8,
2176 seed_len: usize,
2177 out_handle: *mut *mut IdentityHandle,
2178) -> c_int {
2179 if seed.is_null() || out_handle.is_null() {
2180 return NetError::NullPointer.into();
2181 }
2182 if seed_len != 32 {
2183 return NET_ERR_IDENTITY;
2184 }
2185 let mut arr = [0u8; 32];
2186 arr.copy_from_slice(unsafe { std::slice::from_raw_parts(seed, 32) });
2187 let handle = Box::new(IdentityHandle {
2188 keypair: ManuallyDrop::new(Arc::new(EntityKeypair::from_bytes(arr))),
2189 cache: ManuallyDrop::new(Arc::new(TokenCache::new())),
2190 guard: HandleGuard::new(),
2191 });
2192 unsafe {
2193 *out_handle = Box::into_raw(handle);
2194 }
2195 0
2196}
2197
2198#[unsafe(no_mangle)]
2199pub unsafe extern "C" fn net_identity_free(handle: *mut IdentityHandle) {
2200 if handle.is_null() {
2201 return;
2202 }
2203 let h: &IdentityHandle = unsafe { &*handle };
2205 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
2206 unsafe {
2208 let mh = &mut *handle;
2209 let kp = ManuallyDrop::take(&mut mh.keypair);
2210 let cache = ManuallyDrop::take(&mut mh.cache);
2211 drop(kp);
2212 drop(cache);
2213 }
2214 } else {
2215 tracing::warn!(
2216 "net_identity_free: in-flight ops did not drain within deadline; \
2217 leaking inner to avoid use-after-free"
2218 );
2219 }
2220}
2221
2222#[unsafe(no_mangle)]
2225pub unsafe extern "C" fn net_identity_to_seed(handle: *mut IdentityHandle, out: *mut u8) -> c_int {
2226 if handle.is_null() || out.is_null() {
2227 return NetError::NullPointer.into();
2228 }
2229 let h = unsafe { &*handle };
2230 let _op = match h.guard.try_enter() {
2231 Some(op) => op,
2232 None => return NetError::ShuttingDown.into(),
2233 };
2234 let seed = h.keypair.secret_bytes();
2235 unsafe {
2236 std::ptr::copy_nonoverlapping(seed.as_ptr(), out, 32);
2237 }
2238 0
2239}
2240
2241#[unsafe(no_mangle)]
2243pub unsafe extern "C" fn net_identity_entity_id(
2244 handle: *mut IdentityHandle,
2245 out: *mut u8,
2246) -> c_int {
2247 if handle.is_null() || out.is_null() {
2248 return NetError::NullPointer.into();
2249 }
2250 let h = unsafe { &*handle };
2251 let _op = match h.guard.try_enter() {
2252 Some(op) => op,
2253 None => return NetError::ShuttingDown.into(),
2254 };
2255 let id = h.keypair.entity_id().as_bytes();
2256 unsafe {
2257 std::ptr::copy_nonoverlapping(id.as_ptr(), out, 32);
2258 }
2259 0
2260}
2261
2262#[unsafe(no_mangle)]
2263pub unsafe extern "C" fn net_identity_node_id(handle: *mut IdentityHandle) -> u64 {
2264 if handle.is_null() {
2265 return 0;
2266 }
2267 let h = unsafe { &*handle };
2268 let _op = match h.guard.try_enter() {
2270 Some(op) => op,
2271 None => return 0,
2272 };
2273 h.keypair.node_id()
2274}
2275
2276#[unsafe(no_mangle)]
2277pub unsafe extern "C" fn net_identity_origin_hash(handle: *mut IdentityHandle) -> u64 {
2278 if handle.is_null() {
2279 return 0;
2280 }
2281 let h = unsafe { &*handle };
2282 let _op = match h.guard.try_enter() {
2284 Some(op) => op,
2285 None => return 0,
2286 };
2287 h.keypair.origin_hash()
2288}
2289
2290#[unsafe(no_mangle)]
2293pub unsafe extern "C" fn net_identity_sign(
2294 handle: *mut IdentityHandle,
2295 msg: *const u8,
2296 len: usize,
2297 out_sig: *mut u8,
2298) -> c_int {
2299 if handle.is_null() || out_sig.is_null() {
2300 return NetError::NullPointer.into();
2301 }
2302 if len > 0 && msg.is_null() {
2303 return NetError::NullPointer.into();
2304 }
2305 let h = unsafe { &*handle };
2306 let _op = match h.guard.try_enter() {
2307 Some(op) => op,
2308 None => return NetError::ShuttingDown.into(),
2309 };
2310 let slice = if len == 0 {
2311 &[][..]
2312 } else if len > isize::MAX as usize {
2313 return NetError::InvalidJson.into();
2315 } else {
2316 unsafe { std::slice::from_raw_parts(msg, len) }
2317 };
2318 let sig = h.keypair.sign(slice).to_bytes();
2319 unsafe {
2320 std::ptr::copy_nonoverlapping(sig.as_ptr(), out_sig, 64);
2321 }
2322 0
2323}
2324
2325#[unsafe(no_mangle)]
2328pub unsafe extern "C" fn net_identity_issue_token(
2329 signer: *mut IdentityHandle,
2330 subject: *const u8,
2331 subject_len: usize,
2332 scope_json: *const c_char,
2333 channel: *const c_char,
2334 ttl_seconds: u32,
2335 delegation_depth: u8,
2336 out_token: *mut *mut u8,
2337 out_token_len: *mut usize,
2338) -> c_int {
2339 if signer.is_null() || out_token.is_null() || out_token_len.is_null() {
2340 return NetError::NullPointer.into();
2341 }
2342 let Some(subject_id) = entity_id_from_bytes(subject, subject_len) else {
2343 return NET_ERR_IDENTITY;
2344 };
2345 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
2346 return NetError::InvalidUtf8.into();
2347 };
2348 let Some(scope) = parse_scope_list(&scope_s) else {
2349 return NET_ERR_IDENTITY;
2350 };
2351 let Some(channel_s) = (unsafe { c_str_to_string(channel) }) else {
2352 return NetError::InvalidUtf8.into();
2353 };
2354 let Some(channel_hash) = channel_name_to_hash(&channel_s) else {
2355 return NET_ERR_IDENTITY;
2356 };
2357 let h = unsafe { &*signer };
2358 let _op = match h.guard.try_enter() {
2362 Some(op) => op,
2363 None => return NetError::ShuttingDown.into(),
2364 };
2365 let token = match PermissionToken::try_issue(
2371 &h.keypair,
2372 subject_id,
2373 scope,
2374 channel_hash,
2375 u64::from(ttl_seconds),
2376 delegation_depth,
2377 ) {
2378 Ok(t) => t,
2379 Err(e) => return token_err_to_code(&e),
2380 };
2381 alloc_bytes(&token.to_bytes(), out_token, out_token_len)
2382}
2383
2384#[unsafe(no_mangle)]
2388pub unsafe extern "C" fn net_identity_install_token(
2389 handle: *mut IdentityHandle,
2390 token: *const u8,
2391 len: usize,
2392) -> c_int {
2393 if handle.is_null() || token.is_null() {
2394 return NetError::NullPointer.into();
2395 }
2396 if len > isize::MAX as usize {
2398 return NetError::InvalidJson.into();
2399 }
2400 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2401 let parsed = match PermissionToken::from_bytes(slice) {
2402 Ok(t) => t,
2403 Err(e) => return token_err_to_code(&e),
2404 };
2405 let h = unsafe { &*handle };
2406 let _op = match h.guard.try_enter() {
2407 Some(op) => op,
2408 None => return NetError::ShuttingDown.into(),
2409 };
2410 match h.cache.insert(parsed) {
2411 Ok(()) => 0,
2412 Err(e) => token_err_to_code(&e),
2413 }
2414}
2415
2416#[unsafe(no_mangle)]
2420pub unsafe extern "C" fn net_identity_lookup_token(
2421 handle: *mut IdentityHandle,
2422 subject: *const u8,
2423 subject_len: usize,
2424 channel: *const c_char,
2425 out_token: *mut *mut u8,
2426 out_token_len: *mut usize,
2427) -> c_int {
2428 if handle.is_null() || out_token.is_null() || out_token_len.is_null() {
2429 return NetError::NullPointer.into();
2430 }
2431 let Some(subject_id) = entity_id_from_bytes(subject, subject_len) else {
2432 return NET_ERR_IDENTITY;
2433 };
2434 let Some(channel_s) = (unsafe { c_str_to_string(channel) }) else {
2435 return NetError::InvalidUtf8.into();
2436 };
2437 let Some(channel_hash) = channel_name_to_hash(&channel_s) else {
2438 return NET_ERR_IDENTITY;
2439 };
2440 let h = unsafe { &*handle };
2441 let _op = match h.guard.try_enter() {
2442 Some(op) => op,
2443 None => return NetError::ShuttingDown.into(),
2444 };
2445 match h.cache.get(&subject_id, channel_hash) {
2446 Some(token) => alloc_bytes(&token.to_bytes(), out_token, out_token_len),
2447 None => {
2448 unsafe {
2449 *out_token = std::ptr::null_mut();
2450 *out_token_len = 0;
2451 }
2452 0
2453 }
2454 }
2455}
2456
2457#[unsafe(no_mangle)]
2458pub unsafe extern "C" fn net_identity_token_cache_len(handle: *mut IdentityHandle) -> u32 {
2459 if handle.is_null() {
2460 return 0;
2461 }
2462 let h = unsafe { &*handle };
2463 let _op = match h.guard.try_enter() {
2465 Some(op) => op,
2466 None => return 0,
2467 };
2468 h.cache.len() as u32
2469}
2470
2471#[derive(Serialize)]
2476struct ParsedTokenJson {
2477 issuer_hex: String,
2478 subject_hex: String,
2479 scope: Vec<&'static str>,
2480 channel_hash: ChannelHash,
2481 not_before: u64,
2482 not_after: u64,
2483 delegation_depth: u8,
2484 nonce: u64,
2485 signature_hex: String,
2486}
2487
2488#[unsafe(no_mangle)]
2493pub unsafe extern "C" fn net_parse_token(
2494 token: *const u8,
2495 len: usize,
2496 out_json: *mut *mut c_char,
2497 out_len: *mut usize,
2498) -> c_int {
2499 if token.is_null() || out_json.is_null() || out_len.is_null() {
2500 return NetError::NullPointer.into();
2501 }
2502 if len > isize::MAX as usize {
2504 return NetError::InvalidJson.into();
2505 }
2506 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2507 let parsed = match PermissionToken::from_bytes(slice) {
2508 Ok(t) => t,
2509 Err(e) => return token_err_to_code(&e),
2510 };
2511 let out = ParsedTokenJson {
2512 issuer_hex: hex::encode(parsed.issuer.as_bytes()),
2513 subject_hex: hex::encode(parsed.subject.as_bytes()),
2514 scope: scope_to_strings(parsed.scope),
2515 channel_hash: parsed.channel_hash,
2516 not_before: parsed.not_before,
2517 not_after: parsed.not_after,
2518 delegation_depth: parsed.delegation_depth,
2519 nonce: parsed.nonce,
2520 signature_hex: hex::encode(parsed.signature),
2521 };
2522 write_json_out(&out, out_json, out_len)
2523}
2524
2525#[unsafe(no_mangle)]
2529pub unsafe extern "C" fn net_verify_token(
2530 token: *const u8,
2531 len: usize,
2532 out_ok: *mut c_int,
2533) -> c_int {
2534 if token.is_null() || out_ok.is_null() {
2535 return NetError::NullPointer.into();
2536 }
2537 if len > isize::MAX as usize {
2539 return NetError::InvalidJson.into();
2540 }
2541 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2542 let parsed = match PermissionToken::from_bytes(slice) {
2543 Ok(t) => t,
2544 Err(e) => return token_err_to_code(&e),
2545 };
2546 unsafe {
2547 *out_ok = if parsed.verify().is_ok() { 1 } else { 0 };
2548 }
2549 0
2550}
2551
2552#[unsafe(no_mangle)]
2557pub unsafe extern "C" fn net_token_is_expired(
2558 token: *const u8,
2559 len: usize,
2560 out_expired: *mut c_int,
2561) -> c_int {
2562 if token.is_null() || out_expired.is_null() {
2563 return NetError::NullPointer.into();
2564 }
2565 if len > isize::MAX as usize {
2567 return NetError::InvalidJson.into();
2568 }
2569 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2570 let parsed = match PermissionToken::from_bytes(slice) {
2571 Ok(t) => t,
2572 Err(e) => return token_err_to_code(&e),
2573 };
2574 unsafe {
2575 *out_expired = if parsed.is_expired() { 1 } else { 0 };
2576 }
2577 0
2578}
2579
2580#[unsafe(no_mangle)]
2583pub unsafe extern "C" fn net_delegate_token(
2584 signer: *mut IdentityHandle,
2585 parent: *const u8,
2586 parent_len: usize,
2587 new_subject: *const u8,
2588 new_subject_len: usize,
2589 restricted_scope_json: *const c_char,
2590 out_token: *mut *mut u8,
2591 out_token_len: *mut usize,
2592) -> c_int {
2593 if signer.is_null()
2594 || parent.is_null()
2595 || new_subject.is_null()
2596 || restricted_scope_json.is_null()
2597 || out_token.is_null()
2598 || out_token_len.is_null()
2599 {
2600 return NetError::NullPointer.into();
2601 }
2602 if parent_len > isize::MAX as usize {
2604 return NetError::InvalidJson.into();
2605 }
2606 let parent_slice = unsafe { std::slice::from_raw_parts(parent, parent_len) };
2607 let parent_tok = match PermissionToken::from_bytes(parent_slice) {
2608 Ok(t) => t,
2609 Err(e) => return token_err_to_code(&e),
2610 };
2611 let Some(subject_id) = entity_id_from_bytes(new_subject, new_subject_len) else {
2612 return NET_ERR_IDENTITY;
2613 };
2614 let Some(scope_s) = (unsafe { c_str_to_string(restricted_scope_json) }) else {
2615 return NetError::InvalidUtf8.into();
2616 };
2617 let Some(scope) = parse_scope_list(&scope_s) else {
2618 return NET_ERR_IDENTITY;
2619 };
2620 let h = unsafe { &*signer };
2621 let _op = match h.guard.try_enter() {
2625 Some(op) => op,
2626 None => return NetError::ShuttingDown.into(),
2627 };
2628 match parent_tok.delegate(&h.keypair, subject_id, scope) {
2629 Ok(child) => alloc_bytes(&child.to_bytes(), out_token, out_token_len),
2630 Err(e) => token_err_to_code(&e),
2631 }
2632}
2633
2634#[unsafe(no_mangle)]
2639pub unsafe extern "C" fn net_channel_hash(channel: *const c_char, out_hash: *mut u64) -> c_int {
2640 if channel.is_null() || out_hash.is_null() {
2641 return NetError::NullPointer.into();
2642 }
2643 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
2644 return NetError::InvalidUtf8.into();
2645 };
2646 let Some(hash) = channel_name_to_hash(&s) else {
2647 return NET_ERR_IDENTITY;
2648 };
2649 unsafe {
2650 *out_hash = hash;
2651 }
2652 0
2653}
2654
2655use crate::adapter::net::behavior::capability::{
2662 AcceleratorInfo, AcceleratorType, CapabilityFilter, CapabilitySet, GpuInfo, GpuVendor,
2663 HardwareCapabilities, Modality, ModelCapability, ResourceLimits, SoftwareCapabilities,
2664 ToolCapability, TAG_SCOPE_REGION_PREFIX, TAG_SCOPE_SUBNET_LOCAL, TAG_SCOPE_TENANT_PREFIX,
2665};
2666
2667fn parse_gpu_vendor_cap(s: &str) -> GpuVendor {
2670 match s.to_ascii_lowercase().as_str() {
2671 "nvidia" => GpuVendor::Nvidia,
2672 "amd" => GpuVendor::Amd,
2673 "intel" => GpuVendor::Intel,
2674 "apple" => GpuVendor::Apple,
2675 "qualcomm" => GpuVendor::Qualcomm,
2676 _ => GpuVendor::Unknown,
2677 }
2678}
2679
2680fn gpu_vendor_to_string_cap(v: GpuVendor) -> &'static str {
2681 match v {
2682 GpuVendor::Nvidia => "nvidia",
2683 GpuVendor::Amd => "amd",
2684 GpuVendor::Intel => "intel",
2685 GpuVendor::Apple => "apple",
2686 GpuVendor::Qualcomm => "qualcomm",
2687 GpuVendor::Unknown => "unknown",
2688 }
2689}
2690
2691fn parse_modality_cap(s: &str) -> Option<Modality> {
2692 match s.to_ascii_lowercase().as_str() {
2693 "text" => Some(Modality::Text),
2694 "image" => Some(Modality::Image),
2695 "audio" => Some(Modality::Audio),
2696 "video" => Some(Modality::Video),
2697 "code" => Some(Modality::Code),
2698 "embedding" => Some(Modality::Embedding),
2699 "tool-use" | "tool_use" | "tooluse" => Some(Modality::ToolUse),
2700 _ => None,
2709 }
2710}
2711
2712fn parse_accelerator_type_cap(s: &str) -> AcceleratorType {
2713 match s.to_ascii_lowercase().as_str() {
2714 "tpu" => AcceleratorType::Tpu,
2715 "npu" => AcceleratorType::Npu,
2716 "fpga" => AcceleratorType::Fpga,
2717 "asic" => AcceleratorType::Asic,
2718 "dsp" => AcceleratorType::Dsp,
2719 _ => AcceleratorType::Unknown,
2720 }
2721}
2722
2723#[derive(Deserialize, Default)]
2726struct CapabilitySetJson {
2727 #[serde(default)]
2728 hardware: Option<HardwareJson>,
2729 #[serde(default)]
2730 software: Option<SoftwareJson>,
2731 #[serde(default)]
2732 models: Vec<ModelJson>,
2733 #[serde(default)]
2734 tools: Vec<ToolJson>,
2735 #[serde(default)]
2736 tags: Vec<String>,
2737 #[serde(default)]
2738 limits: Option<LimitsJson>,
2739}
2740
2741#[derive(Deserialize, Default)]
2742struct HardwareJson {
2743 cpu_cores: Option<u32>,
2744 cpu_threads: Option<u32>,
2745 memory_gb: Option<u32>,
2746 gpu: Option<GpuJson>,
2747 #[serde(default)]
2748 additional_gpus: Vec<GpuJson>,
2749 storage_gb: Option<u64>,
2750 network_gbps: Option<u32>,
2751 #[serde(default)]
2752 accelerators: Vec<AcceleratorJson>,
2753}
2754
2755#[derive(Deserialize)]
2756struct GpuJson {
2757 vendor: Option<String>,
2758 #[serde(default)]
2759 model: String,
2760 #[serde(default)]
2761 vram_gb: u32,
2762 compute_units: Option<u32>,
2763 tensor_cores: Option<u32>,
2764 fp16_tflops_x10: Option<u32>,
2765}
2766
2767#[derive(Deserialize)]
2768struct AcceleratorJson {
2769 #[serde(default)]
2770 kind: String,
2771 #[serde(default)]
2772 model: String,
2773 memory_gb: Option<u32>,
2774 tops_x10: Option<u32>,
2775}
2776
2777#[derive(Deserialize, Default)]
2778struct SoftwareJson {
2779 os: Option<String>,
2780 os_version: Option<String>,
2781 #[serde(default)]
2782 runtimes: Vec<Vec<String>>,
2783 #[serde(default)]
2784 frameworks: Vec<Vec<String>>,
2785 cuda_version: Option<String>,
2786 #[serde(default)]
2787 drivers: Vec<Vec<String>>,
2788}
2789
2790#[derive(Deserialize)]
2791struct ModelJson {
2792 #[serde(default)]
2793 model_id: String,
2794 #[serde(default)]
2795 family: String,
2796 parameters_b_x10: Option<u32>,
2797 context_length: Option<u32>,
2798 quantization: Option<String>,
2799 #[serde(default)]
2800 modalities: Vec<String>,
2801 tokens_per_sec: Option<u32>,
2802 loaded: Option<bool>,
2803}
2804
2805#[derive(Deserialize)]
2806struct ToolJson {
2807 #[serde(default)]
2808 tool_id: String,
2809 #[serde(default)]
2810 name: String,
2811 version: Option<String>,
2812 input_schema: Option<String>,
2813 output_schema: Option<String>,
2814 #[serde(default)]
2815 requires: Vec<String>,
2816 estimated_time_ms: Option<u32>,
2817 stateless: Option<bool>,
2818}
2819
2820#[derive(Deserialize, Default)]
2821struct LimitsJson {
2822 max_concurrent_requests: Option<u32>,
2823 max_tokens_per_request: Option<u32>,
2824 rate_limit_rpm: Option<u32>,
2825 max_batch_size: Option<u32>,
2826 max_input_bytes: Option<u32>,
2827 max_output_bytes: Option<u32>,
2828}
2829
2830#[derive(Deserialize, Default)]
2831struct CapabilityFilterJson {
2832 #[serde(default)]
2833 require_tags: Vec<String>,
2834 #[serde(default)]
2835 require_models: Vec<String>,
2836 #[serde(default)]
2837 require_tools: Vec<String>,
2838 min_memory_gb: Option<u32>,
2839 require_gpu: Option<bool>,
2840 gpu_vendor: Option<String>,
2841 min_vram_gb: Option<u32>,
2842 min_context_length: Option<u32>,
2843 #[serde(default)]
2844 require_modalities: Vec<String>,
2845}
2846
2847fn pair_vec(xs: Vec<Vec<String>>) -> Vec<(String, String)> {
2850 xs.into_iter()
2851 .filter_map(|mut p| {
2852 if p.len() >= 2 {
2853 Some((std::mem::take(&mut p[0]), std::mem::take(&mut p[1])))
2854 } else {
2855 None
2856 }
2857 })
2858 .collect()
2859}
2860
2861#[inline]
2867fn saturating_u16_cap(v: u32) -> u16 {
2868 v.min(u16::MAX as u32) as u16
2869}
2870
2871fn gpu_info_from_json(g: GpuJson) -> GpuInfo {
2872 let vendor = g
2873 .vendor
2874 .as_deref()
2875 .map(parse_gpu_vendor_cap)
2876 .unwrap_or(GpuVendor::Unknown);
2877 let mut info = GpuInfo::new(vendor, g.model, g.vram_gb);
2878 if let Some(cu) = g.compute_units {
2879 info = info.with_compute_units(saturating_u16_cap(cu));
2880 }
2881 if let Some(tc) = g.tensor_cores {
2882 info = info.with_tensor_cores(saturating_u16_cap(tc));
2883 }
2884 if let Some(tf) = g.fp16_tflops_x10 {
2885 let tf_capped = saturating_u16_cap(tf);
2899 info = info.with_fp16_tflops(tf_capped as f32 / 10.0);
2900 }
2901 info
2902}
2903
2904fn accelerator_from_json(a: AcceleratorJson) -> AcceleratorInfo {
2905 AcceleratorInfo {
2906 accel_type: parse_accelerator_type_cap(&a.kind),
2907 model: a.model,
2908 memory_gb: a.memory_gb.unwrap_or(0),
2909 tops_x10: a.tops_x10.map(saturating_u16_cap).unwrap_or(0),
2910 }
2911}
2912
2913fn hardware_from_json(h: HardwareJson) -> HardwareCapabilities {
2914 let mut hw = HardwareCapabilities::new();
2915 match (h.cpu_cores, h.cpu_threads) {
2916 (Some(c), Some(t)) => hw = hw.with_cpu(saturating_u16_cap(c), saturating_u16_cap(t)),
2917 (Some(c), None) => {
2918 let c16 = saturating_u16_cap(c);
2919 hw = hw.with_cpu(c16, c16);
2920 }
2921 _ => {}
2922 }
2923 if let Some(mb) = h.memory_gb {
2924 hw = hw.with_memory(mb);
2925 }
2926 if let Some(g) = h.gpu {
2927 hw = hw.with_gpu(gpu_info_from_json(g));
2928 }
2929 for g in h.additional_gpus {
2930 hw = hw.add_gpu(gpu_info_from_json(g));
2931 }
2932 if let Some(mb) = h.storage_gb {
2933 hw = hw.with_storage(mb);
2934 }
2935 if let Some(gbps) = h.network_gbps {
2936 hw = hw.with_network(gbps);
2937 }
2938 for a in h.accelerators {
2939 hw = hw.add_accelerator(accelerator_from_json(a));
2940 }
2941 hw
2942}
2943
2944fn software_from_json(s: SoftwareJson) -> SoftwareCapabilities {
2945 let mut sw = SoftwareCapabilities::new()
2946 .with_os(s.os.unwrap_or_default(), s.os_version.unwrap_or_default());
2947 for (k, v) in pair_vec(s.runtimes) {
2948 sw = sw.add_runtime(k, v);
2949 }
2950 for (k, v) in pair_vec(s.frameworks) {
2951 sw = sw.add_framework(k, v);
2952 }
2953 if let Some(c) = s.cuda_version {
2954 sw = sw.with_cuda(c);
2955 }
2956 sw.drivers = pair_vec(s.drivers);
2957 sw
2958}
2959
2960fn model_from_json(m: ModelJson) -> ModelCapability {
2961 let mut mc = ModelCapability::new(m.model_id, m.family);
2962 if let Some(p) = m.parameters_b_x10 {
2963 mc.parameters_b_x10 = p;
2964 }
2965 if let Some(c) = m.context_length {
2966 mc = mc.with_context_length(c);
2967 }
2968 if let Some(q) = m.quantization {
2969 mc = mc.with_quantization(q);
2970 }
2971 for modality in m.modalities {
2972 match parse_modality_cap(&modality) {
2973 Some(parsed) => mc = mc.add_modality(parsed),
2974 None => {
2975 tracing::warn!(
2976 modality = %modality,
2977 "announce_capabilities: unknown modality string (typo?), \
2978 skipping rather than the pre-fix silent fallback to Text — \
2979 advertising a Text capability the node doesn't actually \
2980 have produced wrong scheduling decisions on the receiver",
2981 );
2982 }
2983 }
2984 }
2985 if let Some(t) = m.tokens_per_sec {
2986 mc = mc.with_tokens_per_sec(t);
2987 }
2988 if let Some(l) = m.loaded {
2989 mc = mc.with_loaded(l);
2990 }
2991 mc
2992}
2993
2994fn tool_from_json(t: ToolJson) -> ToolCapability {
2995 let mut tc = ToolCapability::new(t.tool_id, t.name);
2996 if let Some(v) = t.version {
2997 tc = tc.with_version(v);
2998 }
2999 if let Some(s) = t.input_schema {
3000 tc = tc.with_input_schema(s);
3001 }
3002 if let Some(s) = t.output_schema {
3003 tc = tc.with_output_schema(s);
3004 }
3005 for r in t.requires {
3006 tc = tc.requires(r);
3007 }
3008 if let Some(ms) = t.estimated_time_ms {
3009 tc = tc.with_estimated_time(ms);
3010 }
3011 if let Some(st) = t.stateless {
3012 tc = tc.with_stateless(st);
3013 }
3014 tc
3015}
3016
3017fn limits_from_json(l: LimitsJson) -> ResourceLimits {
3018 let mut rl = ResourceLimits::new();
3019 if let Some(n) = l.max_concurrent_requests {
3020 rl = rl.with_max_concurrent(n);
3021 }
3022 if let Some(n) = l.max_tokens_per_request {
3023 rl = rl.with_max_tokens(n);
3024 }
3025 if let Some(n) = l.rate_limit_rpm {
3026 rl = rl.with_rate_limit(n);
3027 }
3028 if let Some(n) = l.max_batch_size {
3029 rl = rl.with_max_batch(n);
3030 }
3031 if let Some(n) = l.max_input_bytes {
3032 rl.max_input_bytes = n;
3033 }
3034 if let Some(n) = l.max_output_bytes {
3035 rl.max_output_bytes = n;
3036 }
3037 rl
3038}
3039
3040fn capability_set_from_json(caps: CapabilitySetJson) -> CapabilitySet {
3041 let mut cs = CapabilitySet::new();
3042 if let Some(h) = caps.hardware {
3043 cs = cs.with_hardware(hardware_from_json(h));
3044 }
3045 if let Some(s) = caps.software {
3046 cs = cs.with_software(software_from_json(s));
3047 }
3048 for m in caps.models {
3049 cs = cs.add_model(model_from_json(m));
3050 }
3051 for t in caps.tools {
3052 cs = cs.add_tool(tool_from_json(t));
3053 }
3054 for tag in caps.tags {
3062 if tag == TAG_SCOPE_SUBNET_LOCAL {
3063 cs = cs.with_subnet_local_scope();
3064 } else if let Some(id) = tag.strip_prefix(TAG_SCOPE_TENANT_PREFIX) {
3065 cs = cs.with_tenant_scope(id);
3066 } else if let Some(name) = tag.strip_prefix(TAG_SCOPE_REGION_PREFIX) {
3067 cs = cs.with_region_scope(name);
3068 } else {
3069 cs = cs.add_tag(tag);
3070 }
3071 }
3072 if let Some(l) = caps.limits {
3073 cs = cs.with_limits(limits_from_json(l));
3074 }
3075 cs
3076}
3077
3078fn capability_filter_from_json(f: CapabilityFilterJson) -> CapabilityFilter {
3079 let mut cf = CapabilityFilter::new();
3080 for t in f.require_tags {
3081 cf = cf.require_tag(t);
3082 }
3083 for m in f.require_models {
3084 cf = cf.require_model(m);
3085 }
3086 for t in f.require_tools {
3087 cf = cf.require_tool(t);
3088 }
3089 if let Some(mb) = f.min_memory_gb {
3090 cf = cf.with_min_memory(mb);
3091 }
3092 if f.require_gpu.unwrap_or(false) {
3093 cf = cf.require_gpu();
3094 }
3095 if let Some(v) = f.gpu_vendor {
3096 cf = cf.with_gpu_vendor(parse_gpu_vendor_cap(&v));
3097 }
3098 if let Some(mb) = f.min_vram_gb {
3099 cf = cf.with_min_vram(mb);
3100 }
3101 if let Some(n) = f.min_context_length {
3102 cf = cf.with_min_context(n);
3103 }
3104 for m in f.require_modalities {
3105 match parse_modality_cap(&m) {
3106 Some(parsed) => cf = cf.require_modality(parsed),
3107 None => {
3108 tracing::warn!(
3121 modality = %m,
3122 "find_nodes: unknown modality string in require_modalities \
3123 filter (typo?), dropping the constraint; the resulting \
3124 filter is too permissive — pre-fix it was silently \
3125 re-interpreted as `require Text`, which returned the \
3126 wrong nodes",
3127 );
3128 }
3129 }
3130 }
3131 cf
3132}
3133
3134pub(crate) const NET_ERR_CAPABILITY: c_int = -128;
3137
3138#[unsafe(no_mangle)]
3145pub unsafe extern "C" fn net_mesh_announce_capabilities(
3146 handle: *mut MeshNodeHandle,
3147 caps_json: *const c_char,
3148) -> c_int {
3149 if handle.is_null() || caps_json.is_null() {
3150 return NetError::NullPointer.into();
3151 }
3152 let h = unsafe { &*handle };
3153 let _op = match h.guard.try_enter() {
3154 Some(op) => op,
3155 None => return NetError::ShuttingDown.into(),
3156 };
3157 let Some(s) = (unsafe { c_str_to_string(caps_json) }) else {
3158 return NetError::InvalidUtf8.into();
3159 };
3160 let parsed: CapabilitySetJson = match serde_json::from_str(&s) {
3161 Ok(v) => v,
3162 Err(_) => return NetError::InvalidJson.into(),
3163 };
3164 let caps = capability_set_from_json(parsed);
3165 let node = h.inner.clone();
3166 match block_on(async move { node.announce_capabilities(caps).await }) {
3167 Ok(()) => 0,
3168 Err(_) => NET_ERR_CAPABILITY,
3169 }
3170}
3171
3172#[unsafe(no_mangle)]
3175pub unsafe extern "C" fn net_mesh_find_nodes(
3176 handle: *mut MeshNodeHandle,
3177 filter_json: *const c_char,
3178 out_json: *mut *mut c_char,
3179 out_len: *mut usize,
3180) -> c_int {
3181 if handle.is_null() || filter_json.is_null() || out_json.is_null() || out_len.is_null() {
3182 return NetError::NullPointer.into();
3183 }
3184 let h = unsafe { &*handle };
3185 let _op = match h.guard.try_enter() {
3186 Some(op) => op,
3187 None => return NetError::ShuttingDown.into(),
3188 };
3189 let Some(s) = (unsafe { c_str_to_string(filter_json) }) else {
3190 return NetError::InvalidUtf8.into();
3191 };
3192 let parsed: CapabilityFilterJson = match serde_json::from_str(&s) {
3193 Ok(v) => v,
3194 Err(_) => return NetError::InvalidJson.into(),
3195 };
3196 let filter = capability_filter_from_json(parsed);
3197 let ids = h.inner.find_nodes_by_filter(&filter);
3198 write_json_out(&ids, out_json, out_len)
3199}
3200
3201#[derive(serde::Deserialize)]
3218struct ScopeFilterJson {
3219 kind: String,
3220 #[serde(default)]
3221 tenant: Option<String>,
3222 #[serde(default)]
3223 tenants: Option<Vec<String>>,
3224 #[serde(default)]
3225 region: Option<String>,
3226 #[serde(default)]
3227 regions: Option<Vec<String>>,
3228}
3229
3230enum ScopeFilterOwned {
3236 Any,
3237 GlobalOnly,
3238 SameSubnet,
3239 Tenant(String),
3240 Tenants(Vec<String>),
3241 Region(String),
3242 Regions(Vec<String>),
3243}
3244
3245fn scope_filter_from_json(f: ScopeFilterJson) -> ScopeFilterOwned {
3246 match f.kind.as_str() {
3247 "any" => ScopeFilterOwned::Any,
3248 "global_only" | "globalOnly" => ScopeFilterOwned::GlobalOnly,
3249 "same_subnet" | "sameSubnet" => ScopeFilterOwned::SameSubnet,
3250 "tenant" => match f.tenant {
3251 Some(t) if !t.is_empty() => ScopeFilterOwned::Tenant(t),
3252 _ => ScopeFilterOwned::Any,
3253 },
3254 "tenants" => match f.tenants {
3255 Some(ts) => {
3261 let cleaned: Vec<String> = ts.into_iter().filter(|t| !t.is_empty()).collect();
3262 if cleaned.is_empty() {
3263 ScopeFilterOwned::Any
3264 } else {
3265 ScopeFilterOwned::Tenants(cleaned)
3266 }
3267 }
3268 None => ScopeFilterOwned::Any,
3269 },
3270 "region" => match f.region {
3271 Some(r) if !r.is_empty() => ScopeFilterOwned::Region(r),
3272 _ => ScopeFilterOwned::Any,
3273 },
3274 "regions" => match f.regions {
3275 Some(rs) => {
3277 let cleaned: Vec<String> = rs.into_iter().filter(|r| !r.is_empty()).collect();
3278 if cleaned.is_empty() {
3279 ScopeFilterOwned::Any
3280 } else {
3281 ScopeFilterOwned::Regions(cleaned)
3282 }
3283 }
3284 None => ScopeFilterOwned::Any,
3285 },
3286 _ => ScopeFilterOwned::Any,
3287 }
3288}
3289
3290fn with_scope_filter<R>(
3295 owned: &ScopeFilterOwned,
3296 f: impl FnOnce(&crate::adapter::net::behavior::capability::ScopeFilter<'_>) -> R,
3297) -> R {
3298 use crate::adapter::net::behavior::capability::ScopeFilter as F;
3299 match owned {
3300 ScopeFilterOwned::Any => f(&F::Any),
3301 ScopeFilterOwned::GlobalOnly => f(&F::GlobalOnly),
3302 ScopeFilterOwned::SameSubnet => f(&F::SameSubnet),
3303 ScopeFilterOwned::Tenant(t) => f(&F::Tenant(t.as_str())),
3304 ScopeFilterOwned::Tenants(ts) => {
3305 let refs: Vec<&str> = ts.iter().map(|s| s.as_str()).collect();
3306 f(&F::Tenants(refs.as_slice()))
3307 }
3308 ScopeFilterOwned::Region(r) => f(&F::Region(r.as_str())),
3309 ScopeFilterOwned::Regions(rs) => {
3310 let refs: Vec<&str> = rs.iter().map(|s| s.as_str()).collect();
3311 f(&F::Regions(refs.as_slice()))
3312 }
3313 }
3314}
3315
3316#[unsafe(no_mangle)]
3339pub unsafe extern "C" fn net_mesh_find_nodes_scoped(
3340 handle: *mut MeshNodeHandle,
3341 filter_json: *const c_char,
3342 scope_json: *const c_char,
3343 out_json: *mut *mut c_char,
3344 out_len: *mut usize,
3345) -> c_int {
3346 if handle.is_null()
3347 || filter_json.is_null()
3348 || scope_json.is_null()
3349 || out_json.is_null()
3350 || out_len.is_null()
3351 {
3352 return NetError::NullPointer.into();
3353 }
3354 let h = unsafe { &*handle };
3355 let _op = match h.guard.try_enter() {
3356 Some(op) => op,
3357 None => return NetError::ShuttingDown.into(),
3358 };
3359 let Some(filter_s) = (unsafe { c_str_to_string(filter_json) }) else {
3360 return NetError::InvalidUtf8.into();
3361 };
3362 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
3363 return NetError::InvalidUtf8.into();
3364 };
3365 let parsed_filter: CapabilityFilterJson = match serde_json::from_str(&filter_s) {
3366 Ok(v) => v,
3367 Err(_) => return NetError::InvalidJson.into(),
3368 };
3369 let parsed_scope: ScopeFilterJson = match serde_json::from_str(&scope_s) {
3370 Ok(v) => v,
3371 Err(_) => return NetError::InvalidJson.into(),
3372 };
3373 let filter = capability_filter_from_json(parsed_filter);
3374 let owned = scope_filter_from_json(parsed_scope);
3375 let ids = with_scope_filter(&owned, |sf| {
3376 h.inner.find_nodes_by_filter_scoped(&filter, sf)
3377 });
3378 write_json_out(&ids, out_json, out_len)
3379}
3380
3381#[derive(serde::Deserialize)]
3395struct CapabilityRequirementJson {
3396 #[serde(default)]
3397 filter: CapabilityFilterJson,
3398 #[serde(default)]
3399 prefer_more_memory: f32,
3400 #[serde(default)]
3401 prefer_more_vram: f32,
3402 #[serde(default)]
3403 prefer_faster_inference: f32,
3404 #[serde(default)]
3405 prefer_loaded_models: f32,
3406}
3407
3408fn capability_requirement_from_json(
3409 j: CapabilityRequirementJson,
3410) -> crate::adapter::net::behavior::capability::CapabilityRequirement {
3411 crate::adapter::net::behavior::capability::CapabilityRequirement::from_filter(
3412 capability_filter_from_json(j.filter),
3413 )
3414 .prefer_memory(j.prefer_more_memory)
3415 .prefer_vram(j.prefer_more_vram)
3416 .prefer_speed(j.prefer_faster_inference)
3417 .prefer_loaded(j.prefer_loaded_models)
3418}
3419
3420#[unsafe(no_mangle)]
3430pub unsafe extern "C" fn net_mesh_find_best_node(
3431 handle: *mut MeshNodeHandle,
3432 requirement_json: *const c_char,
3433 out_node_id: *mut u64,
3434 out_has_match: *mut c_int,
3435) -> c_int {
3436 if handle.is_null()
3437 || requirement_json.is_null()
3438 || out_node_id.is_null()
3439 || out_has_match.is_null()
3440 {
3441 return NetError::NullPointer.into();
3442 }
3443 let h = unsafe { &*handle };
3444 let _op = match h.guard.try_enter() {
3445 Some(op) => op,
3446 None => return NetError::ShuttingDown.into(),
3447 };
3448 let Some(s) = (unsafe { c_str_to_string(requirement_json) }) else {
3449 return NetError::InvalidUtf8.into();
3450 };
3451 let parsed: CapabilityRequirementJson = match serde_json::from_str(&s) {
3452 Ok(v) => v,
3453 Err(_) => return NetError::InvalidJson.into(),
3454 };
3455 let req = capability_requirement_from_json(parsed);
3456 match h.inner.find_best_node(&req) {
3457 Some(node_id) => unsafe {
3458 *out_node_id = node_id;
3459 *out_has_match = 1;
3460 },
3461 None => unsafe {
3462 *out_has_match = 0;
3463 },
3464 }
3465 0
3466}
3467
3468#[unsafe(no_mangle)]
3477pub unsafe extern "C" fn net_mesh_find_best_node_scoped(
3478 handle: *mut MeshNodeHandle,
3479 requirement_json: *const c_char,
3480 scope_json: *const c_char,
3481 out_node_id: *mut u64,
3482 out_has_match: *mut c_int,
3483) -> c_int {
3484 if handle.is_null()
3485 || requirement_json.is_null()
3486 || scope_json.is_null()
3487 || out_node_id.is_null()
3488 || out_has_match.is_null()
3489 {
3490 return NetError::NullPointer.into();
3491 }
3492 let h = unsafe { &*handle };
3493 let _op = match h.guard.try_enter() {
3494 Some(op) => op,
3495 None => return NetError::ShuttingDown.into(),
3496 };
3497 let Some(req_s) = (unsafe { c_str_to_string(requirement_json) }) else {
3498 return NetError::InvalidUtf8.into();
3499 };
3500 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
3501 return NetError::InvalidUtf8.into();
3502 };
3503 let parsed_req: CapabilityRequirementJson = match serde_json::from_str(&req_s) {
3504 Ok(v) => v,
3505 Err(_) => return NetError::InvalidJson.into(),
3506 };
3507 let parsed_scope: ScopeFilterJson = match serde_json::from_str(&scope_s) {
3508 Ok(v) => v,
3509 Err(_) => return NetError::InvalidJson.into(),
3510 };
3511 let req = capability_requirement_from_json(parsed_req);
3512 let owned = scope_filter_from_json(parsed_scope);
3513 let result = with_scope_filter(&owned, |sf| h.inner.find_best_node_scoped(&req, sf));
3514 match result {
3515 Some(node_id) => unsafe {
3516 *out_node_id = node_id;
3517 *out_has_match = 1;
3518 },
3519 None => unsafe {
3520 *out_has_match = 0;
3521 },
3522 }
3523 0
3524}
3525
3526#[unsafe(no_mangle)]
3528pub unsafe extern "C" fn net_normalize_gpu_vendor(
3529 raw: *const c_char,
3530 out_json: *mut *mut c_char,
3531 out_len: *mut usize,
3532) -> c_int {
3533 if raw.is_null() || out_json.is_null() || out_len.is_null() {
3534 return NetError::NullPointer.into();
3535 }
3536 let Some(s) = (unsafe { c_str_to_string(raw) }) else {
3537 return NetError::InvalidUtf8.into();
3538 };
3539 let canonical = gpu_vendor_to_string_cap(parse_gpu_vendor_cap(&s));
3540 write_string_out(canonical.to_string(), out_json, out_len)
3541}
3542
3543#[cfg(test)]
3544mod tests {
3545 use super::*;
3546
3547 #[test]
3559 fn saturating_u16_cap_clamps_at_u16_max() {
3560 assert_eq!(saturating_u16_cap(0), 0);
3561 assert_eq!(saturating_u16_cap(42), 42);
3562 assert_eq!(saturating_u16_cap(u16::MAX as u32), u16::MAX);
3563 assert_eq!(saturating_u16_cap(u16::MAX as u32 + 1), u16::MAX);
3564 assert_eq!(saturating_u16_cap(u32::MAX), u16::MAX);
3565 }
3566
3567 #[test]
3576 fn parse_modality_cap_returns_none_on_unknown_strings() {
3577 for (s, expected) in [
3579 ("text", Modality::Text),
3580 ("Text", Modality::Text),
3581 ("TEXT", Modality::Text),
3582 ("image", Modality::Image),
3583 ("audio", Modality::Audio),
3584 ("video", Modality::Video),
3585 ("code", Modality::Code),
3586 ("embedding", Modality::Embedding),
3587 ("tool-use", Modality::ToolUse),
3588 ("tool_use", Modality::ToolUse),
3589 ("tooluse", Modality::ToolUse),
3590 ] {
3591 assert_eq!(
3592 parse_modality_cap(s),
3593 Some(expected),
3594 "known modality `{s}` must parse",
3595 );
3596 }
3597
3598 for s in ["audoi", "imageX", "vidoe", "embeding", "garbage", ""] {
3600 assert_eq!(
3601 parse_modality_cap(s),
3602 None,
3603 "unknown modality `{s}` must return None — pre-fix this \
3604 fell back to Modality::Text, advertising a capability \
3605 the node didn't actually have",
3606 );
3607 }
3608 }
3609
3610 #[test]
3620 fn gpu_info_from_json_saturates_fp16_tflops_to_u16_max() {
3621 let g = GpuJson {
3624 vendor: None,
3625 model: "test".to_string(),
3626 vram_gb: 0,
3627 compute_units: None,
3628 tensor_cores: None,
3629 fp16_tflops_x10: Some(1_000_000_000u32),
3630 };
3631 let info = gpu_info_from_json(g);
3632 assert_eq!(
3636 info.fp16_tflops_x10,
3637 u16::MAX as u32,
3638 "fp16_tflops_x10 must saturate at u16::MAX (65535) instead of \
3639 losing precision through the f32 round-trip; got {}",
3640 info.fp16_tflops_x10,
3641 );
3642
3643 let g_small = GpuJson {
3645 vendor: None,
3646 model: "test".to_string(),
3647 vram_gb: 0,
3648 compute_units: None,
3649 tensor_cores: None,
3650 fp16_tflops_x10: Some(425), };
3652 let info_small = gpu_info_from_json(g_small);
3653 assert_eq!(
3654 info_small.fp16_tflops_x10, 425,
3655 "small fp16_tflops_x10 must round-trip exactly"
3656 );
3657 }
3658
3659 #[test]
3672 fn alloc_bytes_round_trip_across_sizes() {
3673 for size in [0usize, 1, 15, 16, 17, 32, 64, 1024, 8192] {
3674 let src: Vec<u8> = (0..size).map(|i| (i as u8).wrapping_mul(37)).collect();
3675 let mut ptr: *mut u8 = std::ptr::null_mut();
3676 let mut len: usize = 0;
3677 let rc = alloc_bytes(&src, &mut ptr as *mut _, &mut len as *mut _);
3678 assert_eq!(rc, 0);
3679 assert_eq!(len, size);
3680 if size == 0 {
3681 assert!(ptr.is_null());
3682 } else {
3683 assert!(!ptr.is_null());
3684 let observed = unsafe { std::slice::from_raw_parts(ptr, len) };
3685 assert_eq!(observed, &src[..]);
3686 }
3687 unsafe { net_free_bytes(ptr, len) };
3690 }
3691 }
3692
3693 #[test]
3694 fn net_free_bytes_null_and_zero_len_are_noops() {
3695 unsafe { net_free_bytes(std::ptr::null_mut(), 0) };
3697 unsafe { net_free_bytes(std::ptr::null_mut(), 42) };
3698 let mut sentinel: u8 = 0;
3701 unsafe { net_free_bytes(&mut sentinel as *mut u8, 0) };
3702 }
3703
3704 #[test]
3716 fn net_free_bytes_does_not_panic_on_oversized_len() {
3717 let mut sentinel: u8 = 0;
3725 let ptr = &mut sentinel as *mut u8;
3726 unsafe { net_free_bytes(ptr, usize::MAX) };
3729 assert_eq!(sentinel, 0, "sentinel must not have been written through");
3732 }
3733
3734 #[test]
3743 fn net_mesh_shutdown_runs_even_with_outstanding_arc_refs() {
3744 let cfg = serde_json::json!({
3745 "bind_addr": "127.0.0.1:0",
3746 "psk_hex": "0".repeat(64),
3747 });
3748 let cfg_c = CString::new(cfg.to_string()).unwrap();
3749 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
3750 let rc = unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) };
3751 assert_eq!(rc, 0, "net_mesh_new failed: {rc}");
3752 assert!(!out.is_null());
3753
3754 let inner_clone = {
3757 let h = unsafe { &*out };
3758 Arc::clone(&h.inner)
3759 };
3760 assert!(Arc::strong_count(&inner_clone) >= 2);
3761 assert!(!inner_clone.is_shutdown());
3762
3763 let rc = unsafe { net_mesh_shutdown(out) };
3764 assert_eq!(rc, 0, "net_mesh_shutdown returned {rc}");
3765 assert!(
3766 inner_clone.is_shutdown(),
3767 "shutdown flag must be set even when extra Arc refs are outstanding"
3768 );
3769
3770 drop(inner_clone);
3771 unsafe { net_mesh_free(out) };
3775 }
3776
3777 #[test]
3789 fn handles_match_rejects_stream_node_mismatch() {
3790 fn make_node_handle() -> *mut MeshNodeHandle {
3791 let cfg = serde_json::json!({
3792 "bind_addr": "127.0.0.1:0",
3793 "psk_hex": "0".repeat(64),
3794 });
3795 let cfg_c = CString::new(cfg.to_string()).unwrap();
3796 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
3797 let rc = unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) };
3798 assert_eq!(rc, 0);
3799 assert!(!out.is_null());
3800 out
3801 }
3802
3803 let nh_a = make_node_handle();
3804 let nh_b = make_node_handle();
3805
3806 let sh_a = {
3814 let h = unsafe { &*nh_a };
3815 let node_clone: Arc<MeshNode> = Arc::clone(&h.inner);
3816 MeshStreamHandle {
3817 stream: ManuallyDrop::new(CoreStream {
3818 peer_node_id: 0xDEAD,
3819 stream_id: 1,
3820 epoch: 0,
3821 config: StreamConfig::new(),
3822 }),
3823 _node: ManuallyDrop::new(node_clone),
3824 guard: HandleGuard::new(),
3825 }
3826 };
3827
3828 assert!(
3830 handles_match(&sh_a, unsafe { &*nh_a }),
3831 "stream from node_a + node_a handle must match"
3832 );
3833 assert!(
3835 !handles_match(&sh_a, unsafe { &*nh_b }),
3836 "stream from node_a + node_b handle must be rejected (#19)"
3837 );
3838
3839 unsafe {
3848 let mut sh_a = sh_a;
3849 let _ = ManuallyDrop::take(&mut sh_a.stream);
3850 let _ = ManuallyDrop::take(&mut sh_a._node);
3851 }
3852 unsafe { net_mesh_free(nh_a) };
3853 unsafe { net_mesh_free(nh_b) };
3854 }
3855
3856 #[test]
3863 fn net_mesh_free_is_idempotent() {
3864 let cfg = serde_json::json!({
3865 "bind_addr": "127.0.0.1:0",
3866 "psk_hex": "0".repeat(64),
3867 });
3868 let cfg_c = CString::new(cfg.to_string()).unwrap();
3869 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
3870 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
3871 assert!(!nh.is_null());
3872
3873 unsafe { net_mesh_free(nh) };
3874 unsafe { net_mesh_free(nh) };
3878 }
3879
3880 #[test]
3884 fn net_identity_free_is_idempotent() {
3885 let mut h: *mut IdentityHandle = std::ptr::null_mut();
3886 assert_eq!(unsafe { net_identity_generate(&mut h) }, 0);
3887 assert!(!h.is_null());
3888
3889 unsafe { net_identity_free(h) };
3890 unsafe { net_identity_free(h) };
3892 }
3893
3894 #[test]
3906 fn net_mesh_free_waits_for_inflight_op() {
3907 use std::sync::atomic::{AtomicBool, Ordering};
3908 use std::time::{Duration, Instant};
3909
3910 let cfg = serde_json::json!({
3911 "bind_addr": "127.0.0.1:0",
3912 "psk_hex": "0".repeat(64),
3913 });
3914 let cfg_c = CString::new(cfg.to_string()).unwrap();
3915 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
3916 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
3917 assert!(!nh.is_null());
3918
3919 let nh_addr = nh as usize;
3922 let started = Arc::new(AtomicBool::new(false));
3923 let release = Arc::new(AtomicBool::new(false));
3924 let started_w = started.clone();
3925 let release_w = release.clone();
3926
3927 let worker = std::thread::spawn(move || {
3928 let h = unsafe { &*(nh_addr as *mut MeshNodeHandle) };
3929 let op = h.guard.try_enter().expect("entry must succeed pre-free");
3933 started_w.store(true, Ordering::SeqCst);
3934 while !release_w.load(Ordering::SeqCst) {
3935 std::thread::sleep(Duration::from_millis(1));
3936 }
3937 drop(op);
3938 });
3939
3940 while !started.load(Ordering::SeqCst) {
3942 std::thread::yield_now();
3943 }
3944
3945 let release_clone = release.clone();
3948 std::thread::spawn(move || {
3949 std::thread::sleep(Duration::from_millis(50));
3950 release_clone.store(true, Ordering::SeqCst);
3951 });
3952
3953 let t0 = Instant::now();
3955 unsafe { net_mesh_free(nh) };
3956 let elapsed = t0.elapsed();
3957 assert!(
3958 elapsed >= Duration::from_millis(40),
3959 "net_mesh_free returned in {:?} — pre-fix it would have proceeded \
3960 immediately and the worker's subsequent op would UAF",
3961 elapsed,
3962 );
3963 worker.join().unwrap();
3964 }
3965
3966 #[test]
3973 fn net_mesh_stream_stats_returns_shutting_down_after_free() {
3974 let cfg = serde_json::json!({
3975 "bind_addr": "127.0.0.1:0",
3976 "psk_hex": "0".repeat(64),
3977 });
3978 let cfg_c = CString::new(cfg.to_string()).unwrap();
3979 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
3980 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
3981 assert!(!nh.is_null());
3982
3983 unsafe { net_mesh_free(nh) };
3986
3987 let mut out_json: *mut c_char = std::ptr::null_mut();
3988 let mut out_len: usize = 0;
3989 let rc = unsafe { net_mesh_stream_stats(nh, 0xDEAD, 1, &mut out_json, &mut out_len) };
3990 assert_eq!(
3991 rc,
3992 NetError::ShuttingDown as c_int,
3993 "post-free stream_stats must surface ShuttingDown (got {rc})",
3994 );
3995 assert!(
3996 out_json.is_null(),
3997 "no payload may be written after the guard fires",
3998 );
3999 }
4000
4001 #[test]
4006 fn net_identity_issue_token_returns_shutting_down_after_free() {
4007 let mut signer: *mut IdentityHandle = std::ptr::null_mut();
4008 assert_eq!(unsafe { net_identity_generate(&mut signer) }, 0);
4009 assert!(!signer.is_null());
4010 unsafe { net_identity_free(signer) };
4011
4012 let subject = [0u8; 32];
4015 let scope = CString::new("[\"publish\"]").unwrap();
4016 let channel = CString::new("test-channel").unwrap();
4017 let mut out_token: *mut u8 = std::ptr::null_mut();
4018 let mut out_token_len: usize = 0;
4019 let rc = unsafe {
4020 net_identity_issue_token(
4021 signer,
4022 subject.as_ptr(),
4023 subject.len(),
4024 scope.as_ptr(),
4025 channel.as_ptr(),
4026 60,
4027 0,
4028 &mut out_token,
4029 &mut out_token_len,
4030 )
4031 };
4032 assert_eq!(
4033 rc,
4034 NetError::ShuttingDown as c_int,
4035 "post-free issue_token must surface ShuttingDown (got {rc})",
4036 );
4037 assert!(out_token.is_null(), "no token bytes may be allocated");
4038 }
4039
4040 #[test]
4046 fn net_delegate_token_returns_shutting_down_after_free() {
4047 let mut signer: *mut IdentityHandle = std::ptr::null_mut();
4048 assert_eq!(unsafe { net_identity_generate(&mut signer) }, 0);
4049 assert!(!signer.is_null());
4050
4051 let subject = [0u8; 32];
4053 let scope = CString::new("[\"publish\",\"delegate\"]").unwrap();
4054 let channel = CString::new("test-channel").unwrap();
4055 let mut parent_bytes: *mut u8 = std::ptr::null_mut();
4056 let mut parent_len: usize = 0;
4057 assert_eq!(
4058 unsafe {
4059 net_identity_issue_token(
4060 signer,
4061 subject.as_ptr(),
4062 subject.len(),
4063 scope.as_ptr(),
4064 channel.as_ptr(),
4065 60,
4066 1,
4067 &mut parent_bytes,
4068 &mut parent_len,
4069 )
4070 },
4071 0,
4072 );
4073 assert!(!parent_bytes.is_null());
4074
4075 unsafe { net_identity_free(signer) };
4077
4078 let new_subject = [1u8; 32];
4079 let restricted = CString::new("[\"publish\"]").unwrap();
4080 let mut child_bytes: *mut u8 = std::ptr::null_mut();
4081 let mut child_len: usize = 0;
4082 let rc = unsafe {
4083 net_delegate_token(
4084 signer,
4085 parent_bytes,
4086 parent_len,
4087 new_subject.as_ptr(),
4088 new_subject.len(),
4089 restricted.as_ptr(),
4090 &mut child_bytes,
4091 &mut child_len,
4092 )
4093 };
4094 assert_eq!(
4095 rc,
4096 NetError::ShuttingDown as c_int,
4097 "post-free delegate_token must surface ShuttingDown (got {rc})",
4098 );
4099 assert!(child_bytes.is_null(), "no child token may be allocated");
4100
4101 unsafe { net_free_bytes(parent_bytes, parent_len) };
4103 }
4104
4105 #[test]
4106 fn hardware_from_json_saturates_overflow_cpu_fields() {
4107 let h = HardwareJson {
4110 cpu_cores: Some(70_000),
4111 cpu_threads: Some(200_000),
4112 memory_gb: None,
4113 gpu: None,
4114 additional_gpus: Vec::new(),
4115 storage_gb: None,
4116 network_gbps: None,
4117 accelerators: Vec::new(),
4118 };
4119 let hw = hardware_from_json(h);
4120 assert_eq!(hw.cpu_cores, u16::MAX);
4121 assert_eq!(hw.cpu_threads, u16::MAX);
4122 }
4123
4124 #[test]
4131 fn token_entry_points_reject_oversize_len() {
4132 let invalid_json: c_int = NetError::InvalidJson.into();
4133 let mut sentinel: u8 = 0;
4134 let token = &mut sentinel as *mut u8 as *const u8;
4135
4136 let mut out_json: *mut c_char = std::ptr::null_mut();
4137 let mut out_len: usize = 0;
4138 assert_eq!(
4139 unsafe { net_parse_token(token, usize::MAX, &mut out_json, &mut out_len) },
4140 invalid_json,
4141 );
4142 assert!(out_json.is_null());
4143
4144 let mut out_ok: c_int = -42;
4145 assert_eq!(
4146 unsafe { net_verify_token(token, usize::MAX, &mut out_ok) },
4147 invalid_json,
4148 );
4149
4150 let mut out_expired: c_int = -42;
4151 assert_eq!(
4152 unsafe { net_token_is_expired(token, usize::MAX, &mut out_expired) },
4153 invalid_json,
4154 );
4155
4156 assert_eq!(
4157 sentinel, 0,
4158 "sentinel must not be touched: the length guard fires before any deref"
4159 );
4160 }
4161}
4162
4163#[cfg(all(test, not(feature = "nat-traversal")))]
4164mod nat_traversal_stub_tests {
4165 use super::*;
4182 use std::ptr;
4183
4184 #[test]
4185 fn nat_type_stub_returns_unsupported() {
4186 let mut out_str: *mut c_char = ptr::null_mut();
4187 let mut out_len: usize = 0;
4188 let code = unsafe { net_mesh_nat_type(ptr::null_mut(), &mut out_str, &mut out_len) };
4191 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4192 }
4193
4194 #[test]
4195 fn reflex_addr_stub_returns_unsupported() {
4196 let mut out_str: *mut c_char = ptr::null_mut();
4197 let mut out_len: usize = 0;
4198 let code = unsafe { net_mesh_reflex_addr(ptr::null_mut(), &mut out_str, &mut out_len) };
4200 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4201 }
4202
4203 #[test]
4204 fn peer_nat_type_stub_returns_unsupported() {
4205 let mut out_str: *mut c_char = ptr::null_mut();
4206 let mut out_len: usize = 0;
4207 let code =
4209 unsafe { net_mesh_peer_nat_type(ptr::null_mut(), 0, &mut out_str, &mut out_len) };
4210 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4211 }
4212
4213 #[test]
4214 fn probe_reflex_stub_returns_unsupported() {
4215 let mut out_str: *mut c_char = ptr::null_mut();
4216 let mut out_len: usize = 0;
4217 let code = unsafe { net_mesh_probe_reflex(ptr::null_mut(), 0, &mut out_str, &mut out_len) };
4219 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4220 }
4221
4222 #[test]
4223 fn reclassify_nat_stub_returns_unsupported() {
4224 let code = unsafe { net_mesh_reclassify_nat(ptr::null_mut()) };
4226 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4227 }
4228
4229 #[test]
4230 fn traversal_stats_stub_returns_unsupported() {
4231 let mut a: u64 = 0;
4232 let mut b: u64 = 0;
4233 let mut c: u64 = 0;
4234 let code = unsafe { net_mesh_traversal_stats(ptr::null_mut(), &mut a, &mut b, &mut c) };
4236 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4237 }
4238
4239 #[test]
4240 fn connect_direct_stub_returns_unsupported() {
4241 let code = unsafe { net_mesh_connect_direct(ptr::null_mut(), 0, ptr::null(), 0) };
4243 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4244 }
4245
4246 #[test]
4247 fn set_reflex_override_stub_returns_unsupported() {
4248 let code = unsafe { net_mesh_set_reflex_override(ptr::null_mut(), ptr::null()) };
4250 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4251 }
4252
4253 #[test]
4254 fn clear_reflex_override_stub_returns_unsupported() {
4255 let code = unsafe { net_mesh_clear_reflex_override(ptr::null_mut()) };
4257 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4258 }
4259
4260 #[test]
4266 fn unsupported_code_is_stable() {
4267 assert_eq!(NET_ERR_TRAVERSAL_UNSUPPORTED, -137);
4268 }
4269
4270 #[test]
4274 fn capability_set_from_go_marshal_preserves_gpu_vendor() {
4275 let json = r#"{"hardware":{"cpu_cores":16,"memory_gb":64,"gpu":{"vendor":"nvidia","model":"h100","vram_gb":80}},"tags":["gpu"]}"#;
4276 let parsed: CapabilitySetJson = serde_json::from_str(json).expect("JSON should parse");
4277 let caps = capability_set_from_json(parsed);
4278 let views = caps.views();
4282 assert_eq!(
4283 views.hardware().gpu_vendor(),
4284 Some(super::GpuVendor::Nvidia),
4285 "vendor lost in conversion"
4286 );
4287 assert_eq!(views.hardware().memory_gb, 64);
4288 assert_eq!(views.hardware().total_vram_gb(), 80);
4289 assert!(caps.has_tag("gpu"));
4290 }
4291
4292 #[test]
4301 fn collect_payloads_rejects_null_entry_with_nonzero_length() {
4302 let buf_a = b"hello".as_slice();
4303 let buf_b = b"world".as_slice();
4304 let ptrs: [*const u8; 3] = [buf_a.as_ptr(), std::ptr::null(), buf_b.as_ptr()];
4305 let lens: [usize; 3] = [buf_a.len(), 4, buf_b.len()];
4306
4307 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 3) };
4308 assert!(
4309 result.is_none(),
4310 "null entry with non-zero length must reject the whole batch"
4311 );
4312 }
4313
4314 #[test]
4315 fn collect_payloads_allows_null_entry_with_zero_length() {
4316 let buf_a = b"hello".as_slice();
4317 let ptrs: [*const u8; 2] = [buf_a.as_ptr(), std::ptr::null()];
4318 let lens: [usize; 2] = [buf_a.len(), 0];
4319
4320 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 2) }
4321 .expect("zero-length null is treated as empty payload");
4322 assert_eq!(result.len(), 2);
4323 assert_eq!(&result[0][..], b"hello");
4324 assert!(result[1].is_empty());
4325 }
4326
4327 #[test]
4328 fn collect_payloads_happy_path() {
4329 let buf_a = b"abc".as_slice();
4330 let buf_b = b"defg".as_slice();
4331 let ptrs: [*const u8; 2] = [buf_a.as_ptr(), buf_b.as_ptr()];
4332 let lens: [usize; 2] = [buf_a.len(), buf_b.len()];
4333
4334 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 2) }
4335 .expect("non-null entries should succeed");
4336 assert_eq!(result.len(), 2);
4337 assert_eq!(&result[0][..], b"abc");
4338 assert_eq!(&result[1][..], b"defg");
4339 }
4340}