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
224fn block_on<F: std::future::Future>(future: F) -> F::Output {
239 if tokio::runtime::Handle::try_current().is_ok() {
240 eprintln!(
241 "FATAL: mesh FFI called from inside a tokio runtime context; \
242 aborting to avoid runtime-in-runtime panic across the FFI boundary"
243 );
244 std::process::abort();
245 }
246 runtime().block_on(future)
247}
248
249#[inline]
272pub(super) unsafe fn c_str_to_string(p: *const c_char) -> Option<String> {
273 if p.is_null() {
274 return None;
275 }
276 CStr::from_ptr(p).to_str().ok().map(str::to_owned)
277}
278
279fn write_json_out<T: Serialize>(
285 value: &T,
286 out_ptr: *mut *mut c_char,
287 out_len: *mut usize,
288) -> c_int {
289 if out_ptr.is_null() || out_len.is_null() {
290 return NetError::NullPointer.into();
291 }
292 let Ok(s) = serde_json::to_string(value) else {
293 return NetError::Unknown.into();
294 };
295 let len = s.len();
296 let Ok(cs) = CString::new(s) else {
297 return NetError::Unknown.into();
298 };
299 unsafe {
300 *out_ptr = cs.into_raw();
301 *out_len = len;
302 }
303 0
304}
305
306pub(super) fn write_string_out(s: String, out_ptr: *mut *mut c_char, out_len: *mut usize) -> c_int {
307 if out_ptr.is_null() || out_len.is_null() {
308 return NetError::NullPointer.into();
309 }
310 let len = s.len();
311 let Ok(cs) = CString::new(s) else {
312 return NetError::Unknown.into();
313 };
314 unsafe {
315 *out_ptr = cs.into_raw();
316 *out_len = len;
317 }
318 0
319}
320
321fn adapter_err_to_code(err: &AdapterError) -> c_int {
322 match err {
323 AdapterError::Connection(_) => NET_ERR_MESH_HANDSHAKE,
324 _ => NET_ERR_MESH_TRANSPORT,
325 }
326}
327
328fn stream_err_to_code(err: &StreamError) -> c_int {
329 match err {
330 StreamError::Backpressure => NET_ERR_MESH_BACKPRESSURE,
331 StreamError::NotConnected => NET_ERR_MESH_NOT_CONNECTED,
332 StreamError::Transport(_) => NET_ERR_MESH_TRANSPORT,
333 }
334}
335
336#[derive(Deserialize)]
341struct SubnetPolicyJson {
342 #[serde(default)]
343 rules: Vec<SubnetRuleJson>,
344}
345
346#[derive(Deserialize)]
347struct SubnetRuleJson {
348 tag_prefix: String,
349 level: u32,
350 #[serde(default)]
351 values: std::collections::HashMap<String, u32>,
352}
353
354fn u8_from_u32(value: u32) -> Option<u8> {
355 if value > 255 {
356 None
357 } else {
358 Some(value as u8)
359 }
360}
361
362fn subnet_id_from_json(levels: Vec<u32>) -> Option<SubnetId> {
363 if levels.is_empty() || levels.len() > 4 {
364 return None;
365 }
366 let mut bytes = [0u8; 4];
367 for (i, raw) in levels.iter().enumerate() {
368 bytes[i] = u8_from_u32(*raw)?;
369 }
370 Some(SubnetId::new(&bytes[..levels.len()]))
371}
372
373fn subnet_policy_from_json(p: SubnetPolicyJson) -> Option<SubnetPolicy> {
374 let mut policy = SubnetPolicy::new();
375 for rule_json in p.rules {
376 let level = u8_from_u32(rule_json.level)?;
377 if level > 3 {
378 return None;
379 }
380 let mut rule = SubnetRule::new(rule_json.tag_prefix, level);
381 for (tag_value, raw_val) in rule_json.values {
382 let v = u8_from_u32(raw_val)?;
383 if v == 0 {
389 return None;
390 }
391 rule = rule.map(tag_value, v);
392 }
393 policy = policy.add_rule(rule);
394 }
395 Some(policy)
396}
397
398#[derive(Deserialize)]
399struct MeshNewConfig {
400 bind_addr: String,
401 psk_hex: String,
403 heartbeat_ms: Option<u64>,
404 session_timeout_ms: Option<u64>,
405 num_shards: Option<u16>,
406 capability_gc_interval_ms: Option<u64>,
409 require_signed_capabilities: Option<bool>,
412 subnet: Option<Vec<u32>>,
414 subnet_policy: Option<SubnetPolicyJson>,
416 identity_seed_hex: Option<String>,
421 #[serde(default)]
427 reflex_override: Option<String>,
428 #[serde(default)]
432 try_port_mapping: bool,
433}
434
435pub struct MeshNodeHandle {
448 inner: ManuallyDrop<Arc<MeshNode>>,
449 channel_configs: ManuallyDrop<Arc<ChannelConfigRegistry>>,
450 guard: HandleGuard,
451}
452
453#[unsafe(no_mangle)]
468pub unsafe extern "C" fn net_mesh_new(
469 config_json: *const c_char,
470 out_handle: *mut *mut MeshNodeHandle,
471) -> c_int {
472 if config_json.is_null() || out_handle.is_null() {
473 return NetError::NullPointer.into();
474 }
475 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
476 return NetError::InvalidUtf8.into();
477 };
478 let cfg: MeshNewConfig = match serde_json::from_str(&s) {
479 Ok(v) => v,
480 Err(_) => return NetError::InvalidJson.into(),
481 };
482 let bind_addr: std::net::SocketAddr = match cfg.bind_addr.parse() {
483 Ok(a) => a,
484 Err(_) => return NET_ERR_MESH_INIT,
485 };
486 let psk_bytes = match hex::decode(&cfg.psk_hex) {
487 Ok(b) => b,
488 Err(_) => return NET_ERR_MESH_INIT,
489 };
490 if psk_bytes.len() != 32 {
491 return NET_ERR_MESH_INIT;
492 }
493 let mut psk = [0u8; 32];
494 psk.copy_from_slice(&psk_bytes);
495
496 let mut node_cfg = MeshNodeConfig::new(bind_addr, psk);
497 if let Some(ms) = cfg.heartbeat_ms {
505 if ms == 0 {
506 return NetError::InvalidJson.into();
507 }
508 node_cfg = node_cfg.with_heartbeat_interval(std::time::Duration::from_millis(ms));
509 }
510 if let Some(ms) = cfg.session_timeout_ms {
511 if ms == 0 {
512 return NetError::InvalidJson.into();
513 }
514 node_cfg = node_cfg.with_session_timeout(std::time::Duration::from_millis(ms));
515 }
516 if let Some(n) = cfg.num_shards {
517 node_cfg = node_cfg.with_num_shards(n);
518 }
519 if let Some(ms) = cfg.capability_gc_interval_ms {
520 node_cfg = node_cfg.with_capability_gc_interval(std::time::Duration::from_millis(ms));
521 }
522 if let Some(b) = cfg.require_signed_capabilities {
523 node_cfg = node_cfg.with_require_signed_capabilities(b);
524 }
525 if let Some(levels) = cfg.subnet {
526 let Some(id) = subnet_id_from_json(levels) else {
527 return NET_ERR_MESH_INIT;
528 };
529 node_cfg = node_cfg.with_subnet(id);
530 }
531 if let Some(policy_js) = cfg.subnet_policy {
532 let Some(policy) = subnet_policy_from_json(policy_js) else {
533 return NET_ERR_MESH_INIT;
534 };
535 node_cfg = node_cfg.with_subnet_policy(Arc::new(policy));
536 }
537 #[cfg(feature = "nat-traversal")]
538 if let Some(external_str) = cfg.reflex_override.as_deref() {
539 let Ok(external) = external_str.parse::<std::net::SocketAddr>() else {
540 return NET_ERR_MESH_INIT;
541 };
542 node_cfg = node_cfg.with_reflex_override(external);
543 }
544 #[cfg(not(feature = "nat-traversal"))]
548 let _ = cfg.reflex_override;
549 #[cfg(feature = "port-mapping")]
550 if cfg.try_port_mapping {
551 node_cfg = node_cfg.with_try_port_mapping(true);
552 }
553 #[cfg(not(feature = "port-mapping"))]
555 let _ = cfg.try_port_mapping;
556
557 let identity = match cfg.identity_seed_hex {
558 Some(seed_hex) => {
559 let bytes = match hex::decode(&seed_hex) {
560 Ok(b) => b,
561 Err(_) => return NET_ERR_MESH_INIT,
562 };
563 if bytes.len() != 32 {
564 return NET_ERR_MESH_INIT;
565 }
566 let mut arr = [0u8; 32];
567 arr.copy_from_slice(&bytes);
568 EntityKeypair::from_bytes(arr)
569 }
570 None => EntityKeypair::generate(),
571 };
572 let result = block_on(async move { MeshNode::new(identity, node_cfg).await });
573 match result {
574 Ok(mut node) => {
575 let channel_configs = Arc::new(ChannelConfigRegistry::new());
576 node.set_channel_configs(channel_configs.clone());
577 node.set_token_cache(Arc::new(TokenCache::new()));
581 let handle = Box::new(MeshNodeHandle {
582 inner: ManuallyDrop::new(Arc::new(node)),
583 channel_configs: ManuallyDrop::new(channel_configs),
584 guard: HandleGuard::new(),
585 });
586 unsafe {
587 *out_handle = Box::into_raw(handle);
588 }
589 0
590 }
591 Err(_) => NET_ERR_MESH_INIT,
592 }
593}
594
595#[unsafe(no_mangle)]
596pub unsafe extern "C" fn net_mesh_free(handle: *mut MeshNodeHandle) {
597 if handle.is_null() {
598 return;
599 }
600 let h: &MeshNodeHandle = unsafe { &*handle };
605 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
606 unsafe {
608 let mh = &mut *handle;
609 let inner = ManuallyDrop::take(&mut mh.inner);
610 let configs = ManuallyDrop::take(&mut mh.channel_configs);
611 drop(inner);
612 drop(configs);
613 }
614 } else {
615 tracing::warn!(
616 "net_mesh_free: in-flight ops did not drain within deadline; \
617 leaking inner to avoid use-after-free"
618 );
619 }
620}
621
622#[unsafe(no_mangle)]
630pub unsafe extern "C" fn net_mesh_arc_clone(handle: *mut MeshNodeHandle) -> *mut Arc<MeshNode> {
631 if handle.is_null() {
632 return std::ptr::null_mut();
633 }
634 let h = unsafe { &*handle };
635 let _op = match h.guard.try_enter() {
637 Some(op) => op,
638 None => return std::ptr::null_mut(),
639 };
640 let cloned: Arc<MeshNode> = Arc::clone(&h.inner);
641 Box::into_raw(Box::new(cloned))
642}
643
644#[unsafe(no_mangle)]
651pub unsafe extern "C" fn net_mesh_channel_configs_arc_clone(
652 handle: *mut MeshNodeHandle,
653) -> *mut Arc<ChannelConfigRegistry> {
654 if handle.is_null() {
655 return std::ptr::null_mut();
656 }
657 let h = unsafe { &*handle };
658 let _op = match h.guard.try_enter() {
660 Some(op) => op,
661 None => return std::ptr::null_mut(),
662 };
663 let cloned: Arc<ChannelConfigRegistry> = Arc::clone(&h.channel_configs);
664 Box::into_raw(Box::new(cloned))
665}
666
667#[unsafe(no_mangle)]
670pub unsafe extern "C" fn net_mesh_arc_free(p: *mut Arc<MeshNode>) {
671 if p.is_null() {
672 return;
673 }
674 unsafe {
675 drop(Box::from_raw(p));
676 }
677}
678
679#[unsafe(no_mangle)]
682pub unsafe extern "C" fn net_mesh_channel_configs_arc_free(p: *mut Arc<ChannelConfigRegistry>) {
683 if p.is_null() {
684 return;
685 }
686 unsafe {
687 drop(Box::from_raw(p));
688 }
689}
690
691#[unsafe(no_mangle)]
694pub unsafe extern "C" fn net_mesh_public_key_hex(
695 handle: *mut MeshNodeHandle,
696 out_ptr: *mut *mut c_char,
697 out_len: *mut usize,
698) -> c_int {
699 if handle.is_null() || out_ptr.is_null() || out_len.is_null() {
700 return NetError::NullPointer.into();
701 }
702 let h = unsafe { &*handle };
703 let _op = match h.guard.try_enter() {
704 Some(op) => op,
705 None => return NetError::ShuttingDown.into(),
706 };
707 let s = hex::encode(h.inner.public_key());
708 write_string_out(s, out_ptr, out_len)
709}
710
711#[unsafe(no_mangle)]
712pub unsafe extern "C" fn net_mesh_node_id(handle: *mut MeshNodeHandle) -> u64 {
713 if handle.is_null() {
714 return 0;
715 }
716 let h = unsafe { &*handle };
717 let _op = match h.guard.try_enter() {
719 Some(op) => op,
720 None => return 0,
721 };
722 h.inner.node_id()
723}
724
725#[unsafe(no_mangle)]
729pub unsafe extern "C" fn net_mesh_entity_id(handle: *mut MeshNodeHandle, out: *mut u8) -> c_int {
730 if handle.is_null() || out.is_null() {
731 return NetError::NullPointer.into();
732 }
733 let h = unsafe { &*handle };
734 let _op = match h.guard.try_enter() {
735 Some(op) => op,
736 None => return NetError::ShuttingDown.into(),
737 };
738 let bytes = h.inner.entity_id().as_bytes();
739 unsafe {
740 std::ptr::copy_nonoverlapping(bytes.as_ptr(), out, 32);
741 }
742 0
743}
744
745#[unsafe(no_mangle)]
747pub unsafe extern "C" fn net_mesh_connect(
748 handle: *mut MeshNodeHandle,
749 peer_addr: *const c_char,
750 peer_pubkey_hex: *const c_char,
751 peer_node_id: u64,
752) -> c_int {
753 if handle.is_null() || peer_addr.is_null() || peer_pubkey_hex.is_null() {
754 return NetError::NullPointer.into();
755 }
756 let h = unsafe { &*handle };
757 let _op = match h.guard.try_enter() {
758 Some(op) => op,
759 None => return NetError::ShuttingDown.into(),
760 };
761 let Some(addr_s) = (unsafe { c_str_to_string(peer_addr) }) else {
762 return NetError::InvalidUtf8.into();
763 };
764 let addr: std::net::SocketAddr = match addr_s.parse() {
765 Ok(a) => a,
766 Err(_) => return NET_ERR_MESH_HANDSHAKE,
767 };
768 let Some(pk_s) = (unsafe { c_str_to_string(peer_pubkey_hex) }) else {
769 return NetError::InvalidUtf8.into();
770 };
771 let pk_bytes = match hex::decode(pk_s) {
772 Ok(b) => b,
773 Err(_) => return NET_ERR_MESH_HANDSHAKE,
774 };
775 if pk_bytes.len() != 32 {
776 return NET_ERR_MESH_HANDSHAKE;
777 }
778 let mut pk = [0u8; 32];
779 pk.copy_from_slice(&pk_bytes);
780
781 let node = h.inner.clone();
782 match block_on(async move { node.connect(addr, &pk, peer_node_id).await }) {
783 Ok(_) => 0,
784 Err(e) => adapter_err_to_code(&e),
785 }
786}
787
788#[unsafe(no_mangle)]
791pub unsafe extern "C" fn net_mesh_accept(
792 handle: *mut MeshNodeHandle,
793 peer_node_id: u64,
794 out_addr: *mut *mut c_char,
795 out_len: *mut usize,
796) -> c_int {
797 if handle.is_null() || out_addr.is_null() || out_len.is_null() {
798 return NetError::NullPointer.into();
799 }
800 let h = unsafe { &*handle };
801 let _op = match h.guard.try_enter() {
802 Some(op) => op,
803 None => return NetError::ShuttingDown.into(),
804 };
805 let node = h.inner.clone();
806 match block_on(async move { node.accept(peer_node_id).await }) {
807 Ok((addr, _)) => write_string_out(addr.to_string(), out_addr, out_len),
808 Err(e) => adapter_err_to_code(&e),
809 }
810}
811
812#[unsafe(no_mangle)]
813pub unsafe extern "C" fn net_mesh_start(handle: *mut MeshNodeHandle) -> c_int {
814 if handle.is_null() {
815 return NetError::NullPointer.into();
816 }
817 let h = unsafe { &*handle };
818 let _op = match h.guard.try_enter() {
819 Some(op) => op,
820 None => return NetError::ShuttingDown.into(),
821 };
822 let node = h.inner.clone();
823 block_on(async move { node.start() });
826 0
827}
828
829#[unsafe(no_mangle)]
841pub unsafe extern "C" fn net_mesh_shutdown(handle: *mut MeshNodeHandle) -> c_int {
842 if handle.is_null() {
843 return NetError::NullPointer.into();
844 }
845 let h = unsafe { &*handle };
846 let _op = match h.guard.try_enter() {
847 Some(op) => op,
848 None => return NetError::ShuttingDown.into(),
849 };
850 match block_on(async { h.inner.shutdown().await }) {
851 Ok(()) => 0,
852 Err(e) => adapter_err_to_code(&e),
853 }
854}
855
856#[cfg(feature = "nat-traversal")]
878#[unsafe(no_mangle)]
879pub unsafe extern "C" fn net_mesh_nat_type(
880 handle: *mut MeshNodeHandle,
881 out_str: *mut *mut c_char,
882 out_len: *mut usize,
883) -> c_int {
884 if handle.is_null() || out_str.is_null() || out_len.is_null() {
885 return NetError::NullPointer.into();
886 }
887 let h = unsafe { &*handle };
888 let _op = match h.guard.try_enter() {
889 Some(op) => op,
890 None => return NetError::ShuttingDown.into(),
891 };
892 write_string_out(
893 nat_class_to_str(h.inner.nat_class()).to_string(),
894 out_str,
895 out_len,
896 )
897}
898
899#[cfg(feature = "nat-traversal")]
904#[unsafe(no_mangle)]
905pub unsafe extern "C" fn net_mesh_reflex_addr(
906 handle: *mut MeshNodeHandle,
907 out_str: *mut *mut c_char,
908 out_len: *mut usize,
909) -> c_int {
910 if handle.is_null() || out_str.is_null() || out_len.is_null() {
911 return NetError::NullPointer.into();
912 }
913 let h = unsafe { &*handle };
914 let _op = match h.guard.try_enter() {
915 Some(op) => op,
916 None => return NetError::ShuttingDown.into(),
917 };
918 let s = h
919 .inner
920 .reflex_addr()
921 .map(|a| a.to_string())
922 .unwrap_or_default();
923 write_string_out(s, out_str, out_len)
924}
925
926#[cfg(feature = "nat-traversal")]
930#[unsafe(no_mangle)]
931pub unsafe extern "C" fn net_mesh_peer_nat_type(
932 handle: *mut MeshNodeHandle,
933 peer_node_id: u64,
934 out_str: *mut *mut c_char,
935 out_len: *mut usize,
936) -> c_int {
937 if handle.is_null() || out_str.is_null() || out_len.is_null() {
938 return NetError::NullPointer.into();
939 }
940 let h = unsafe { &*handle };
941 let _op = match h.guard.try_enter() {
942 Some(op) => op,
943 None => return NetError::ShuttingDown.into(),
944 };
945 write_string_out(
946 nat_class_to_str(h.inner.peer_nat_class(peer_node_id)).to_string(),
947 out_str,
948 out_len,
949 )
950}
951
952#[cfg(feature = "nat-traversal")]
961#[unsafe(no_mangle)]
962pub unsafe extern "C" fn net_mesh_probe_reflex(
963 handle: *mut MeshNodeHandle,
964 peer_node_id: u64,
965 out_str: *mut *mut c_char,
966 out_len: *mut usize,
967) -> c_int {
968 if handle.is_null() || out_str.is_null() || out_len.is_null() {
969 return NetError::NullPointer.into();
970 }
971 let h = unsafe { &*handle };
972 let _op = match h.guard.try_enter() {
973 Some(op) => op,
974 None => return NetError::ShuttingDown.into(),
975 };
976 let node = h.inner.clone();
977 match block_on(async move { node.probe_reflex(peer_node_id).await }) {
978 Ok(addr) => write_string_out(addr.to_string(), out_str, out_len),
979 Err(e) => traversal_err_to_code(&e),
980 }
981}
982
983#[cfg(feature = "nat-traversal")]
988#[unsafe(no_mangle)]
989pub unsafe extern "C" fn net_mesh_reclassify_nat(handle: *mut MeshNodeHandle) -> c_int {
990 if handle.is_null() {
991 return NetError::NullPointer.into();
992 }
993 let h = unsafe { &*handle };
994 let _op = match h.guard.try_enter() {
995 Some(op) => op,
996 None => return NetError::ShuttingDown.into(),
997 };
998 let node = h.inner.clone();
999 block_on(async move { node.reclassify_nat().await });
1000 0
1001}
1002
1003#[cfg(feature = "nat-traversal")]
1008#[unsafe(no_mangle)]
1009pub unsafe extern "C" fn net_mesh_traversal_stats(
1010 handle: *mut MeshNodeHandle,
1011 out_punches_attempted: *mut u64,
1012 out_punches_succeeded: *mut u64,
1013 out_relay_fallbacks: *mut u64,
1014) -> c_int {
1015 if handle.is_null() {
1016 return NetError::NullPointer.into();
1017 }
1018 let h = unsafe { &*handle };
1019 let _op = match h.guard.try_enter() {
1020 Some(op) => op,
1021 None => return NetError::ShuttingDown.into(),
1022 };
1023 let snap = h.inner.traversal_stats();
1024 unsafe {
1025 if !out_punches_attempted.is_null() {
1026 *out_punches_attempted = snap.punches_attempted;
1027 }
1028 if !out_punches_succeeded.is_null() {
1029 *out_punches_succeeded = snap.punches_succeeded;
1030 }
1031 if !out_relay_fallbacks.is_null() {
1032 *out_relay_fallbacks = snap.relay_fallbacks;
1033 }
1034 }
1035 0
1036}
1037
1038#[cfg(feature = "nat-traversal")]
1050#[unsafe(no_mangle)]
1051pub unsafe extern "C" fn net_mesh_connect_direct(
1052 handle: *mut MeshNodeHandle,
1053 peer_node_id: u64,
1054 peer_pubkey_hex: *const c_char,
1055 coordinator: u64,
1056) -> c_int {
1057 if handle.is_null() || peer_pubkey_hex.is_null() {
1058 return NetError::NullPointer.into();
1059 }
1060 let h = unsafe { &*handle };
1061 let _op = match h.guard.try_enter() {
1062 Some(op) => op,
1063 None => return NetError::ShuttingDown.into(),
1064 };
1065 let Some(pk_s) = (unsafe { c_str_to_string(peer_pubkey_hex) }) else {
1066 return NetError::InvalidUtf8.into();
1067 };
1068 let pk_bytes = match hex::decode(pk_s) {
1069 Ok(b) => b,
1070 Err(_) => return NET_ERR_MESH_HANDSHAKE,
1071 };
1072 if pk_bytes.len() != 32 {
1073 return NET_ERR_MESH_HANDSHAKE;
1074 }
1075 let mut pk = [0u8; 32];
1076 pk.copy_from_slice(&pk_bytes);
1077
1078 let node = h.inner.clone();
1079 match block_on(async move { node.connect_direct(peer_node_id, &pk, coordinator).await }) {
1080 Ok(_) => 0,
1081 Err(e) => traversal_err_to_code(&e),
1082 }
1083}
1084
1085#[cfg(feature = "nat-traversal")]
1093#[unsafe(no_mangle)]
1094pub unsafe extern "C" fn net_mesh_set_reflex_override(
1095 handle: *mut MeshNodeHandle,
1096 external: *const c_char,
1097) -> c_int {
1098 if handle.is_null() || external.is_null() {
1099 return NetError::NullPointer.into();
1100 }
1101 let h = unsafe { &*handle };
1102 let _op = match h.guard.try_enter() {
1103 Some(op) => op,
1104 None => return NetError::ShuttingDown.into(),
1105 };
1106 let Some(s) = (unsafe { c_str_to_string(external) }) else {
1107 return NetError::InvalidUtf8.into();
1108 };
1109 let Ok(addr) = s.parse::<std::net::SocketAddr>() else {
1110 return NET_ERR_MESH_INIT;
1111 };
1112 h.inner.set_reflex_override(addr);
1113 0
1114}
1115
1116#[cfg(feature = "nat-traversal")]
1124#[unsafe(no_mangle)]
1125pub unsafe extern "C" fn net_mesh_clear_reflex_override(handle: *mut MeshNodeHandle) -> c_int {
1126 if handle.is_null() {
1127 return NetError::NullPointer.into();
1128 }
1129 let h = unsafe { &*handle };
1130 let _op = match h.guard.try_enter() {
1131 Some(op) => op,
1132 None => return NetError::ShuttingDown.into(),
1133 };
1134 h.inner.clear_reflex_override();
1135 0
1136}
1137
1138#[cfg(not(feature = "nat-traversal"))]
1161#[unsafe(no_mangle)]
1162pub unsafe extern "C" fn net_mesh_nat_type(
1163 _handle: *mut MeshNodeHandle,
1164 _out_str: *mut *mut c_char,
1165 _out_len: *mut usize,
1166) -> c_int {
1167 NET_ERR_TRAVERSAL_UNSUPPORTED
1168}
1169
1170#[cfg(not(feature = "nat-traversal"))]
1171#[unsafe(no_mangle)]
1172pub unsafe extern "C" fn net_mesh_reflex_addr(
1173 _handle: *mut MeshNodeHandle,
1174 _out_str: *mut *mut c_char,
1175 _out_len: *mut usize,
1176) -> c_int {
1177 NET_ERR_TRAVERSAL_UNSUPPORTED
1178}
1179
1180#[cfg(not(feature = "nat-traversal"))]
1181#[unsafe(no_mangle)]
1182pub unsafe extern "C" fn net_mesh_peer_nat_type(
1183 _handle: *mut MeshNodeHandle,
1184 _peer_node_id: u64,
1185 _out_str: *mut *mut c_char,
1186 _out_len: *mut usize,
1187) -> c_int {
1188 NET_ERR_TRAVERSAL_UNSUPPORTED
1189}
1190
1191#[cfg(not(feature = "nat-traversal"))]
1192#[unsafe(no_mangle)]
1193pub unsafe extern "C" fn net_mesh_probe_reflex(
1194 _handle: *mut MeshNodeHandle,
1195 _peer_node_id: u64,
1196 _out_str: *mut *mut c_char,
1197 _out_len: *mut usize,
1198) -> c_int {
1199 NET_ERR_TRAVERSAL_UNSUPPORTED
1200}
1201
1202#[cfg(not(feature = "nat-traversal"))]
1203#[unsafe(no_mangle)]
1204pub unsafe extern "C" fn net_mesh_reclassify_nat(_handle: *mut MeshNodeHandle) -> c_int {
1205 NET_ERR_TRAVERSAL_UNSUPPORTED
1206}
1207
1208#[cfg(not(feature = "nat-traversal"))]
1209#[unsafe(no_mangle)]
1210pub unsafe extern "C" fn net_mesh_traversal_stats(
1211 _handle: *mut MeshNodeHandle,
1212 _out_punches_attempted: *mut u64,
1213 _out_punches_succeeded: *mut u64,
1214 _out_relay_fallbacks: *mut u64,
1215) -> c_int {
1216 NET_ERR_TRAVERSAL_UNSUPPORTED
1217}
1218
1219#[cfg(not(feature = "nat-traversal"))]
1220#[unsafe(no_mangle)]
1221pub unsafe extern "C" fn net_mesh_connect_direct(
1222 _handle: *mut MeshNodeHandle,
1223 _peer_node_id: u64,
1224 _peer_pubkey_hex: *const c_char,
1225 _coordinator: u64,
1226) -> c_int {
1227 NET_ERR_TRAVERSAL_UNSUPPORTED
1228}
1229
1230#[cfg(not(feature = "nat-traversal"))]
1231#[unsafe(no_mangle)]
1232pub unsafe extern "C" fn net_mesh_set_reflex_override(
1233 _handle: *mut MeshNodeHandle,
1234 _external: *const c_char,
1235) -> c_int {
1236 NET_ERR_TRAVERSAL_UNSUPPORTED
1237}
1238
1239#[cfg(not(feature = "nat-traversal"))]
1240#[unsafe(no_mangle)]
1241pub unsafe extern "C" fn net_mesh_clear_reflex_override(_handle: *mut MeshNodeHandle) -> c_int {
1242 NET_ERR_TRAVERSAL_UNSUPPORTED
1243}
1244
1245#[derive(Deserialize, Default)]
1250struct StreamOpenConfig {
1251 reliability: Option<String>,
1253 window_bytes: Option<u32>,
1256 fairness_weight: Option<u8>,
1257}
1258
1259pub struct MeshStreamHandle {
1274 stream: ManuallyDrop<CoreStream>,
1275 _node: ManuallyDrop<Arc<MeshNode>>,
1278 guard: HandleGuard,
1279}
1280
1281#[unsafe(no_mangle)]
1282pub unsafe extern "C" fn net_mesh_open_stream(
1283 handle: *mut MeshNodeHandle,
1284 peer_node_id: u64,
1285 stream_id: u64,
1286 config_json: *const c_char,
1287 out_stream: *mut *mut MeshStreamHandle,
1288) -> c_int {
1289 if handle.is_null() || out_stream.is_null() {
1290 return NetError::NullPointer.into();
1291 }
1292 let h = unsafe { &*handle };
1293 let _op = match h.guard.try_enter() {
1294 Some(op) => op,
1295 None => return NetError::ShuttingDown.into(),
1296 };
1297 let cfg_json: StreamOpenConfig = if config_json.is_null() {
1298 StreamOpenConfig::default()
1299 } else {
1300 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1301 return NetError::InvalidUtf8.into();
1302 };
1303 match serde_json::from_str(&s) {
1304 Ok(v) => v,
1305 Err(_) => return NetError::InvalidJson.into(),
1306 }
1307 };
1308 let reliability = match cfg_json.reliability.as_deref() {
1309 None | Some("fire_and_forget") => Reliability::FireAndForget,
1310 Some("reliable") => Reliability::Reliable,
1311 Some(_) => return NET_ERR_MESH_TRANSPORT,
1312 };
1313 let window = cfg_json.window_bytes.unwrap_or(DEFAULT_STREAM_WINDOW_BYTES);
1314 let weight = cfg_json.fairness_weight.unwrap_or(1);
1315 let cfg = StreamConfig::new()
1316 .with_reliability(reliability)
1317 .with_window_bytes(window)
1318 .with_fairness_weight(weight);
1319 match h.inner.open_stream(peer_node_id, stream_id, cfg) {
1320 Ok(stream) => {
1321 let node_clone: Arc<MeshNode> = Arc::clone(&h.inner);
1322 let sh = Box::new(MeshStreamHandle {
1323 stream: ManuallyDrop::new(stream),
1324 _node: ManuallyDrop::new(node_clone),
1325 guard: HandleGuard::new(),
1326 });
1327 unsafe {
1328 *out_stream = Box::into_raw(sh);
1329 }
1330 0
1331 }
1332 Err(e) => adapter_err_to_code(&e),
1333 }
1334}
1335
1336#[unsafe(no_mangle)]
1337pub unsafe extern "C" fn net_mesh_stream_free(handle: *mut MeshStreamHandle) {
1338 if handle.is_null() {
1339 return;
1340 }
1341 let h: &MeshStreamHandle = unsafe { &*handle };
1343 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
1344 unsafe {
1346 let _stream = ManuallyDrop::take(&mut (*handle).stream);
1350 let node = ManuallyDrop::take(&mut (*handle)._node);
1351 drop(node);
1352 }
1353 } else {
1354 tracing::warn!(
1355 "net_mesh_stream_free: in-flight ops did not drain within deadline; \
1356 leaking inner to avoid use-after-free"
1357 );
1358 }
1359}
1360
1361unsafe fn collect_payloads(
1371 payloads: *const *const u8,
1372 lens: *const usize,
1373 count: usize,
1374) -> Option<Vec<Bytes>> {
1375 let mut out = Vec::with_capacity(count);
1376 for i in 0..count {
1377 let ptr = *payloads.add(i);
1378 let len = *lens.add(i);
1379 if ptr.is_null() {
1380 if len == 0 {
1381 out.push(Bytes::new());
1382 continue;
1383 }
1384 return None;
1385 }
1386 if len > isize::MAX as usize {
1390 return None;
1391 }
1392 let slice = std::slice::from_raw_parts(ptr, len);
1393 out.push(Bytes::copy_from_slice(slice));
1394 }
1395 Some(out)
1396}
1397
1398#[inline]
1406fn handles_match(sh: &MeshStreamHandle, nh: &MeshNodeHandle) -> bool {
1407 Arc::ptr_eq(&sh._node, &nh.inner)
1408}
1409
1410#[unsafe(no_mangle)]
1411pub unsafe extern "C" fn net_mesh_send(
1412 handle: *mut MeshStreamHandle,
1413 payloads: *const *const u8,
1414 lens: *const usize,
1415 count: usize,
1416 node_handle: *mut MeshNodeHandle,
1417) -> c_int {
1418 if handle.is_null() || node_handle.is_null() {
1419 return NetError::NullPointer.into();
1420 }
1421 if count > 0 && (payloads.is_null() || lens.is_null()) {
1422 return NetError::NullPointer.into();
1423 }
1424 let sh = unsafe { &*handle };
1425 let nh = unsafe { &*node_handle };
1426 let _sh_op = match sh.guard.try_enter() {
1429 Some(op) => op,
1430 None => return NetError::ShuttingDown.into(),
1431 };
1432 let _nh_op = match nh.guard.try_enter() {
1433 Some(op) => op,
1434 None => return NetError::ShuttingDown.into(),
1435 };
1436 if !handles_match(sh, nh) {
1437 return NetError::MismatchedHandles.into();
1438 }
1439 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1440 Some(v) => v,
1441 None => return NetError::NullPointer.into(),
1442 };
1443 let node = nh.inner.clone();
1444 let stream = sh.stream.clone();
1445 match block_on(async move { node.send_on_stream(&stream, &payloads).await }) {
1446 Ok(()) => 0,
1447 Err(e) => stream_err_to_code(&e),
1448 }
1449}
1450
1451#[unsafe(no_mangle)]
1452pub unsafe extern "C" fn net_mesh_send_with_retry(
1453 handle: *mut MeshStreamHandle,
1454 payloads: *const *const u8,
1455 lens: *const usize,
1456 count: usize,
1457 max_retries: u32,
1458 node_handle: *mut MeshNodeHandle,
1459) -> c_int {
1460 if handle.is_null() || node_handle.is_null() {
1461 return NetError::NullPointer.into();
1462 }
1463 if count > 0 && (payloads.is_null() || lens.is_null()) {
1464 return NetError::NullPointer.into();
1465 }
1466 let sh = unsafe { &*handle };
1467 let nh = unsafe { &*node_handle };
1468 let _sh_op = match sh.guard.try_enter() {
1471 Some(op) => op,
1472 None => return NetError::ShuttingDown.into(),
1473 };
1474 let _nh_op = match nh.guard.try_enter() {
1475 Some(op) => op,
1476 None => return NetError::ShuttingDown.into(),
1477 };
1478 if !handles_match(sh, nh) {
1479 return NetError::MismatchedHandles.into();
1480 }
1481 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1482 Some(v) => v,
1483 None => return NetError::NullPointer.into(),
1484 };
1485 let node = nh.inner.clone();
1486 let stream = sh.stream.clone();
1487 match block_on(async move {
1488 node.send_with_retry(&stream, &payloads, max_retries as usize)
1489 .await
1490 }) {
1491 Ok(()) => 0,
1492 Err(e) => stream_err_to_code(&e),
1493 }
1494}
1495
1496#[unsafe(no_mangle)]
1497pub unsafe extern "C" fn net_mesh_send_blocking(
1498 handle: *mut MeshStreamHandle,
1499 payloads: *const *const u8,
1500 lens: *const usize,
1501 count: usize,
1502 node_handle: *mut MeshNodeHandle,
1503) -> c_int {
1504 if handle.is_null() || node_handle.is_null() {
1505 return NetError::NullPointer.into();
1506 }
1507 if count > 0 && (payloads.is_null() || lens.is_null()) {
1508 return NetError::NullPointer.into();
1509 }
1510 let sh = unsafe { &*handle };
1511 let nh = unsafe { &*node_handle };
1512 let _sh_op = match sh.guard.try_enter() {
1515 Some(op) => op,
1516 None => return NetError::ShuttingDown.into(),
1517 };
1518 let _nh_op = match nh.guard.try_enter() {
1519 Some(op) => op,
1520 None => return NetError::ShuttingDown.into(),
1521 };
1522 if !handles_match(sh, nh) {
1523 return NetError::MismatchedHandles.into();
1524 }
1525 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1526 Some(v) => v,
1527 None => return NetError::NullPointer.into(),
1528 };
1529 let node = nh.inner.clone();
1530 let stream = sh.stream.clone();
1531 match block_on(async move { node.send_blocking(&stream, &payloads).await }) {
1532 Ok(()) => 0,
1533 Err(e) => stream_err_to_code(&e),
1534 }
1535}
1536
1537#[derive(Serialize)]
1538struct StreamStatsJson {
1539 tx_seq: u64,
1540 rx_seq: u64,
1541 inbound_pending: u64,
1542 last_activity_ns: u64,
1543 active: bool,
1544 backpressure_events: u64,
1545 tx_credit_remaining: u32,
1546 tx_window: u32,
1547 credit_grants_received: u64,
1548 credit_grants_sent: u64,
1549}
1550
1551#[unsafe(no_mangle)]
1552pub unsafe extern "C" fn net_mesh_stream_stats(
1553 node_handle: *mut MeshNodeHandle,
1554 peer_node_id: u64,
1555 stream_id: u64,
1556 out_json: *mut *mut c_char,
1557 out_len: *mut usize,
1558) -> c_int {
1559 if node_handle.is_null() || out_json.is_null() || out_len.is_null() {
1560 return NetError::NullPointer.into();
1561 }
1562 let h = unsafe { &*node_handle };
1563 let _op = match h.guard.try_enter() {
1564 Some(op) => op,
1565 None => return NetError::ShuttingDown.into(),
1566 };
1567 match h.inner.stream_stats(peer_node_id, stream_id) {
1568 Some(s) => {
1569 let js = StreamStatsJson {
1570 tx_seq: s.tx_seq,
1571 rx_seq: s.rx_seq,
1572 inbound_pending: s.inbound_pending,
1573 last_activity_ns: s.last_activity_ns,
1574 active: s.active,
1575 backpressure_events: s.backpressure_events,
1576 tx_credit_remaining: s.tx_credit_remaining,
1577 tx_window: s.tx_window,
1578 credit_grants_received: s.credit_grants_received,
1579 credit_grants_sent: s.credit_grants_sent,
1580 };
1581 write_json_out(&js, out_json, out_len)
1582 }
1583 None => {
1584 write_string_out("null".to_string(), out_json, out_len)
1587 }
1588 }
1589}
1590
1591#[derive(Serialize)]
1596struct RecvEventJson {
1597 id: String,
1598 payload_b64: String,
1600 insertion_ts: u64,
1601 shard_id: u16,
1602}
1603
1604#[unsafe(no_mangle)]
1605pub unsafe extern "C" fn net_mesh_recv_shard(
1606 handle: *mut MeshNodeHandle,
1607 shard_id: u16,
1608 limit: u32,
1609 out_json: *mut *mut c_char,
1610 out_len: *mut usize,
1611) -> c_int {
1612 if handle.is_null() || out_json.is_null() || out_len.is_null() {
1613 return NetError::NullPointer.into();
1614 }
1615 let h = unsafe { &*handle };
1616 let _op = match h.guard.try_enter() {
1617 Some(op) => op,
1618 None => return NetError::ShuttingDown.into(),
1619 };
1620 let node = h.inner.clone();
1621 let result = block_on(async move { node.poll_shard(shard_id, None, limit as usize).await });
1622 let result = match result {
1623 Ok(r) => r,
1624 Err(e) => return adapter_err_to_code(&e),
1625 };
1626 let events: Vec<RecvEventJson> = result
1627 .events
1628 .into_iter()
1629 .map(|e| RecvEventJson {
1630 id: e.id,
1631 payload_b64: encode_b64(&e.raw),
1632 insertion_ts: e.insertion_ts,
1633 shard_id: e.shard_id,
1634 })
1635 .collect();
1636 write_json_out(&events, out_json, out_len)
1637}
1638
1639fn encode_b64(bytes: &[u8]) -> String {
1640 const ALPH: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1643 let mut s = String::with_capacity(bytes.len().div_ceil(3) * 4);
1644 let mut i = 0;
1645 while i + 3 <= bytes.len() {
1646 let chunk = &bytes[i..i + 3];
1647 s.push(ALPH[(chunk[0] >> 2) as usize] as char);
1648 s.push(ALPH[(((chunk[0] & 0b11) << 4) | (chunk[1] >> 4)) as usize] as char);
1649 s.push(ALPH[(((chunk[1] & 0b1111) << 2) | (chunk[2] >> 6)) as usize] as char);
1650 s.push(ALPH[(chunk[2] & 0b111111) as usize] as char);
1651 i += 3;
1652 }
1653 let rem = bytes.len() - i;
1654 if rem == 1 {
1655 let b = bytes[i];
1656 s.push(ALPH[(b >> 2) as usize] as char);
1657 s.push(ALPH[((b & 0b11) << 4) as usize] as char);
1658 s.push('=');
1659 s.push('=');
1660 } else if rem == 2 {
1661 let b0 = bytes[i];
1662 let b1 = bytes[i + 1];
1663 s.push(ALPH[(b0 >> 2) as usize] as char);
1664 s.push(ALPH[(((b0 & 0b11) << 4) | (b1 >> 4)) as usize] as char);
1665 s.push(ALPH[((b1 & 0b1111) << 2) as usize] as char);
1666 s.push('=');
1667 }
1668 s
1669}
1670
1671#[derive(Deserialize)]
1676struct ChannelConfigInput {
1677 name: String,
1678 visibility: Option<String>,
1679 reliable: Option<bool>,
1680 require_token: Option<bool>,
1681 priority: Option<u8>,
1682 max_rate_pps: Option<u32>,
1683 publish_caps: Option<CapabilityFilterJson>,
1687 subscribe_caps: Option<CapabilityFilterJson>,
1691}
1692
1693fn parse_visibility(s: &str) -> Option<InnerVisibility> {
1694 match s {
1695 "subnet-local" => Some(InnerVisibility::SubnetLocal),
1696 "parent-visible" => Some(InnerVisibility::ParentVisible),
1697 "exported" => Some(InnerVisibility::Exported),
1698 "global" => Some(InnerVisibility::Global),
1699 _ => None,
1700 }
1701}
1702
1703#[unsafe(no_mangle)]
1704pub unsafe extern "C" fn net_mesh_register_channel(
1705 handle: *mut MeshNodeHandle,
1706 config_json: *const c_char,
1707) -> c_int {
1708 if handle.is_null() || config_json.is_null() {
1709 return NetError::NullPointer.into();
1710 }
1711 let h = unsafe { &*handle };
1712 let _op = match h.guard.try_enter() {
1713 Some(op) => op,
1714 None => return NetError::ShuttingDown.into(),
1715 };
1716 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1717 return NetError::InvalidUtf8.into();
1718 };
1719 let input: ChannelConfigInput = match serde_json::from_str(&s) {
1720 Ok(v) => v,
1721 Err(_) => return NetError::InvalidJson.into(),
1722 };
1723 let name = match InnerChannelName::new(&input.name) {
1724 Ok(n) => n,
1725 Err(_) => return NET_ERR_CHANNEL,
1726 };
1727 let mut cfg = InnerChannelConfig::new(ChannelId::new(name));
1728 if let Some(v) = input.visibility {
1729 let Some(vis) = parse_visibility(&v) else {
1730 return NET_ERR_CHANNEL;
1731 };
1732 cfg = cfg.with_visibility(vis);
1733 }
1734 if let Some(r) = input.reliable {
1735 cfg = cfg.with_reliable(r);
1736 }
1737 if let Some(t) = input.require_token {
1738 cfg = cfg.with_require_token(t);
1739 }
1740 if let Some(p) = input.priority {
1741 cfg = cfg.with_priority(p);
1742 }
1743 if let Some(pps) = input.max_rate_pps {
1744 cfg = cfg.with_rate_limit(pps);
1745 }
1746 if let Some(filter_json) = input.publish_caps {
1747 cfg = cfg.with_publish_caps(capability_filter_from_json(filter_json));
1748 }
1749 if let Some(filter_json) = input.subscribe_caps {
1750 cfg = cfg.with_subscribe_caps(capability_filter_from_json(filter_json));
1751 }
1752 h.channel_configs.insert(cfg);
1753 0
1754}
1755
1756#[unsafe(no_mangle)]
1757pub unsafe extern "C" fn net_mesh_subscribe_channel(
1758 handle: *mut MeshNodeHandle,
1759 publisher_node_id: u64,
1760 channel: *const c_char,
1761) -> c_int {
1762 subscribe_or_unsubscribe(handle, publisher_node_id, channel, true)
1763}
1764
1765#[unsafe(no_mangle)]
1766pub unsafe extern "C" fn net_mesh_unsubscribe_channel(
1767 handle: *mut MeshNodeHandle,
1768 publisher_node_id: u64,
1769 channel: *const c_char,
1770) -> c_int {
1771 subscribe_or_unsubscribe(handle, publisher_node_id, channel, false)
1772}
1773
1774#[unsafe(no_mangle)]
1781pub unsafe extern "C" fn net_mesh_subscribe_channel_with_token(
1782 handle: *mut MeshNodeHandle,
1783 publisher_node_id: u64,
1784 channel: *const c_char,
1785 token: *const u8,
1786 token_len: usize,
1787) -> c_int {
1788 if handle.is_null() || channel.is_null() || token.is_null() {
1789 return NetError::NullPointer.into();
1790 }
1791 let h = unsafe { &*handle };
1792 let _op = match h.guard.try_enter() {
1793 Some(op) => op,
1794 None => return NetError::ShuttingDown.into(),
1795 };
1796 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
1797 return NetError::InvalidUtf8.into();
1798 };
1799 let name = match InnerChannelName::new(&s) {
1800 Ok(n) => n,
1801 Err(_) => return NET_ERR_CHANNEL,
1802 };
1803 if token_len > isize::MAX as usize {
1805 return NetError::InvalidJson.into();
1806 }
1807 let slice = unsafe { std::slice::from_raw_parts(token, token_len) };
1808 let parsed = match PermissionToken::from_bytes(slice) {
1809 Ok(t) => t,
1810 Err(e) => return token_err_to_code(&e),
1811 };
1812 let node = h.inner.clone();
1813 match block_on(async move {
1814 node.subscribe_channel_with_token(publisher_node_id, name, parsed)
1815 .await
1816 }) {
1817 Ok(()) => 0,
1818 Err(e) => adapter_err_to_channel_code(&e),
1819 }
1820}
1821
1822fn subscribe_or_unsubscribe(
1823 handle: *mut MeshNodeHandle,
1824 publisher_node_id: u64,
1825 channel: *const c_char,
1826 subscribe: bool,
1827) -> c_int {
1828 if handle.is_null() || channel.is_null() {
1829 return NetError::NullPointer.into();
1830 }
1831 let h = unsafe { &*handle };
1832 let _op = match h.guard.try_enter() {
1833 Some(op) => op,
1834 None => return NetError::ShuttingDown.into(),
1835 };
1836 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
1837 return NetError::InvalidUtf8.into();
1838 };
1839 let name = match InnerChannelName::new(&s) {
1840 Ok(n) => n,
1841 Err(_) => return NET_ERR_CHANNEL,
1842 };
1843 let node = h.inner.clone();
1844 let outcome = if subscribe {
1845 block_on(async move { node.subscribe_channel(publisher_node_id, name).await })
1846 } else {
1847 block_on(async move { node.unsubscribe_channel(publisher_node_id, name).await })
1848 };
1849 match outcome {
1850 Ok(()) => 0,
1851 Err(e) => adapter_err_to_channel_code(&e),
1852 }
1853}
1854
1855fn adapter_err_to_channel_code(err: &AdapterError) -> c_int {
1856 if let AdapterError::Connection(msg) = err {
1857 let prefix = "membership request rejected: ";
1858 if let Some(tail) = msg.strip_prefix(prefix) {
1859 if tail.trim() == "Some(Unauthorized)" {
1860 return NET_ERR_CHANNEL_AUTH;
1861 }
1862 }
1863 }
1864 NET_ERR_CHANNEL
1865}
1866
1867#[derive(Deserialize, Default)]
1868struct PublishConfigInput {
1869 reliability: Option<String>,
1870 on_failure: Option<String>,
1871 max_inflight: Option<u32>,
1872}
1873
1874#[derive(Serialize)]
1875struct PublishReportJson {
1876 attempted: u32,
1877 delivered: u32,
1878 errors: Vec<PublishFailureJson>,
1879}
1880
1881#[derive(Serialize)]
1882struct PublishFailureJson {
1883 node_id: u64,
1884 message: String,
1885}
1886
1887fn to_publish_report_json(r: InnerPublishReport) -> PublishReportJson {
1888 PublishReportJson {
1889 attempted: r.attempted as u32,
1890 delivered: r.delivered as u32,
1891 errors: r
1892 .errors
1893 .into_iter()
1894 .map(|(id, e)| PublishFailureJson {
1895 node_id: id,
1896 message: format!("{}", e),
1897 })
1898 .collect(),
1899 }
1900}
1901
1902#[unsafe(no_mangle)]
1903pub unsafe extern "C" fn net_mesh_publish(
1904 handle: *mut MeshNodeHandle,
1905 channel: *const c_char,
1906 payload: *const u8,
1907 len: usize,
1908 config_json: *const c_char,
1909 out_json: *mut *mut c_char,
1910 out_len: *mut usize,
1911) -> c_int {
1912 if handle.is_null() || channel.is_null() || out_json.is_null() || out_len.is_null() {
1913 return NetError::NullPointer.into();
1914 }
1915 let h = unsafe { &*handle };
1916 let _op = match h.guard.try_enter() {
1917 Some(op) => op,
1918 None => return NetError::ShuttingDown.into(),
1919 };
1920 let Some(ch) = (unsafe { c_str_to_string(channel) }) else {
1921 return NetError::InvalidUtf8.into();
1922 };
1923 let name = match InnerChannelName::new(&ch) {
1924 Ok(n) => n,
1925 Err(_) => return NET_ERR_CHANNEL,
1926 };
1927 let cfg_in: PublishConfigInput = if config_json.is_null() {
1928 PublishConfigInput::default()
1929 } else {
1930 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1931 return NetError::InvalidUtf8.into();
1932 };
1933 match serde_json::from_str(&s) {
1934 Ok(v) => v,
1935 Err(_) => return NetError::InvalidJson.into(),
1936 }
1937 };
1938 let reliability = match cfg_in.reliability.as_deref() {
1939 None | Some("fire_and_forget") => Reliability::FireAndForget,
1940 Some("reliable") => Reliability::Reliable,
1941 Some(_) => return NET_ERR_CHANNEL,
1942 };
1943 let on_failure = match cfg_in.on_failure.as_deref() {
1944 None | Some("best_effort") => InnerOnFailure::BestEffort,
1945 Some("fail_fast") => InnerOnFailure::FailFast,
1946 Some("collect") => InnerOnFailure::Collect,
1947 Some(_) => return NET_ERR_CHANNEL,
1948 };
1949 let max_inflight = cfg_in.max_inflight.unwrap_or(32) as usize;
1950 let publish_cfg = InnerPublishConfig {
1951 reliability,
1952 on_failure,
1953 max_inflight,
1954 };
1955 let publisher = ChannelPublisher::new(name, publish_cfg);
1956
1957 let bytes = if len == 0 {
1959 Bytes::new()
1960 } else if payload.is_null() {
1961 return NetError::NullPointer.into();
1962 } else if len > isize::MAX as usize {
1963 return NetError::InvalidJson.into();
1965 } else {
1966 Bytes::copy_from_slice(unsafe { std::slice::from_raw_parts(payload, len) })
1967 };
1968
1969 let node = h.inner.clone();
1970 match block_on(async move { node.publish(&publisher, bytes).await }) {
1971 Ok(report) => {
1972 let js = to_publish_report_json(report);
1973 write_json_out(&js, out_json, out_len)
1974 }
1975 Err(e) => adapter_err_to_channel_code(&e),
1976 }
1977}
1978
1979pub struct IdentityHandle {
1993 keypair: ManuallyDrop<Arc<EntityKeypair>>,
1994 cache: ManuallyDrop<Arc<TokenCache>>,
1995 guard: HandleGuard,
1996}
1997
1998fn alloc_bytes(src: &[u8], out_ptr: *mut *mut u8, out_len: *mut usize) -> c_int {
2012 if out_ptr.is_null() || out_len.is_null() {
2013 return NetError::NullPointer.into();
2014 }
2015 let len = src.len();
2016 if len == 0 {
2017 unsafe {
2018 *out_ptr = std::ptr::null_mut();
2019 *out_len = 0;
2020 }
2021 return 0;
2022 }
2023 let layout = match std::alloc::Layout::array::<u8>(len) {
2032 Ok(l) => l,
2033 Err(_) => return NET_ERR_IDENTITY,
2039 };
2040 let ptr = unsafe { std::alloc::alloc(layout) };
2041 if ptr.is_null() {
2042 std::alloc::handle_alloc_error(layout);
2043 }
2044 unsafe {
2045 std::ptr::copy_nonoverlapping(src.as_ptr(), ptr, len);
2046 *out_ptr = ptr;
2047 *out_len = len;
2048 }
2049 0
2050}
2051
2052#[unsafe(no_mangle)]
2067pub unsafe extern "C" fn net_free_bytes(ptr: *mut u8, len: usize) {
2068 if ptr.is_null() || len == 0 {
2069 return;
2070 }
2071 let layout = match std::alloc::Layout::array::<u8>(len) {
2077 Ok(l) => l,
2078 Err(_) => return,
2079 };
2080 unsafe {
2081 std::alloc::dealloc(ptr, layout);
2082 }
2083}
2084
2085fn entity_id_from_bytes(bytes: *const u8, len: usize) -> Option<EntityId> {
2086 if bytes.is_null() || len != 32 {
2087 return None;
2088 }
2089 let slice = unsafe { std::slice::from_raw_parts(bytes, 32) };
2090 let mut arr = [0u8; 32];
2091 arr.copy_from_slice(slice);
2092 Some(EntityId::from_bytes(arr))
2093}
2094
2095fn parse_scope_list(raw: &str) -> Option<TokenScope> {
2096 let values: Vec<String> = serde_json::from_str(raw).ok()?;
2100 let mut acc = TokenScope::NONE;
2101 for s in &values {
2102 acc = acc.union(match s.as_str() {
2103 "publish" => TokenScope::PUBLISH,
2104 "subscribe" => TokenScope::SUBSCRIBE,
2105 "admin" => TokenScope::ADMIN,
2106 "delegate" => TokenScope::DELEGATE,
2107 _ => return None,
2108 });
2109 }
2110 Some(acc)
2111}
2112
2113fn scope_to_strings(scope: TokenScope) -> Vec<&'static str> {
2114 let mut out = Vec::new();
2115 if scope.contains(TokenScope::PUBLISH) {
2116 out.push("publish");
2117 }
2118 if scope.contains(TokenScope::SUBSCRIBE) {
2119 out.push("subscribe");
2120 }
2121 if scope.contains(TokenScope::ADMIN) {
2122 out.push("admin");
2123 }
2124 if scope.contains(TokenScope::DELEGATE) {
2125 out.push("delegate");
2126 }
2127 out
2128}
2129
2130fn channel_name_to_hash(channel: &str) -> Option<ChannelHash> {
2131 InnerChannelName::new(channel).ok().map(|n| n.hash())
2132}
2133
2134#[unsafe(no_mangle)]
2137pub unsafe extern "C" fn net_identity_generate(out_handle: *mut *mut IdentityHandle) -> c_int {
2138 if out_handle.is_null() {
2139 return NetError::NullPointer.into();
2140 }
2141 let handle = Box::new(IdentityHandle {
2142 keypair: ManuallyDrop::new(Arc::new(EntityKeypair::generate())),
2143 cache: ManuallyDrop::new(Arc::new(TokenCache::new())),
2144 guard: HandleGuard::new(),
2145 });
2146 unsafe {
2147 *out_handle = Box::into_raw(handle);
2148 }
2149 0
2150}
2151
2152#[unsafe(no_mangle)]
2156pub unsafe extern "C" fn net_identity_from_seed(
2157 seed: *const u8,
2158 seed_len: usize,
2159 out_handle: *mut *mut IdentityHandle,
2160) -> c_int {
2161 if seed.is_null() || out_handle.is_null() {
2162 return NetError::NullPointer.into();
2163 }
2164 if seed_len != 32 {
2165 return NET_ERR_IDENTITY;
2166 }
2167 let mut arr = [0u8; 32];
2168 arr.copy_from_slice(unsafe { std::slice::from_raw_parts(seed, 32) });
2169 let handle = Box::new(IdentityHandle {
2170 keypair: ManuallyDrop::new(Arc::new(EntityKeypair::from_bytes(arr))),
2171 cache: ManuallyDrop::new(Arc::new(TokenCache::new())),
2172 guard: HandleGuard::new(),
2173 });
2174 unsafe {
2175 *out_handle = Box::into_raw(handle);
2176 }
2177 0
2178}
2179
2180#[unsafe(no_mangle)]
2181pub unsafe extern "C" fn net_identity_free(handle: *mut IdentityHandle) {
2182 if handle.is_null() {
2183 return;
2184 }
2185 let h: &IdentityHandle = unsafe { &*handle };
2187 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
2188 unsafe {
2190 let mh = &mut *handle;
2191 let kp = ManuallyDrop::take(&mut mh.keypair);
2192 let cache = ManuallyDrop::take(&mut mh.cache);
2193 drop(kp);
2194 drop(cache);
2195 }
2196 } else {
2197 tracing::warn!(
2198 "net_identity_free: in-flight ops did not drain within deadline; \
2199 leaking inner to avoid use-after-free"
2200 );
2201 }
2202}
2203
2204#[unsafe(no_mangle)]
2207pub unsafe extern "C" fn net_identity_to_seed(handle: *mut IdentityHandle, out: *mut u8) -> c_int {
2208 if handle.is_null() || out.is_null() {
2209 return NetError::NullPointer.into();
2210 }
2211 let h = unsafe { &*handle };
2212 let _op = match h.guard.try_enter() {
2213 Some(op) => op,
2214 None => return NetError::ShuttingDown.into(),
2215 };
2216 let seed = h.keypair.secret_bytes();
2217 unsafe {
2218 std::ptr::copy_nonoverlapping(seed.as_ptr(), out, 32);
2219 }
2220 0
2221}
2222
2223#[unsafe(no_mangle)]
2225pub unsafe extern "C" fn net_identity_entity_id(
2226 handle: *mut IdentityHandle,
2227 out: *mut u8,
2228) -> c_int {
2229 if handle.is_null() || out.is_null() {
2230 return NetError::NullPointer.into();
2231 }
2232 let h = unsafe { &*handle };
2233 let _op = match h.guard.try_enter() {
2234 Some(op) => op,
2235 None => return NetError::ShuttingDown.into(),
2236 };
2237 let id = h.keypair.entity_id().as_bytes();
2238 unsafe {
2239 std::ptr::copy_nonoverlapping(id.as_ptr(), out, 32);
2240 }
2241 0
2242}
2243
2244#[unsafe(no_mangle)]
2245pub unsafe extern "C" fn net_identity_node_id(handle: *mut IdentityHandle) -> u64 {
2246 if handle.is_null() {
2247 return 0;
2248 }
2249 let h = unsafe { &*handle };
2250 let _op = match h.guard.try_enter() {
2252 Some(op) => op,
2253 None => return 0,
2254 };
2255 h.keypair.node_id()
2256}
2257
2258#[unsafe(no_mangle)]
2259pub unsafe extern "C" fn net_identity_origin_hash(handle: *mut IdentityHandle) -> u64 {
2260 if handle.is_null() {
2261 return 0;
2262 }
2263 let h = unsafe { &*handle };
2264 let _op = match h.guard.try_enter() {
2266 Some(op) => op,
2267 None => return 0,
2268 };
2269 h.keypair.origin_hash()
2270}
2271
2272#[unsafe(no_mangle)]
2275pub unsafe extern "C" fn net_identity_sign(
2276 handle: *mut IdentityHandle,
2277 msg: *const u8,
2278 len: usize,
2279 out_sig: *mut u8,
2280) -> c_int {
2281 if handle.is_null() || out_sig.is_null() {
2282 return NetError::NullPointer.into();
2283 }
2284 if len > 0 && msg.is_null() {
2285 return NetError::NullPointer.into();
2286 }
2287 let h = unsafe { &*handle };
2288 let _op = match h.guard.try_enter() {
2289 Some(op) => op,
2290 None => return NetError::ShuttingDown.into(),
2291 };
2292 let slice = if len == 0 {
2293 &[][..]
2294 } else if len > isize::MAX as usize {
2295 return NetError::InvalidJson.into();
2297 } else {
2298 unsafe { std::slice::from_raw_parts(msg, len) }
2299 };
2300 let sig = h.keypair.sign(slice).to_bytes();
2301 unsafe {
2302 std::ptr::copy_nonoverlapping(sig.as_ptr(), out_sig, 64);
2303 }
2304 0
2305}
2306
2307#[unsafe(no_mangle)]
2310pub unsafe extern "C" fn net_identity_issue_token(
2311 signer: *mut IdentityHandle,
2312 subject: *const u8,
2313 subject_len: usize,
2314 scope_json: *const c_char,
2315 channel: *const c_char,
2316 ttl_seconds: u32,
2317 delegation_depth: u8,
2318 out_token: *mut *mut u8,
2319 out_token_len: *mut usize,
2320) -> c_int {
2321 if signer.is_null() || out_token.is_null() || out_token_len.is_null() {
2322 return NetError::NullPointer.into();
2323 }
2324 let Some(subject_id) = entity_id_from_bytes(subject, subject_len) else {
2325 return NET_ERR_IDENTITY;
2326 };
2327 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
2328 return NetError::InvalidUtf8.into();
2329 };
2330 let Some(scope) = parse_scope_list(&scope_s) else {
2331 return NET_ERR_IDENTITY;
2332 };
2333 let Some(channel_s) = (unsafe { c_str_to_string(channel) }) else {
2334 return NetError::InvalidUtf8.into();
2335 };
2336 let Some(channel_hash) = channel_name_to_hash(&channel_s) else {
2337 return NET_ERR_IDENTITY;
2338 };
2339 let h = unsafe { &*signer };
2340 let _op = match h.guard.try_enter() {
2344 Some(op) => op,
2345 None => return NetError::ShuttingDown.into(),
2346 };
2347 let token = match PermissionToken::try_issue(
2353 &h.keypair,
2354 subject_id,
2355 scope,
2356 channel_hash,
2357 u64::from(ttl_seconds),
2358 delegation_depth,
2359 ) {
2360 Ok(t) => t,
2361 Err(e) => return token_err_to_code(&e),
2362 };
2363 alloc_bytes(&token.to_bytes(), out_token, out_token_len)
2364}
2365
2366#[unsafe(no_mangle)]
2370pub unsafe extern "C" fn net_identity_install_token(
2371 handle: *mut IdentityHandle,
2372 token: *const u8,
2373 len: usize,
2374) -> c_int {
2375 if handle.is_null() || token.is_null() {
2376 return NetError::NullPointer.into();
2377 }
2378 if len > isize::MAX as usize {
2380 return NetError::InvalidJson.into();
2381 }
2382 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2383 let parsed = match PermissionToken::from_bytes(slice) {
2384 Ok(t) => t,
2385 Err(e) => return token_err_to_code(&e),
2386 };
2387 let h = unsafe { &*handle };
2388 let _op = match h.guard.try_enter() {
2389 Some(op) => op,
2390 None => return NetError::ShuttingDown.into(),
2391 };
2392 match h.cache.insert(parsed) {
2393 Ok(()) => 0,
2394 Err(e) => token_err_to_code(&e),
2395 }
2396}
2397
2398#[unsafe(no_mangle)]
2402pub unsafe extern "C" fn net_identity_lookup_token(
2403 handle: *mut IdentityHandle,
2404 subject: *const u8,
2405 subject_len: usize,
2406 channel: *const c_char,
2407 out_token: *mut *mut u8,
2408 out_token_len: *mut usize,
2409) -> c_int {
2410 if handle.is_null() || out_token.is_null() || out_token_len.is_null() {
2411 return NetError::NullPointer.into();
2412 }
2413 let Some(subject_id) = entity_id_from_bytes(subject, subject_len) else {
2414 return NET_ERR_IDENTITY;
2415 };
2416 let Some(channel_s) = (unsafe { c_str_to_string(channel) }) else {
2417 return NetError::InvalidUtf8.into();
2418 };
2419 let Some(channel_hash) = channel_name_to_hash(&channel_s) else {
2420 return NET_ERR_IDENTITY;
2421 };
2422 let h = unsafe { &*handle };
2423 let _op = match h.guard.try_enter() {
2424 Some(op) => op,
2425 None => return NetError::ShuttingDown.into(),
2426 };
2427 match h.cache.get(&subject_id, channel_hash) {
2428 Some(token) => alloc_bytes(&token.to_bytes(), out_token, out_token_len),
2429 None => {
2430 unsafe {
2431 *out_token = std::ptr::null_mut();
2432 *out_token_len = 0;
2433 }
2434 0
2435 }
2436 }
2437}
2438
2439#[unsafe(no_mangle)]
2440pub unsafe extern "C" fn net_identity_token_cache_len(handle: *mut IdentityHandle) -> u32 {
2441 if handle.is_null() {
2442 return 0;
2443 }
2444 let h = unsafe { &*handle };
2445 let _op = match h.guard.try_enter() {
2447 Some(op) => op,
2448 None => return 0,
2449 };
2450 h.cache.len() as u32
2451}
2452
2453#[derive(Serialize)]
2458struct ParsedTokenJson {
2459 issuer_hex: String,
2460 subject_hex: String,
2461 scope: Vec<&'static str>,
2462 channel_hash: ChannelHash,
2463 not_before: u64,
2464 not_after: u64,
2465 delegation_depth: u8,
2466 nonce: u64,
2467 signature_hex: String,
2468}
2469
2470#[unsafe(no_mangle)]
2475pub unsafe extern "C" fn net_parse_token(
2476 token: *const u8,
2477 len: usize,
2478 out_json: *mut *mut c_char,
2479 out_len: *mut usize,
2480) -> c_int {
2481 if token.is_null() || out_json.is_null() || out_len.is_null() {
2482 return NetError::NullPointer.into();
2483 }
2484 if len > isize::MAX as usize {
2486 return NetError::InvalidJson.into();
2487 }
2488 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2489 let parsed = match PermissionToken::from_bytes(slice) {
2490 Ok(t) => t,
2491 Err(e) => return token_err_to_code(&e),
2492 };
2493 let out = ParsedTokenJson {
2494 issuer_hex: hex::encode(parsed.issuer.as_bytes()),
2495 subject_hex: hex::encode(parsed.subject.as_bytes()),
2496 scope: scope_to_strings(parsed.scope),
2497 channel_hash: parsed.channel_hash,
2498 not_before: parsed.not_before,
2499 not_after: parsed.not_after,
2500 delegation_depth: parsed.delegation_depth,
2501 nonce: parsed.nonce,
2502 signature_hex: hex::encode(parsed.signature),
2503 };
2504 write_json_out(&out, out_json, out_len)
2505}
2506
2507#[unsafe(no_mangle)]
2511pub unsafe extern "C" fn net_verify_token(
2512 token: *const u8,
2513 len: usize,
2514 out_ok: *mut c_int,
2515) -> c_int {
2516 if token.is_null() || out_ok.is_null() {
2517 return NetError::NullPointer.into();
2518 }
2519 if len > isize::MAX as usize {
2521 return NetError::InvalidJson.into();
2522 }
2523 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2524 let parsed = match PermissionToken::from_bytes(slice) {
2525 Ok(t) => t,
2526 Err(e) => return token_err_to_code(&e),
2527 };
2528 unsafe {
2529 *out_ok = if parsed.verify().is_ok() { 1 } else { 0 };
2530 }
2531 0
2532}
2533
2534#[unsafe(no_mangle)]
2539pub unsafe extern "C" fn net_token_is_expired(
2540 token: *const u8,
2541 len: usize,
2542 out_expired: *mut c_int,
2543) -> c_int {
2544 if token.is_null() || out_expired.is_null() {
2545 return NetError::NullPointer.into();
2546 }
2547 if len > isize::MAX as usize {
2549 return NetError::InvalidJson.into();
2550 }
2551 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2552 let parsed = match PermissionToken::from_bytes(slice) {
2553 Ok(t) => t,
2554 Err(e) => return token_err_to_code(&e),
2555 };
2556 unsafe {
2557 *out_expired = if parsed.is_expired() { 1 } else { 0 };
2558 }
2559 0
2560}
2561
2562#[unsafe(no_mangle)]
2565pub unsafe extern "C" fn net_delegate_token(
2566 signer: *mut IdentityHandle,
2567 parent: *const u8,
2568 parent_len: usize,
2569 new_subject: *const u8,
2570 new_subject_len: usize,
2571 restricted_scope_json: *const c_char,
2572 out_token: *mut *mut u8,
2573 out_token_len: *mut usize,
2574) -> c_int {
2575 if signer.is_null()
2576 || parent.is_null()
2577 || new_subject.is_null()
2578 || restricted_scope_json.is_null()
2579 || out_token.is_null()
2580 || out_token_len.is_null()
2581 {
2582 return NetError::NullPointer.into();
2583 }
2584 if parent_len > isize::MAX as usize {
2586 return NetError::InvalidJson.into();
2587 }
2588 let parent_slice = unsafe { std::slice::from_raw_parts(parent, parent_len) };
2589 let parent_tok = match PermissionToken::from_bytes(parent_slice) {
2590 Ok(t) => t,
2591 Err(e) => return token_err_to_code(&e),
2592 };
2593 let Some(subject_id) = entity_id_from_bytes(new_subject, new_subject_len) else {
2594 return NET_ERR_IDENTITY;
2595 };
2596 let Some(scope_s) = (unsafe { c_str_to_string(restricted_scope_json) }) else {
2597 return NetError::InvalidUtf8.into();
2598 };
2599 let Some(scope) = parse_scope_list(&scope_s) else {
2600 return NET_ERR_IDENTITY;
2601 };
2602 let h = unsafe { &*signer };
2603 let _op = match h.guard.try_enter() {
2607 Some(op) => op,
2608 None => return NetError::ShuttingDown.into(),
2609 };
2610 match parent_tok.delegate(&h.keypair, subject_id, scope) {
2611 Ok(child) => alloc_bytes(&child.to_bytes(), out_token, out_token_len),
2612 Err(e) => token_err_to_code(&e),
2613 }
2614}
2615
2616#[unsafe(no_mangle)]
2621pub unsafe extern "C" fn net_channel_hash(channel: *const c_char, out_hash: *mut u64) -> c_int {
2622 if channel.is_null() || out_hash.is_null() {
2623 return NetError::NullPointer.into();
2624 }
2625 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
2626 return NetError::InvalidUtf8.into();
2627 };
2628 let Some(hash) = channel_name_to_hash(&s) else {
2629 return NET_ERR_IDENTITY;
2630 };
2631 unsafe {
2632 *out_hash = hash;
2633 }
2634 0
2635}
2636
2637use crate::adapter::net::behavior::capability::{
2644 AcceleratorInfo, AcceleratorType, CapabilityFilter, CapabilitySet, GpuInfo, GpuVendor,
2645 HardwareCapabilities, Modality, ModelCapability, ResourceLimits, SoftwareCapabilities,
2646 ToolCapability, TAG_SCOPE_REGION_PREFIX, TAG_SCOPE_SUBNET_LOCAL, TAG_SCOPE_TENANT_PREFIX,
2647};
2648
2649fn parse_gpu_vendor_cap(s: &str) -> GpuVendor {
2652 match s.to_ascii_lowercase().as_str() {
2653 "nvidia" => GpuVendor::Nvidia,
2654 "amd" => GpuVendor::Amd,
2655 "intel" => GpuVendor::Intel,
2656 "apple" => GpuVendor::Apple,
2657 "qualcomm" => GpuVendor::Qualcomm,
2658 _ => GpuVendor::Unknown,
2659 }
2660}
2661
2662fn gpu_vendor_to_string_cap(v: GpuVendor) -> &'static str {
2663 match v {
2664 GpuVendor::Nvidia => "nvidia",
2665 GpuVendor::Amd => "amd",
2666 GpuVendor::Intel => "intel",
2667 GpuVendor::Apple => "apple",
2668 GpuVendor::Qualcomm => "qualcomm",
2669 GpuVendor::Unknown => "unknown",
2670 }
2671}
2672
2673fn parse_modality_cap(s: &str) -> Option<Modality> {
2674 match s.to_ascii_lowercase().as_str() {
2675 "text" => Some(Modality::Text),
2676 "image" => Some(Modality::Image),
2677 "audio" => Some(Modality::Audio),
2678 "video" => Some(Modality::Video),
2679 "code" => Some(Modality::Code),
2680 "embedding" => Some(Modality::Embedding),
2681 "tool-use" | "tool_use" | "tooluse" => Some(Modality::ToolUse),
2682 _ => None,
2691 }
2692}
2693
2694fn parse_accelerator_type_cap(s: &str) -> AcceleratorType {
2695 match s.to_ascii_lowercase().as_str() {
2696 "tpu" => AcceleratorType::Tpu,
2697 "npu" => AcceleratorType::Npu,
2698 "fpga" => AcceleratorType::Fpga,
2699 "asic" => AcceleratorType::Asic,
2700 "dsp" => AcceleratorType::Dsp,
2701 _ => AcceleratorType::Unknown,
2702 }
2703}
2704
2705#[derive(Deserialize, Default)]
2708struct CapabilitySetJson {
2709 #[serde(default)]
2710 hardware: Option<HardwareJson>,
2711 #[serde(default)]
2712 software: Option<SoftwareJson>,
2713 #[serde(default)]
2714 models: Vec<ModelJson>,
2715 #[serde(default)]
2716 tools: Vec<ToolJson>,
2717 #[serde(default)]
2718 tags: Vec<String>,
2719 #[serde(default)]
2720 limits: Option<LimitsJson>,
2721}
2722
2723#[derive(Deserialize, Default)]
2724struct HardwareJson {
2725 cpu_cores: Option<u32>,
2726 cpu_threads: Option<u32>,
2727 memory_gb: Option<u32>,
2728 gpu: Option<GpuJson>,
2729 #[serde(default)]
2730 additional_gpus: Vec<GpuJson>,
2731 storage_gb: Option<u64>,
2732 network_gbps: Option<u32>,
2733 #[serde(default)]
2734 accelerators: Vec<AcceleratorJson>,
2735}
2736
2737#[derive(Deserialize)]
2738struct GpuJson {
2739 vendor: Option<String>,
2740 #[serde(default)]
2741 model: String,
2742 #[serde(default)]
2743 vram_gb: u32,
2744 compute_units: Option<u32>,
2745 tensor_cores: Option<u32>,
2746 fp16_tflops_x10: Option<u32>,
2747}
2748
2749#[derive(Deserialize)]
2750struct AcceleratorJson {
2751 #[serde(default)]
2752 kind: String,
2753 #[serde(default)]
2754 model: String,
2755 memory_gb: Option<u32>,
2756 tops_x10: Option<u32>,
2757}
2758
2759#[derive(Deserialize, Default)]
2760struct SoftwareJson {
2761 os: Option<String>,
2762 os_version: Option<String>,
2763 #[serde(default)]
2764 runtimes: Vec<Vec<String>>,
2765 #[serde(default)]
2766 frameworks: Vec<Vec<String>>,
2767 cuda_version: Option<String>,
2768 #[serde(default)]
2769 drivers: Vec<Vec<String>>,
2770}
2771
2772#[derive(Deserialize)]
2773struct ModelJson {
2774 #[serde(default)]
2775 model_id: String,
2776 #[serde(default)]
2777 family: String,
2778 parameters_b_x10: Option<u32>,
2779 context_length: Option<u32>,
2780 quantization: Option<String>,
2781 #[serde(default)]
2782 modalities: Vec<String>,
2783 tokens_per_sec: Option<u32>,
2784 loaded: Option<bool>,
2785}
2786
2787#[derive(Deserialize)]
2788struct ToolJson {
2789 #[serde(default)]
2790 tool_id: String,
2791 #[serde(default)]
2792 name: String,
2793 version: Option<String>,
2794 input_schema: Option<String>,
2795 output_schema: Option<String>,
2796 #[serde(default)]
2797 requires: Vec<String>,
2798 estimated_time_ms: Option<u32>,
2799 stateless: Option<bool>,
2800}
2801
2802#[derive(Deserialize, Default)]
2803struct LimitsJson {
2804 max_concurrent_requests: Option<u32>,
2805 max_tokens_per_request: Option<u32>,
2806 rate_limit_rpm: Option<u32>,
2807 max_batch_size: Option<u32>,
2808 max_input_bytes: Option<u32>,
2809 max_output_bytes: Option<u32>,
2810}
2811
2812#[derive(Deserialize, Default)]
2813struct CapabilityFilterJson {
2814 #[serde(default)]
2815 require_tags: Vec<String>,
2816 #[serde(default)]
2817 require_models: Vec<String>,
2818 #[serde(default)]
2819 require_tools: Vec<String>,
2820 min_memory_gb: Option<u32>,
2821 require_gpu: Option<bool>,
2822 gpu_vendor: Option<String>,
2823 min_vram_gb: Option<u32>,
2824 min_context_length: Option<u32>,
2825 #[serde(default)]
2826 require_modalities: Vec<String>,
2827}
2828
2829fn pair_vec(xs: Vec<Vec<String>>) -> Vec<(String, String)> {
2832 xs.into_iter()
2833 .filter_map(|mut p| {
2834 if p.len() >= 2 {
2835 Some((std::mem::take(&mut p[0]), std::mem::take(&mut p[1])))
2836 } else {
2837 None
2838 }
2839 })
2840 .collect()
2841}
2842
2843#[inline]
2849fn saturating_u16_cap(v: u32) -> u16 {
2850 v.min(u16::MAX as u32) as u16
2851}
2852
2853fn gpu_info_from_json(g: GpuJson) -> GpuInfo {
2854 let vendor = g
2855 .vendor
2856 .as_deref()
2857 .map(parse_gpu_vendor_cap)
2858 .unwrap_or(GpuVendor::Unknown);
2859 let mut info = GpuInfo::new(vendor, g.model, g.vram_gb);
2860 if let Some(cu) = g.compute_units {
2861 info = info.with_compute_units(saturating_u16_cap(cu));
2862 }
2863 if let Some(tc) = g.tensor_cores {
2864 info = info.with_tensor_cores(saturating_u16_cap(tc));
2865 }
2866 if let Some(tf) = g.fp16_tflops_x10 {
2867 let tf_capped = saturating_u16_cap(tf);
2881 info = info.with_fp16_tflops(tf_capped as f32 / 10.0);
2882 }
2883 info
2884}
2885
2886fn accelerator_from_json(a: AcceleratorJson) -> AcceleratorInfo {
2887 AcceleratorInfo {
2888 accel_type: parse_accelerator_type_cap(&a.kind),
2889 model: a.model,
2890 memory_gb: a.memory_gb.unwrap_or(0),
2891 tops_x10: a.tops_x10.map(saturating_u16_cap).unwrap_or(0),
2892 }
2893}
2894
2895fn hardware_from_json(h: HardwareJson) -> HardwareCapabilities {
2896 let mut hw = HardwareCapabilities::new();
2897 match (h.cpu_cores, h.cpu_threads) {
2898 (Some(c), Some(t)) => hw = hw.with_cpu(saturating_u16_cap(c), saturating_u16_cap(t)),
2899 (Some(c), None) => {
2900 let c16 = saturating_u16_cap(c);
2901 hw = hw.with_cpu(c16, c16);
2902 }
2903 _ => {}
2904 }
2905 if let Some(mb) = h.memory_gb {
2906 hw = hw.with_memory(mb);
2907 }
2908 if let Some(g) = h.gpu {
2909 hw = hw.with_gpu(gpu_info_from_json(g));
2910 }
2911 for g in h.additional_gpus {
2912 hw = hw.add_gpu(gpu_info_from_json(g));
2913 }
2914 if let Some(mb) = h.storage_gb {
2915 hw = hw.with_storage(mb);
2916 }
2917 if let Some(gbps) = h.network_gbps {
2918 hw = hw.with_network(gbps);
2919 }
2920 for a in h.accelerators {
2921 hw = hw.add_accelerator(accelerator_from_json(a));
2922 }
2923 hw
2924}
2925
2926fn software_from_json(s: SoftwareJson) -> SoftwareCapabilities {
2927 let mut sw = SoftwareCapabilities::new()
2928 .with_os(s.os.unwrap_or_default(), s.os_version.unwrap_or_default());
2929 for (k, v) in pair_vec(s.runtimes) {
2930 sw = sw.add_runtime(k, v);
2931 }
2932 for (k, v) in pair_vec(s.frameworks) {
2933 sw = sw.add_framework(k, v);
2934 }
2935 if let Some(c) = s.cuda_version {
2936 sw = sw.with_cuda(c);
2937 }
2938 sw.drivers = pair_vec(s.drivers);
2939 sw
2940}
2941
2942fn model_from_json(m: ModelJson) -> ModelCapability {
2943 let mut mc = ModelCapability::new(m.model_id, m.family);
2944 if let Some(p) = m.parameters_b_x10 {
2945 mc.parameters_b_x10 = p;
2946 }
2947 if let Some(c) = m.context_length {
2948 mc = mc.with_context_length(c);
2949 }
2950 if let Some(q) = m.quantization {
2951 mc = mc.with_quantization(q);
2952 }
2953 for modality in m.modalities {
2954 match parse_modality_cap(&modality) {
2955 Some(parsed) => mc = mc.add_modality(parsed),
2956 None => {
2957 tracing::warn!(
2958 modality = %modality,
2959 "announce_capabilities: unknown modality string (typo?), \
2960 skipping rather than the pre-fix silent fallback to Text — \
2961 advertising a Text capability the node doesn't actually \
2962 have produced wrong scheduling decisions on the receiver",
2963 );
2964 }
2965 }
2966 }
2967 if let Some(t) = m.tokens_per_sec {
2968 mc = mc.with_tokens_per_sec(t);
2969 }
2970 if let Some(l) = m.loaded {
2971 mc = mc.with_loaded(l);
2972 }
2973 mc
2974}
2975
2976fn tool_from_json(t: ToolJson) -> ToolCapability {
2977 let mut tc = ToolCapability::new(t.tool_id, t.name);
2978 if let Some(v) = t.version {
2979 tc = tc.with_version(v);
2980 }
2981 if let Some(s) = t.input_schema {
2982 tc = tc.with_input_schema(s);
2983 }
2984 if let Some(s) = t.output_schema {
2985 tc = tc.with_output_schema(s);
2986 }
2987 for r in t.requires {
2988 tc = tc.requires(r);
2989 }
2990 if let Some(ms) = t.estimated_time_ms {
2991 tc = tc.with_estimated_time(ms);
2992 }
2993 if let Some(st) = t.stateless {
2994 tc = tc.with_stateless(st);
2995 }
2996 tc
2997}
2998
2999fn limits_from_json(l: LimitsJson) -> ResourceLimits {
3000 let mut rl = ResourceLimits::new();
3001 if let Some(n) = l.max_concurrent_requests {
3002 rl = rl.with_max_concurrent(n);
3003 }
3004 if let Some(n) = l.max_tokens_per_request {
3005 rl = rl.with_max_tokens(n);
3006 }
3007 if let Some(n) = l.rate_limit_rpm {
3008 rl = rl.with_rate_limit(n);
3009 }
3010 if let Some(n) = l.max_batch_size {
3011 rl = rl.with_max_batch(n);
3012 }
3013 if let Some(n) = l.max_input_bytes {
3014 rl.max_input_bytes = n;
3015 }
3016 if let Some(n) = l.max_output_bytes {
3017 rl.max_output_bytes = n;
3018 }
3019 rl
3020}
3021
3022fn capability_set_from_json(caps: CapabilitySetJson) -> CapabilitySet {
3023 let mut cs = CapabilitySet::new();
3024 if let Some(h) = caps.hardware {
3025 cs = cs.with_hardware(hardware_from_json(h));
3026 }
3027 if let Some(s) = caps.software {
3028 cs = cs.with_software(software_from_json(s));
3029 }
3030 for m in caps.models {
3031 cs = cs.add_model(model_from_json(m));
3032 }
3033 for t in caps.tools {
3034 cs = cs.add_tool(tool_from_json(t));
3035 }
3036 for tag in caps.tags {
3044 if tag == TAG_SCOPE_SUBNET_LOCAL {
3045 cs = cs.with_subnet_local_scope();
3046 } else if let Some(id) = tag.strip_prefix(TAG_SCOPE_TENANT_PREFIX) {
3047 cs = cs.with_tenant_scope(id);
3048 } else if let Some(name) = tag.strip_prefix(TAG_SCOPE_REGION_PREFIX) {
3049 cs = cs.with_region_scope(name);
3050 } else {
3051 cs = cs.add_tag(tag);
3052 }
3053 }
3054 if let Some(l) = caps.limits {
3055 cs = cs.with_limits(limits_from_json(l));
3056 }
3057 cs
3058}
3059
3060fn capability_filter_from_json(f: CapabilityFilterJson) -> CapabilityFilter {
3061 let mut cf = CapabilityFilter::new();
3062 for t in f.require_tags {
3063 cf = cf.require_tag(t);
3064 }
3065 for m in f.require_models {
3066 cf = cf.require_model(m);
3067 }
3068 for t in f.require_tools {
3069 cf = cf.require_tool(t);
3070 }
3071 if let Some(mb) = f.min_memory_gb {
3072 cf = cf.with_min_memory(mb);
3073 }
3074 if f.require_gpu.unwrap_or(false) {
3075 cf = cf.require_gpu();
3076 }
3077 if let Some(v) = f.gpu_vendor {
3078 cf = cf.with_gpu_vendor(parse_gpu_vendor_cap(&v));
3079 }
3080 if let Some(mb) = f.min_vram_gb {
3081 cf = cf.with_min_vram(mb);
3082 }
3083 if let Some(n) = f.min_context_length {
3084 cf = cf.with_min_context(n);
3085 }
3086 for m in f.require_modalities {
3087 match parse_modality_cap(&m) {
3088 Some(parsed) => cf = cf.require_modality(parsed),
3089 None => {
3090 tracing::warn!(
3103 modality = %m,
3104 "find_nodes: unknown modality string in require_modalities \
3105 filter (typo?), dropping the constraint; the resulting \
3106 filter is too permissive — pre-fix it was silently \
3107 re-interpreted as `require Text`, which returned the \
3108 wrong nodes",
3109 );
3110 }
3111 }
3112 }
3113 cf
3114}
3115
3116pub(crate) const NET_ERR_CAPABILITY: c_int = -128;
3119
3120#[unsafe(no_mangle)]
3127pub unsafe extern "C" fn net_mesh_announce_capabilities(
3128 handle: *mut MeshNodeHandle,
3129 caps_json: *const c_char,
3130) -> c_int {
3131 if handle.is_null() || caps_json.is_null() {
3132 return NetError::NullPointer.into();
3133 }
3134 let h = unsafe { &*handle };
3135 let _op = match h.guard.try_enter() {
3136 Some(op) => op,
3137 None => return NetError::ShuttingDown.into(),
3138 };
3139 let Some(s) = (unsafe { c_str_to_string(caps_json) }) else {
3140 return NetError::InvalidUtf8.into();
3141 };
3142 let parsed: CapabilitySetJson = match serde_json::from_str(&s) {
3143 Ok(v) => v,
3144 Err(_) => return NetError::InvalidJson.into(),
3145 };
3146 let caps = capability_set_from_json(parsed);
3147 let node = h.inner.clone();
3148 match block_on(async move { node.announce_capabilities(caps).await }) {
3149 Ok(()) => 0,
3150 Err(_) => NET_ERR_CAPABILITY,
3151 }
3152}
3153
3154#[unsafe(no_mangle)]
3157pub unsafe extern "C" fn net_mesh_find_nodes(
3158 handle: *mut MeshNodeHandle,
3159 filter_json: *const c_char,
3160 out_json: *mut *mut c_char,
3161 out_len: *mut usize,
3162) -> c_int {
3163 if handle.is_null() || filter_json.is_null() || out_json.is_null() || out_len.is_null() {
3164 return NetError::NullPointer.into();
3165 }
3166 let h = unsafe { &*handle };
3167 let _op = match h.guard.try_enter() {
3168 Some(op) => op,
3169 None => return NetError::ShuttingDown.into(),
3170 };
3171 let Some(s) = (unsafe { c_str_to_string(filter_json) }) else {
3172 return NetError::InvalidUtf8.into();
3173 };
3174 let parsed: CapabilityFilterJson = match serde_json::from_str(&s) {
3175 Ok(v) => v,
3176 Err(_) => return NetError::InvalidJson.into(),
3177 };
3178 let filter = capability_filter_from_json(parsed);
3179 let ids = h.inner.find_nodes_by_filter(&filter);
3180 write_json_out(&ids, out_json, out_len)
3181}
3182
3183#[derive(serde::Deserialize)]
3200struct ScopeFilterJson {
3201 kind: String,
3202 #[serde(default)]
3203 tenant: Option<String>,
3204 #[serde(default)]
3205 tenants: Option<Vec<String>>,
3206 #[serde(default)]
3207 region: Option<String>,
3208 #[serde(default)]
3209 regions: Option<Vec<String>>,
3210}
3211
3212enum ScopeFilterOwned {
3218 Any,
3219 GlobalOnly,
3220 SameSubnet,
3221 Tenant(String),
3222 Tenants(Vec<String>),
3223 Region(String),
3224 Regions(Vec<String>),
3225}
3226
3227fn scope_filter_from_json(f: ScopeFilterJson) -> ScopeFilterOwned {
3228 match f.kind.as_str() {
3229 "any" => ScopeFilterOwned::Any,
3230 "global_only" | "globalOnly" => ScopeFilterOwned::GlobalOnly,
3231 "same_subnet" | "sameSubnet" => ScopeFilterOwned::SameSubnet,
3232 "tenant" => match f.tenant {
3233 Some(t) if !t.is_empty() => ScopeFilterOwned::Tenant(t),
3234 _ => ScopeFilterOwned::Any,
3235 },
3236 "tenants" => match f.tenants {
3237 Some(ts) => {
3243 let cleaned: Vec<String> = ts.into_iter().filter(|t| !t.is_empty()).collect();
3244 if cleaned.is_empty() {
3245 ScopeFilterOwned::Any
3246 } else {
3247 ScopeFilterOwned::Tenants(cleaned)
3248 }
3249 }
3250 None => ScopeFilterOwned::Any,
3251 },
3252 "region" => match f.region {
3253 Some(r) if !r.is_empty() => ScopeFilterOwned::Region(r),
3254 _ => ScopeFilterOwned::Any,
3255 },
3256 "regions" => match f.regions {
3257 Some(rs) => {
3259 let cleaned: Vec<String> = rs.into_iter().filter(|r| !r.is_empty()).collect();
3260 if cleaned.is_empty() {
3261 ScopeFilterOwned::Any
3262 } else {
3263 ScopeFilterOwned::Regions(cleaned)
3264 }
3265 }
3266 None => ScopeFilterOwned::Any,
3267 },
3268 _ => ScopeFilterOwned::Any,
3269 }
3270}
3271
3272fn with_scope_filter<R>(
3277 owned: &ScopeFilterOwned,
3278 f: impl FnOnce(&crate::adapter::net::behavior::capability::ScopeFilter<'_>) -> R,
3279) -> R {
3280 use crate::adapter::net::behavior::capability::ScopeFilter as F;
3281 match owned {
3282 ScopeFilterOwned::Any => f(&F::Any),
3283 ScopeFilterOwned::GlobalOnly => f(&F::GlobalOnly),
3284 ScopeFilterOwned::SameSubnet => f(&F::SameSubnet),
3285 ScopeFilterOwned::Tenant(t) => f(&F::Tenant(t.as_str())),
3286 ScopeFilterOwned::Tenants(ts) => {
3287 let refs: Vec<&str> = ts.iter().map(|s| s.as_str()).collect();
3288 f(&F::Tenants(refs.as_slice()))
3289 }
3290 ScopeFilterOwned::Region(r) => f(&F::Region(r.as_str())),
3291 ScopeFilterOwned::Regions(rs) => {
3292 let refs: Vec<&str> = rs.iter().map(|s| s.as_str()).collect();
3293 f(&F::Regions(refs.as_slice()))
3294 }
3295 }
3296}
3297
3298#[unsafe(no_mangle)]
3321pub unsafe extern "C" fn net_mesh_find_nodes_scoped(
3322 handle: *mut MeshNodeHandle,
3323 filter_json: *const c_char,
3324 scope_json: *const c_char,
3325 out_json: *mut *mut c_char,
3326 out_len: *mut usize,
3327) -> c_int {
3328 if handle.is_null()
3329 || filter_json.is_null()
3330 || scope_json.is_null()
3331 || out_json.is_null()
3332 || out_len.is_null()
3333 {
3334 return NetError::NullPointer.into();
3335 }
3336 let h = unsafe { &*handle };
3337 let _op = match h.guard.try_enter() {
3338 Some(op) => op,
3339 None => return NetError::ShuttingDown.into(),
3340 };
3341 let Some(filter_s) = (unsafe { c_str_to_string(filter_json) }) else {
3342 return NetError::InvalidUtf8.into();
3343 };
3344 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
3345 return NetError::InvalidUtf8.into();
3346 };
3347 let parsed_filter: CapabilityFilterJson = match serde_json::from_str(&filter_s) {
3348 Ok(v) => v,
3349 Err(_) => return NetError::InvalidJson.into(),
3350 };
3351 let parsed_scope: ScopeFilterJson = match serde_json::from_str(&scope_s) {
3352 Ok(v) => v,
3353 Err(_) => return NetError::InvalidJson.into(),
3354 };
3355 let filter = capability_filter_from_json(parsed_filter);
3356 let owned = scope_filter_from_json(parsed_scope);
3357 let ids = with_scope_filter(&owned, |sf| {
3358 h.inner.find_nodes_by_filter_scoped(&filter, sf)
3359 });
3360 write_json_out(&ids, out_json, out_len)
3361}
3362
3363#[derive(serde::Deserialize)]
3377struct CapabilityRequirementJson {
3378 #[serde(default)]
3379 filter: CapabilityFilterJson,
3380 #[serde(default)]
3381 prefer_more_memory: f32,
3382 #[serde(default)]
3383 prefer_more_vram: f32,
3384 #[serde(default)]
3385 prefer_faster_inference: f32,
3386 #[serde(default)]
3387 prefer_loaded_models: f32,
3388}
3389
3390fn capability_requirement_from_json(
3391 j: CapabilityRequirementJson,
3392) -> crate::adapter::net::behavior::capability::CapabilityRequirement {
3393 crate::adapter::net::behavior::capability::CapabilityRequirement::from_filter(
3394 capability_filter_from_json(j.filter),
3395 )
3396 .prefer_memory(j.prefer_more_memory)
3397 .prefer_vram(j.prefer_more_vram)
3398 .prefer_speed(j.prefer_faster_inference)
3399 .prefer_loaded(j.prefer_loaded_models)
3400}
3401
3402#[unsafe(no_mangle)]
3412pub unsafe extern "C" fn net_mesh_find_best_node(
3413 handle: *mut MeshNodeHandle,
3414 requirement_json: *const c_char,
3415 out_node_id: *mut u64,
3416 out_has_match: *mut c_int,
3417) -> c_int {
3418 if handle.is_null()
3419 || requirement_json.is_null()
3420 || out_node_id.is_null()
3421 || out_has_match.is_null()
3422 {
3423 return NetError::NullPointer.into();
3424 }
3425 let h = unsafe { &*handle };
3426 let _op = match h.guard.try_enter() {
3427 Some(op) => op,
3428 None => return NetError::ShuttingDown.into(),
3429 };
3430 let Some(s) = (unsafe { c_str_to_string(requirement_json) }) else {
3431 return NetError::InvalidUtf8.into();
3432 };
3433 let parsed: CapabilityRequirementJson = match serde_json::from_str(&s) {
3434 Ok(v) => v,
3435 Err(_) => return NetError::InvalidJson.into(),
3436 };
3437 let req = capability_requirement_from_json(parsed);
3438 match h.inner.find_best_node(&req) {
3439 Some(node_id) => unsafe {
3440 *out_node_id = node_id;
3441 *out_has_match = 1;
3442 },
3443 None => unsafe {
3444 *out_has_match = 0;
3445 },
3446 }
3447 0
3448}
3449
3450#[unsafe(no_mangle)]
3459pub unsafe extern "C" fn net_mesh_find_best_node_scoped(
3460 handle: *mut MeshNodeHandle,
3461 requirement_json: *const c_char,
3462 scope_json: *const c_char,
3463 out_node_id: *mut u64,
3464 out_has_match: *mut c_int,
3465) -> c_int {
3466 if handle.is_null()
3467 || requirement_json.is_null()
3468 || scope_json.is_null()
3469 || out_node_id.is_null()
3470 || out_has_match.is_null()
3471 {
3472 return NetError::NullPointer.into();
3473 }
3474 let h = unsafe { &*handle };
3475 let _op = match h.guard.try_enter() {
3476 Some(op) => op,
3477 None => return NetError::ShuttingDown.into(),
3478 };
3479 let Some(req_s) = (unsafe { c_str_to_string(requirement_json) }) else {
3480 return NetError::InvalidUtf8.into();
3481 };
3482 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
3483 return NetError::InvalidUtf8.into();
3484 };
3485 let parsed_req: CapabilityRequirementJson = match serde_json::from_str(&req_s) {
3486 Ok(v) => v,
3487 Err(_) => return NetError::InvalidJson.into(),
3488 };
3489 let parsed_scope: ScopeFilterJson = match serde_json::from_str(&scope_s) {
3490 Ok(v) => v,
3491 Err(_) => return NetError::InvalidJson.into(),
3492 };
3493 let req = capability_requirement_from_json(parsed_req);
3494 let owned = scope_filter_from_json(parsed_scope);
3495 let result = with_scope_filter(&owned, |sf| h.inner.find_best_node_scoped(&req, sf));
3496 match result {
3497 Some(node_id) => unsafe {
3498 *out_node_id = node_id;
3499 *out_has_match = 1;
3500 },
3501 None => unsafe {
3502 *out_has_match = 0;
3503 },
3504 }
3505 0
3506}
3507
3508#[unsafe(no_mangle)]
3510pub unsafe extern "C" fn net_normalize_gpu_vendor(
3511 raw: *const c_char,
3512 out_json: *mut *mut c_char,
3513 out_len: *mut usize,
3514) -> c_int {
3515 if raw.is_null() || out_json.is_null() || out_len.is_null() {
3516 return NetError::NullPointer.into();
3517 }
3518 let Some(s) = (unsafe { c_str_to_string(raw) }) else {
3519 return NetError::InvalidUtf8.into();
3520 };
3521 let canonical = gpu_vendor_to_string_cap(parse_gpu_vendor_cap(&s));
3522 write_string_out(canonical.to_string(), out_json, out_len)
3523}
3524
3525#[cfg(test)]
3526mod tests {
3527 use super::*;
3528
3529 #[test]
3541 fn saturating_u16_cap_clamps_at_u16_max() {
3542 assert_eq!(saturating_u16_cap(0), 0);
3543 assert_eq!(saturating_u16_cap(42), 42);
3544 assert_eq!(saturating_u16_cap(u16::MAX as u32), u16::MAX);
3545 assert_eq!(saturating_u16_cap(u16::MAX as u32 + 1), u16::MAX);
3546 assert_eq!(saturating_u16_cap(u32::MAX), u16::MAX);
3547 }
3548
3549 #[test]
3558 fn parse_modality_cap_returns_none_on_unknown_strings() {
3559 for (s, expected) in [
3561 ("text", Modality::Text),
3562 ("Text", Modality::Text),
3563 ("TEXT", Modality::Text),
3564 ("image", Modality::Image),
3565 ("audio", Modality::Audio),
3566 ("video", Modality::Video),
3567 ("code", Modality::Code),
3568 ("embedding", Modality::Embedding),
3569 ("tool-use", Modality::ToolUse),
3570 ("tool_use", Modality::ToolUse),
3571 ("tooluse", Modality::ToolUse),
3572 ] {
3573 assert_eq!(
3574 parse_modality_cap(s),
3575 Some(expected),
3576 "known modality `{s}` must parse",
3577 );
3578 }
3579
3580 for s in ["audoi", "imageX", "vidoe", "embeding", "garbage", ""] {
3582 assert_eq!(
3583 parse_modality_cap(s),
3584 None,
3585 "unknown modality `{s}` must return None — pre-fix this \
3586 fell back to Modality::Text, advertising a capability \
3587 the node didn't actually have",
3588 );
3589 }
3590 }
3591
3592 #[test]
3602 fn gpu_info_from_json_saturates_fp16_tflops_to_u16_max() {
3603 let g = GpuJson {
3606 vendor: None,
3607 model: "test".to_string(),
3608 vram_gb: 0,
3609 compute_units: None,
3610 tensor_cores: None,
3611 fp16_tflops_x10: Some(1_000_000_000u32),
3612 };
3613 let info = gpu_info_from_json(g);
3614 assert_eq!(
3618 info.fp16_tflops_x10,
3619 u16::MAX as u32,
3620 "fp16_tflops_x10 must saturate at u16::MAX (65535) instead of \
3621 losing precision through the f32 round-trip; got {}",
3622 info.fp16_tflops_x10,
3623 );
3624
3625 let g_small = GpuJson {
3627 vendor: None,
3628 model: "test".to_string(),
3629 vram_gb: 0,
3630 compute_units: None,
3631 tensor_cores: None,
3632 fp16_tflops_x10: Some(425), };
3634 let info_small = gpu_info_from_json(g_small);
3635 assert_eq!(
3636 info_small.fp16_tflops_x10, 425,
3637 "small fp16_tflops_x10 must round-trip exactly"
3638 );
3639 }
3640
3641 #[test]
3654 fn alloc_bytes_round_trip_across_sizes() {
3655 for size in [0usize, 1, 15, 16, 17, 32, 64, 1024, 8192] {
3656 let src: Vec<u8> = (0..size).map(|i| (i as u8).wrapping_mul(37)).collect();
3657 let mut ptr: *mut u8 = std::ptr::null_mut();
3658 let mut len: usize = 0;
3659 let rc = alloc_bytes(&src, &mut ptr as *mut _, &mut len as *mut _);
3660 assert_eq!(rc, 0);
3661 assert_eq!(len, size);
3662 if size == 0 {
3663 assert!(ptr.is_null());
3664 } else {
3665 assert!(!ptr.is_null());
3666 let observed = unsafe { std::slice::from_raw_parts(ptr, len) };
3667 assert_eq!(observed, &src[..]);
3668 }
3669 unsafe { net_free_bytes(ptr, len) };
3672 }
3673 }
3674
3675 #[test]
3676 fn net_free_bytes_null_and_zero_len_are_noops() {
3677 unsafe { net_free_bytes(std::ptr::null_mut(), 0) };
3679 unsafe { net_free_bytes(std::ptr::null_mut(), 42) };
3680 let mut sentinel: u8 = 0;
3683 unsafe { net_free_bytes(&mut sentinel as *mut u8, 0) };
3684 }
3685
3686 #[test]
3698 fn net_free_bytes_does_not_panic_on_oversized_len() {
3699 let mut sentinel: u8 = 0;
3707 let ptr = &mut sentinel as *mut u8;
3708 unsafe { net_free_bytes(ptr, usize::MAX) };
3711 assert_eq!(sentinel, 0, "sentinel must not have been written through");
3714 }
3715
3716 #[test]
3725 fn net_mesh_shutdown_runs_even_with_outstanding_arc_refs() {
3726 let cfg = serde_json::json!({
3727 "bind_addr": "127.0.0.1:0",
3728 "psk_hex": "0".repeat(64),
3729 });
3730 let cfg_c = CString::new(cfg.to_string()).unwrap();
3731 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
3732 let rc = unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) };
3733 assert_eq!(rc, 0, "net_mesh_new failed: {rc}");
3734 assert!(!out.is_null());
3735
3736 let inner_clone = {
3739 let h = unsafe { &*out };
3740 Arc::clone(&h.inner)
3741 };
3742 assert!(Arc::strong_count(&inner_clone) >= 2);
3743 assert!(!inner_clone.is_shutdown());
3744
3745 let rc = unsafe { net_mesh_shutdown(out) };
3746 assert_eq!(rc, 0, "net_mesh_shutdown returned {rc}");
3747 assert!(
3748 inner_clone.is_shutdown(),
3749 "shutdown flag must be set even when extra Arc refs are outstanding"
3750 );
3751
3752 drop(inner_clone);
3753 unsafe { net_mesh_free(out) };
3757 }
3758
3759 #[test]
3771 fn handles_match_rejects_stream_node_mismatch() {
3772 fn make_node_handle() -> *mut MeshNodeHandle {
3773 let cfg = serde_json::json!({
3774 "bind_addr": "127.0.0.1:0",
3775 "psk_hex": "0".repeat(64),
3776 });
3777 let cfg_c = CString::new(cfg.to_string()).unwrap();
3778 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
3779 let rc = unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) };
3780 assert_eq!(rc, 0);
3781 assert!(!out.is_null());
3782 out
3783 }
3784
3785 let nh_a = make_node_handle();
3786 let nh_b = make_node_handle();
3787
3788 let sh_a = {
3796 let h = unsafe { &*nh_a };
3797 let node_clone: Arc<MeshNode> = Arc::clone(&h.inner);
3798 MeshStreamHandle {
3799 stream: ManuallyDrop::new(CoreStream {
3800 peer_node_id: 0xDEAD,
3801 stream_id: 1,
3802 epoch: 0,
3803 config: StreamConfig::new(),
3804 }),
3805 _node: ManuallyDrop::new(node_clone),
3806 guard: HandleGuard::new(),
3807 }
3808 };
3809
3810 assert!(
3812 handles_match(&sh_a, unsafe { &*nh_a }),
3813 "stream from node_a + node_a handle must match"
3814 );
3815 assert!(
3817 !handles_match(&sh_a, unsafe { &*nh_b }),
3818 "stream from node_a + node_b handle must be rejected (#19)"
3819 );
3820
3821 unsafe {
3830 let mut sh_a = sh_a;
3831 let _ = ManuallyDrop::take(&mut sh_a.stream);
3832 let _ = ManuallyDrop::take(&mut sh_a._node);
3833 }
3834 unsafe { net_mesh_free(nh_a) };
3835 unsafe { net_mesh_free(nh_b) };
3836 }
3837
3838 #[test]
3845 fn net_mesh_free_is_idempotent() {
3846 let cfg = serde_json::json!({
3847 "bind_addr": "127.0.0.1:0",
3848 "psk_hex": "0".repeat(64),
3849 });
3850 let cfg_c = CString::new(cfg.to_string()).unwrap();
3851 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
3852 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
3853 assert!(!nh.is_null());
3854
3855 unsafe { net_mesh_free(nh) };
3856 unsafe { net_mesh_free(nh) };
3860 }
3861
3862 #[test]
3866 fn net_identity_free_is_idempotent() {
3867 let mut h: *mut IdentityHandle = std::ptr::null_mut();
3868 assert_eq!(unsafe { net_identity_generate(&mut h) }, 0);
3869 assert!(!h.is_null());
3870
3871 unsafe { net_identity_free(h) };
3872 unsafe { net_identity_free(h) };
3874 }
3875
3876 #[test]
3888 fn net_mesh_free_waits_for_inflight_op() {
3889 use std::sync::atomic::{AtomicBool, Ordering};
3890 use std::time::{Duration, Instant};
3891
3892 let cfg = serde_json::json!({
3893 "bind_addr": "127.0.0.1:0",
3894 "psk_hex": "0".repeat(64),
3895 });
3896 let cfg_c = CString::new(cfg.to_string()).unwrap();
3897 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
3898 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
3899 assert!(!nh.is_null());
3900
3901 let nh_addr = nh as usize;
3904 let started = Arc::new(AtomicBool::new(false));
3905 let release = Arc::new(AtomicBool::new(false));
3906 let started_w = started.clone();
3907 let release_w = release.clone();
3908
3909 let worker = std::thread::spawn(move || {
3910 let h = unsafe { &*(nh_addr as *mut MeshNodeHandle) };
3911 let op = h.guard.try_enter().expect("entry must succeed pre-free");
3915 started_w.store(true, Ordering::SeqCst);
3916 while !release_w.load(Ordering::SeqCst) {
3917 std::thread::sleep(Duration::from_millis(1));
3918 }
3919 drop(op);
3920 });
3921
3922 while !started.load(Ordering::SeqCst) {
3924 std::thread::yield_now();
3925 }
3926
3927 let release_clone = release.clone();
3930 std::thread::spawn(move || {
3931 std::thread::sleep(Duration::from_millis(50));
3932 release_clone.store(true, Ordering::SeqCst);
3933 });
3934
3935 let t0 = Instant::now();
3937 unsafe { net_mesh_free(nh) };
3938 let elapsed = t0.elapsed();
3939 assert!(
3940 elapsed >= Duration::from_millis(40),
3941 "net_mesh_free returned in {:?} — pre-fix it would have proceeded \
3942 immediately and the worker's subsequent op would UAF",
3943 elapsed,
3944 );
3945 worker.join().unwrap();
3946 }
3947
3948 #[test]
3955 fn net_mesh_stream_stats_returns_shutting_down_after_free() {
3956 let cfg = serde_json::json!({
3957 "bind_addr": "127.0.0.1:0",
3958 "psk_hex": "0".repeat(64),
3959 });
3960 let cfg_c = CString::new(cfg.to_string()).unwrap();
3961 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
3962 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
3963 assert!(!nh.is_null());
3964
3965 unsafe { net_mesh_free(nh) };
3968
3969 let mut out_json: *mut c_char = std::ptr::null_mut();
3970 let mut out_len: usize = 0;
3971 let rc = unsafe { net_mesh_stream_stats(nh, 0xDEAD, 1, &mut out_json, &mut out_len) };
3972 assert_eq!(
3973 rc,
3974 NetError::ShuttingDown as c_int,
3975 "post-free stream_stats must surface ShuttingDown (got {rc})",
3976 );
3977 assert!(
3978 out_json.is_null(),
3979 "no payload may be written after the guard fires",
3980 );
3981 }
3982
3983 #[test]
3988 fn net_identity_issue_token_returns_shutting_down_after_free() {
3989 let mut signer: *mut IdentityHandle = std::ptr::null_mut();
3990 assert_eq!(unsafe { net_identity_generate(&mut signer) }, 0);
3991 assert!(!signer.is_null());
3992 unsafe { net_identity_free(signer) };
3993
3994 let subject = [0u8; 32];
3997 let scope = CString::new("[\"publish\"]").unwrap();
3998 let channel = CString::new("test-channel").unwrap();
3999 let mut out_token: *mut u8 = std::ptr::null_mut();
4000 let mut out_token_len: usize = 0;
4001 let rc = unsafe {
4002 net_identity_issue_token(
4003 signer,
4004 subject.as_ptr(),
4005 subject.len(),
4006 scope.as_ptr(),
4007 channel.as_ptr(),
4008 60,
4009 0,
4010 &mut out_token,
4011 &mut out_token_len,
4012 )
4013 };
4014 assert_eq!(
4015 rc,
4016 NetError::ShuttingDown as c_int,
4017 "post-free issue_token must surface ShuttingDown (got {rc})",
4018 );
4019 assert!(out_token.is_null(), "no token bytes may be allocated");
4020 }
4021
4022 #[test]
4028 fn net_delegate_token_returns_shutting_down_after_free() {
4029 let mut signer: *mut IdentityHandle = std::ptr::null_mut();
4030 assert_eq!(unsafe { net_identity_generate(&mut signer) }, 0);
4031 assert!(!signer.is_null());
4032
4033 let subject = [0u8; 32];
4035 let scope = CString::new("[\"publish\",\"delegate\"]").unwrap();
4036 let channel = CString::new("test-channel").unwrap();
4037 let mut parent_bytes: *mut u8 = std::ptr::null_mut();
4038 let mut parent_len: usize = 0;
4039 assert_eq!(
4040 unsafe {
4041 net_identity_issue_token(
4042 signer,
4043 subject.as_ptr(),
4044 subject.len(),
4045 scope.as_ptr(),
4046 channel.as_ptr(),
4047 60,
4048 1,
4049 &mut parent_bytes,
4050 &mut parent_len,
4051 )
4052 },
4053 0,
4054 );
4055 assert!(!parent_bytes.is_null());
4056
4057 unsafe { net_identity_free(signer) };
4059
4060 let new_subject = [1u8; 32];
4061 let restricted = CString::new("[\"publish\"]").unwrap();
4062 let mut child_bytes: *mut u8 = std::ptr::null_mut();
4063 let mut child_len: usize = 0;
4064 let rc = unsafe {
4065 net_delegate_token(
4066 signer,
4067 parent_bytes,
4068 parent_len,
4069 new_subject.as_ptr(),
4070 new_subject.len(),
4071 restricted.as_ptr(),
4072 &mut child_bytes,
4073 &mut child_len,
4074 )
4075 };
4076 assert_eq!(
4077 rc,
4078 NetError::ShuttingDown as c_int,
4079 "post-free delegate_token must surface ShuttingDown (got {rc})",
4080 );
4081 assert!(child_bytes.is_null(), "no child token may be allocated");
4082
4083 unsafe { net_free_bytes(parent_bytes, parent_len) };
4085 }
4086
4087 #[test]
4088 fn hardware_from_json_saturates_overflow_cpu_fields() {
4089 let h = HardwareJson {
4092 cpu_cores: Some(70_000),
4093 cpu_threads: Some(200_000),
4094 memory_gb: None,
4095 gpu: None,
4096 additional_gpus: Vec::new(),
4097 storage_gb: None,
4098 network_gbps: None,
4099 accelerators: Vec::new(),
4100 };
4101 let hw = hardware_from_json(h);
4102 assert_eq!(hw.cpu_cores, u16::MAX);
4103 assert_eq!(hw.cpu_threads, u16::MAX);
4104 }
4105
4106 #[test]
4113 fn token_entry_points_reject_oversize_len() {
4114 let invalid_json: c_int = NetError::InvalidJson.into();
4115 let mut sentinel: u8 = 0;
4116 let token = &mut sentinel as *mut u8 as *const u8;
4117
4118 let mut out_json: *mut c_char = std::ptr::null_mut();
4119 let mut out_len: usize = 0;
4120 assert_eq!(
4121 unsafe { net_parse_token(token, usize::MAX, &mut out_json, &mut out_len) },
4122 invalid_json,
4123 );
4124 assert!(out_json.is_null());
4125
4126 let mut out_ok: c_int = -42;
4127 assert_eq!(
4128 unsafe { net_verify_token(token, usize::MAX, &mut out_ok) },
4129 invalid_json,
4130 );
4131
4132 let mut out_expired: c_int = -42;
4133 assert_eq!(
4134 unsafe { net_token_is_expired(token, usize::MAX, &mut out_expired) },
4135 invalid_json,
4136 );
4137
4138 assert_eq!(
4139 sentinel, 0,
4140 "sentinel must not be touched: the length guard fires before any deref"
4141 );
4142 }
4143}
4144
4145#[cfg(all(test, not(feature = "nat-traversal")))]
4146mod nat_traversal_stub_tests {
4147 use super::*;
4164 use std::ptr;
4165
4166 #[test]
4167 fn nat_type_stub_returns_unsupported() {
4168 let mut out_str: *mut c_char = ptr::null_mut();
4169 let mut out_len: usize = 0;
4170 let code = net_mesh_nat_type(ptr::null_mut(), &mut out_str, &mut out_len);
4171 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4172 }
4173
4174 #[test]
4175 fn reflex_addr_stub_returns_unsupported() {
4176 let mut out_str: *mut c_char = ptr::null_mut();
4177 let mut out_len: usize = 0;
4178 let code = net_mesh_reflex_addr(ptr::null_mut(), &mut out_str, &mut out_len);
4179 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4180 }
4181
4182 #[test]
4183 fn peer_nat_type_stub_returns_unsupported() {
4184 let mut out_str: *mut c_char = ptr::null_mut();
4185 let mut out_len: usize = 0;
4186 let code = net_mesh_peer_nat_type(ptr::null_mut(), 0, &mut out_str, &mut out_len);
4187 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4188 }
4189
4190 #[test]
4191 fn probe_reflex_stub_returns_unsupported() {
4192 let mut out_str: *mut c_char = ptr::null_mut();
4193 let mut out_len: usize = 0;
4194 let code = net_mesh_probe_reflex(ptr::null_mut(), 0, &mut out_str, &mut out_len);
4195 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4196 }
4197
4198 #[test]
4199 fn reclassify_nat_stub_returns_unsupported() {
4200 let code = net_mesh_reclassify_nat(ptr::null_mut());
4201 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4202 }
4203
4204 #[test]
4205 fn traversal_stats_stub_returns_unsupported() {
4206 let mut a: u64 = 0;
4207 let mut b: u64 = 0;
4208 let mut c: u64 = 0;
4209 let code = net_mesh_traversal_stats(ptr::null_mut(), &mut a, &mut b, &mut c);
4210 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4211 }
4212
4213 #[test]
4214 fn connect_direct_stub_returns_unsupported() {
4215 let code = net_mesh_connect_direct(ptr::null_mut(), 0, ptr::null(), 0);
4216 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4217 }
4218
4219 #[test]
4220 fn set_reflex_override_stub_returns_unsupported() {
4221 let code = net_mesh_set_reflex_override(ptr::null_mut(), ptr::null());
4222 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4223 }
4224
4225 #[test]
4226 fn clear_reflex_override_stub_returns_unsupported() {
4227 let code = net_mesh_clear_reflex_override(ptr::null_mut());
4228 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4229 }
4230
4231 #[test]
4237 fn unsupported_code_is_stable() {
4238 assert_eq!(NET_ERR_TRAVERSAL_UNSUPPORTED, -137);
4239 }
4240
4241 #[test]
4245 fn capability_set_from_go_marshal_preserves_gpu_vendor() {
4246 let json = r#"{"hardware":{"cpu_cores":16,"memory_gb":64,"gpu":{"vendor":"nvidia","model":"h100","vram_gb":80}},"tags":["gpu"]}"#;
4247 let parsed: CapabilitySetJson = serde_json::from_str(json).expect("JSON should parse");
4248 let caps = capability_set_from_json(parsed);
4249 let views = caps.views();
4253 assert_eq!(
4254 views.hardware().gpu_vendor(),
4255 Some(super::GpuVendor::Nvidia),
4256 "vendor lost in conversion"
4257 );
4258 assert_eq!(views.hardware().memory_gb, 64);
4259 assert_eq!(views.hardware().total_vram_gb(), 80);
4260 assert!(caps.has_tag("gpu"));
4261 }
4262
4263 #[test]
4272 fn collect_payloads_rejects_null_entry_with_nonzero_length() {
4273 let buf_a = b"hello".as_slice();
4274 let buf_b = b"world".as_slice();
4275 let ptrs: [*const u8; 3] = [buf_a.as_ptr(), std::ptr::null(), buf_b.as_ptr()];
4276 let lens: [usize; 3] = [buf_a.len(), 4, buf_b.len()];
4277
4278 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 3) };
4279 assert!(
4280 result.is_none(),
4281 "null entry with non-zero length must reject the whole batch"
4282 );
4283 }
4284
4285 #[test]
4286 fn collect_payloads_allows_null_entry_with_zero_length() {
4287 let buf_a = b"hello".as_slice();
4288 let ptrs: [*const u8; 2] = [buf_a.as_ptr(), std::ptr::null()];
4289 let lens: [usize; 2] = [buf_a.len(), 0];
4290
4291 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 2) }
4292 .expect("zero-length null is treated as empty payload");
4293 assert_eq!(result.len(), 2);
4294 assert_eq!(&result[0][..], b"hello");
4295 assert!(result[1].is_empty());
4296 }
4297
4298 #[test]
4299 fn collect_payloads_happy_path() {
4300 let buf_a = b"abc".as_slice();
4301 let buf_b = b"defg".as_slice();
4302 let ptrs: [*const u8; 2] = [buf_a.as_ptr(), buf_b.as_ptr()];
4303 let lens: [usize; 2] = [buf_a.len(), buf_b.len()];
4304
4305 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 2) }
4306 .expect("non-null entries should succeed");
4307 assert_eq!(result.len(), 2);
4308 assert_eq!(&result[0][..], b"abc");
4309 assert_eq!(&result[1][..], b"defg");
4310 }
4311}