1use std::ffi::{c_char, c_int, CStr, CString};
19use std::mem::ManuallyDrop;
20use std::sync::Arc;
21
22use bytes::Bytes;
23use serde::{Deserialize, Serialize};
24use tokio::runtime::Runtime;
25
26use crate::adapter::net::identity::{
27 EntityId, PermissionToken, TokenCache, TokenError as CoreTokenError, TokenScope,
28};
29use crate::adapter::net::{
30 ChannelConfig as InnerChannelConfig, ChannelConfigRegistry, ChannelHash, ChannelId,
31 ChannelName as InnerChannelName, ChannelPublisher, EntityKeypair, MeshNode, MeshNodeConfig,
32 OnFailure as InnerOnFailure, PublishConfig as InnerPublishConfig,
33 PublishReport as InnerPublishReport, Reliability, Stream as CoreStream, StreamConfig,
34 StreamError, Visibility as InnerVisibility, DEFAULT_STREAM_WINDOW_BYTES,
35};
36use crate::adapter::net::{SubnetId, SubnetPolicy, SubnetRule};
37use crate::adapter::Adapter;
38use crate::error::AdapterError;
39
40use super::handle_guard::{HandleGuard, FFI_HANDLE_FREE_DEADLINE};
41use super::NetError;
42
43pub(crate) const NET_ERR_MESH_INIT: c_int = -110;
49pub(crate) const NET_ERR_MESH_HANDSHAKE: c_int = -111;
50pub(crate) const NET_ERR_MESH_BACKPRESSURE: c_int = -112;
51pub(crate) const NET_ERR_MESH_NOT_CONNECTED: c_int = -113;
52pub(crate) const NET_ERR_MESH_TRANSPORT: c_int = -114;
53pub(crate) const NET_ERR_CHANNEL: c_int = -115;
54pub(crate) const NET_ERR_CHANNEL_AUTH: c_int = -116;
55
56pub(crate) const NET_ERR_IDENTITY: c_int = -120;
61pub(crate) const NET_ERR_TOKEN_INVALID_FORMAT: c_int = -121;
62pub(crate) const NET_ERR_TOKEN_INVALID_SIGNATURE: c_int = -122;
63pub(crate) const NET_ERR_TOKEN_EXPIRED: c_int = -123;
64pub(crate) const NET_ERR_TOKEN_NOT_YET_VALID: c_int = -124;
65pub(crate) const NET_ERR_TOKEN_DELEGATION_EXHAUSTED: c_int = -125;
66pub(crate) const NET_ERR_TOKEN_DELEGATION_NOT_ALLOWED: c_int = -126;
67pub(crate) const NET_ERR_TOKEN_NOT_AUTHORIZED: c_int = -127;
68
69#[cfg(feature = "nat-traversal")]
82pub(crate) const NET_ERR_TRAVERSAL_REFLEX_TIMEOUT: c_int = -130;
83#[cfg(feature = "nat-traversal")]
84pub(crate) const NET_ERR_TRAVERSAL_PEER_NOT_REACHABLE: c_int = -131;
85#[cfg(feature = "nat-traversal")]
86pub(crate) const NET_ERR_TRAVERSAL_TRANSPORT: c_int = -132;
87#[cfg(feature = "nat-traversal")]
88pub(crate) const NET_ERR_TRAVERSAL_RENDEZVOUS_NO_RELAY: c_int = -133;
89#[cfg(feature = "nat-traversal")]
90pub(crate) const NET_ERR_TRAVERSAL_RENDEZVOUS_REJECTED: c_int = -134;
91#[cfg(feature = "nat-traversal")]
92pub(crate) const NET_ERR_TRAVERSAL_PUNCH_FAILED: c_int = -135;
93#[cfg(feature = "nat-traversal")]
94pub(crate) const NET_ERR_TRAVERSAL_PORT_MAP_UNAVAILABLE: c_int = -136;
95pub(crate) const NET_ERR_TRAVERSAL_UNSUPPORTED: c_int = -137;
101
102#[cfg(feature = "nat-traversal")]
103fn traversal_err_to_code(e: &crate::adapter::net::traversal::TraversalError) -> c_int {
104 use crate::adapter::net::traversal::TraversalError;
105 match e {
106 TraversalError::ReflexTimeout => NET_ERR_TRAVERSAL_REFLEX_TIMEOUT,
107 TraversalError::PeerNotReachable => NET_ERR_TRAVERSAL_PEER_NOT_REACHABLE,
108 TraversalError::Transport(_) => NET_ERR_TRAVERSAL_TRANSPORT,
109 TraversalError::RendezvousNoRelay => NET_ERR_TRAVERSAL_RENDEZVOUS_NO_RELAY,
110 TraversalError::RendezvousRejected(_) => NET_ERR_TRAVERSAL_RENDEZVOUS_REJECTED,
111 TraversalError::PunchFailed => NET_ERR_TRAVERSAL_PUNCH_FAILED,
112 TraversalError::PortMapUnavailable => NET_ERR_TRAVERSAL_PORT_MAP_UNAVAILABLE,
113 TraversalError::Unsupported => NET_ERR_TRAVERSAL_UNSUPPORTED,
114 }
115}
116
117#[cfg(feature = "nat-traversal")]
121fn nat_class_to_str(class: crate::adapter::net::traversal::classify::NatClass) -> &'static str {
122 use crate::adapter::net::traversal::classify::NatClass;
123 match class {
124 NatClass::Open => "open",
125 NatClass::Cone => "cone",
126 NatClass::Symmetric => "symmetric",
127 NatClass::Unknown => "unknown",
128 }
129}
130
131fn token_err_to_code(e: &CoreTokenError) -> c_int {
132 match e {
133 CoreTokenError::InvalidFormat => NET_ERR_TOKEN_INVALID_FORMAT,
134 CoreTokenError::InvalidSignature => NET_ERR_TOKEN_INVALID_SIGNATURE,
135 CoreTokenError::Expired => NET_ERR_TOKEN_EXPIRED,
136 CoreTokenError::NotYetValid => NET_ERR_TOKEN_NOT_YET_VALID,
137 CoreTokenError::DelegationExhausted => NET_ERR_TOKEN_DELEGATION_EXHAUSTED,
138 CoreTokenError::DelegationNotAllowed => NET_ERR_TOKEN_DELEGATION_NOT_ALLOWED,
139 CoreTokenError::NotAuthorized => NET_ERR_TOKEN_NOT_AUTHORIZED,
140 CoreTokenError::ReadOnly => NET_ERR_IDENTITY,
145 CoreTokenError::ZeroTtl => NET_ERR_TOKEN_INVALID_FORMAT,
152 }
153}
154
155fn runtime() -> &'static Arc<Runtime> {
171 use std::sync::OnceLock;
172 static RT: OnceLock<Arc<Runtime>> = OnceLock::new();
173 RT.get_or_init(|| {
174 match tokio::runtime::Builder::new_multi_thread()
175 .enable_all()
176 .build()
177 {
178 Ok(rt) => Arc::new(rt),
179 Err(e) => {
180 eprintln!(
181 "FATAL: mesh FFI tokio runtime build failure ({e:?}); aborting to avoid panic across the FFI boundary"
182 );
183 std::process::abort();
184 }
185 }
186 })
187}
188
189fn block_on<F: std::future::Future>(future: F) -> F::Output {
204 if tokio::runtime::Handle::try_current().is_ok() {
205 eprintln!(
206 "FATAL: mesh FFI called from inside a tokio runtime context; \
207 aborting to avoid runtime-in-runtime panic across the FFI boundary"
208 );
209 std::process::abort();
210 }
211 runtime().block_on(future)
212}
213
214#[inline]
237pub(super) unsafe fn c_str_to_string(p: *const c_char) -> Option<String> {
238 if p.is_null() {
239 return None;
240 }
241 CStr::from_ptr(p).to_str().ok().map(str::to_owned)
242}
243
244fn write_json_out<T: Serialize>(
250 value: &T,
251 out_ptr: *mut *mut c_char,
252 out_len: *mut usize,
253) -> c_int {
254 if out_ptr.is_null() || out_len.is_null() {
255 return NetError::NullPointer.into();
256 }
257 let Ok(s) = serde_json::to_string(value) else {
258 return NetError::Unknown.into();
259 };
260 let len = s.len();
261 let Ok(cs) = CString::new(s) else {
262 return NetError::Unknown.into();
263 };
264 unsafe {
265 *out_ptr = cs.into_raw();
266 *out_len = len;
267 }
268 0
269}
270
271pub(super) fn write_string_out(s: String, out_ptr: *mut *mut c_char, out_len: *mut usize) -> c_int {
272 if out_ptr.is_null() || out_len.is_null() {
273 return NetError::NullPointer.into();
274 }
275 let len = s.len();
276 let Ok(cs) = CString::new(s) else {
277 return NetError::Unknown.into();
278 };
279 unsafe {
280 *out_ptr = cs.into_raw();
281 *out_len = len;
282 }
283 0
284}
285
286fn adapter_err_to_code(err: &AdapterError) -> c_int {
287 match err {
288 AdapterError::Connection(_) => NET_ERR_MESH_HANDSHAKE,
289 _ => NET_ERR_MESH_TRANSPORT,
290 }
291}
292
293fn stream_err_to_code(err: &StreamError) -> c_int {
294 match err {
295 StreamError::Backpressure => NET_ERR_MESH_BACKPRESSURE,
296 StreamError::NotConnected => NET_ERR_MESH_NOT_CONNECTED,
297 StreamError::Transport(_) => NET_ERR_MESH_TRANSPORT,
298 }
299}
300
301#[derive(Deserialize)]
306struct SubnetPolicyJson {
307 #[serde(default)]
308 rules: Vec<SubnetRuleJson>,
309}
310
311#[derive(Deserialize)]
312struct SubnetRuleJson {
313 tag_prefix: String,
314 level: u32,
315 #[serde(default)]
316 values: std::collections::HashMap<String, u32>,
317}
318
319fn u8_from_u32(value: u32) -> Option<u8> {
320 if value > 255 {
321 None
322 } else {
323 Some(value as u8)
324 }
325}
326
327fn subnet_id_from_json(levels: Vec<u32>) -> Option<SubnetId> {
328 if levels.is_empty() || levels.len() > 4 {
329 return None;
330 }
331 let mut bytes = [0u8; 4];
332 for (i, raw) in levels.iter().enumerate() {
333 bytes[i] = u8_from_u32(*raw)?;
334 }
335 Some(SubnetId::new(&bytes[..levels.len()]))
336}
337
338fn subnet_policy_from_json(p: SubnetPolicyJson) -> Option<SubnetPolicy> {
339 let mut policy = SubnetPolicy::new();
340 for rule_json in p.rules {
341 let level = u8_from_u32(rule_json.level)?;
342 if level > 3 {
343 return None;
344 }
345 let mut rule = SubnetRule::new(rule_json.tag_prefix, level);
346 for (tag_value, raw_val) in rule_json.values {
347 let v = u8_from_u32(raw_val)?;
348 if v == 0 {
354 return None;
355 }
356 rule = rule.map(tag_value, v);
357 }
358 policy = policy.add_rule(rule);
359 }
360 Some(policy)
361}
362
363#[derive(Deserialize)]
364struct MeshNewConfig {
365 bind_addr: String,
366 psk_hex: String,
368 heartbeat_ms: Option<u64>,
369 session_timeout_ms: Option<u64>,
370 num_shards: Option<u16>,
371 capability_gc_interval_ms: Option<u64>,
374 require_signed_capabilities: Option<bool>,
377 subnet: Option<Vec<u32>>,
379 subnet_policy: Option<SubnetPolicyJson>,
381 identity_seed_hex: Option<String>,
386 #[serde(default)]
392 reflex_override: Option<String>,
393 #[serde(default)]
397 try_port_mapping: bool,
398}
399
400pub struct MeshNodeHandle {
413 inner: ManuallyDrop<Arc<MeshNode>>,
414 channel_configs: ManuallyDrop<Arc<ChannelConfigRegistry>>,
415 guard: HandleGuard,
416}
417
418#[unsafe(no_mangle)]
433pub unsafe extern "C" fn net_mesh_new(
434 config_json: *const c_char,
435 out_handle: *mut *mut MeshNodeHandle,
436) -> c_int {
437 if config_json.is_null() || out_handle.is_null() {
438 return NetError::NullPointer.into();
439 }
440 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
441 return NetError::InvalidUtf8.into();
442 };
443 let cfg: MeshNewConfig = match serde_json::from_str(&s) {
444 Ok(v) => v,
445 Err(_) => return NetError::InvalidJson.into(),
446 };
447 let bind_addr: std::net::SocketAddr = match cfg.bind_addr.parse() {
448 Ok(a) => a,
449 Err(_) => return NET_ERR_MESH_INIT,
450 };
451 let psk_bytes = match hex::decode(&cfg.psk_hex) {
452 Ok(b) => b,
453 Err(_) => return NET_ERR_MESH_INIT,
454 };
455 if psk_bytes.len() != 32 {
456 return NET_ERR_MESH_INIT;
457 }
458 let mut psk = [0u8; 32];
459 psk.copy_from_slice(&psk_bytes);
460
461 let mut node_cfg = MeshNodeConfig::new(bind_addr, psk);
462 if let Some(ms) = cfg.heartbeat_ms {
470 if ms == 0 {
471 return NetError::InvalidJson.into();
472 }
473 node_cfg = node_cfg.with_heartbeat_interval(std::time::Duration::from_millis(ms));
474 }
475 if let Some(ms) = cfg.session_timeout_ms {
476 if ms == 0 {
477 return NetError::InvalidJson.into();
478 }
479 node_cfg = node_cfg.with_session_timeout(std::time::Duration::from_millis(ms));
480 }
481 if let Some(n) = cfg.num_shards {
482 node_cfg = node_cfg.with_num_shards(n);
483 }
484 if let Some(ms) = cfg.capability_gc_interval_ms {
485 node_cfg = node_cfg.with_capability_gc_interval(std::time::Duration::from_millis(ms));
486 }
487 if let Some(b) = cfg.require_signed_capabilities {
488 node_cfg = node_cfg.with_require_signed_capabilities(b);
489 }
490 if let Some(levels) = cfg.subnet {
491 let Some(id) = subnet_id_from_json(levels) else {
492 return NET_ERR_MESH_INIT;
493 };
494 node_cfg = node_cfg.with_subnet(id);
495 }
496 if let Some(policy_js) = cfg.subnet_policy {
497 let Some(policy) = subnet_policy_from_json(policy_js) else {
498 return NET_ERR_MESH_INIT;
499 };
500 node_cfg = node_cfg.with_subnet_policy(Arc::new(policy));
501 }
502 #[cfg(feature = "nat-traversal")]
503 if let Some(external_str) = cfg.reflex_override.as_deref() {
504 let Ok(external) = external_str.parse::<std::net::SocketAddr>() else {
505 return NET_ERR_MESH_INIT;
506 };
507 node_cfg = node_cfg.with_reflex_override(external);
508 }
509 #[cfg(not(feature = "nat-traversal"))]
513 let _ = cfg.reflex_override;
514 #[cfg(feature = "port-mapping")]
515 if cfg.try_port_mapping {
516 node_cfg = node_cfg.with_try_port_mapping(true);
517 }
518 #[cfg(not(feature = "port-mapping"))]
520 let _ = cfg.try_port_mapping;
521
522 let identity = match cfg.identity_seed_hex {
523 Some(seed_hex) => {
524 let bytes = match hex::decode(&seed_hex) {
525 Ok(b) => b,
526 Err(_) => return NET_ERR_MESH_INIT,
527 };
528 if bytes.len() != 32 {
529 return NET_ERR_MESH_INIT;
530 }
531 let mut arr = [0u8; 32];
532 arr.copy_from_slice(&bytes);
533 EntityKeypair::from_bytes(arr)
534 }
535 None => EntityKeypair::generate(),
536 };
537 let result = block_on(async move { MeshNode::new(identity, node_cfg).await });
538 match result {
539 Ok(mut node) => {
540 let channel_configs = Arc::new(ChannelConfigRegistry::new());
541 node.set_channel_configs(channel_configs.clone());
542 node.set_token_cache(Arc::new(TokenCache::new()));
546 let handle = Box::new(MeshNodeHandle {
547 inner: ManuallyDrop::new(Arc::new(node)),
548 channel_configs: ManuallyDrop::new(channel_configs),
549 guard: HandleGuard::new(),
550 });
551 unsafe {
552 *out_handle = Box::into_raw(handle);
553 }
554 0
555 }
556 Err(_) => NET_ERR_MESH_INIT,
557 }
558}
559
560#[unsafe(no_mangle)]
561pub unsafe extern "C" fn net_mesh_free(handle: *mut MeshNodeHandle) {
562 if handle.is_null() {
563 return;
564 }
565 let h: &MeshNodeHandle = unsafe { &*handle };
570 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
571 unsafe {
573 let mh = &mut *handle;
574 let inner = ManuallyDrop::take(&mut mh.inner);
575 let configs = ManuallyDrop::take(&mut mh.channel_configs);
576 drop(inner);
577 drop(configs);
578 }
579 } else {
580 tracing::warn!(
581 "net_mesh_free: in-flight ops did not drain within deadline; \
582 leaking inner to avoid use-after-free"
583 );
584 }
585}
586
587#[unsafe(no_mangle)]
595pub unsafe extern "C" fn net_mesh_arc_clone(handle: *mut MeshNodeHandle) -> *mut Arc<MeshNode> {
596 if handle.is_null() {
597 return std::ptr::null_mut();
598 }
599 let h = unsafe { &*handle };
600 let _op = match h.guard.try_enter() {
602 Some(op) => op,
603 None => return std::ptr::null_mut(),
604 };
605 let cloned: Arc<MeshNode> = Arc::clone(&h.inner);
606 Box::into_raw(Box::new(cloned))
607}
608
609#[unsafe(no_mangle)]
616pub unsafe extern "C" fn net_mesh_channel_configs_arc_clone(
617 handle: *mut MeshNodeHandle,
618) -> *mut Arc<ChannelConfigRegistry> {
619 if handle.is_null() {
620 return std::ptr::null_mut();
621 }
622 let h = unsafe { &*handle };
623 let _op = match h.guard.try_enter() {
625 Some(op) => op,
626 None => return std::ptr::null_mut(),
627 };
628 let cloned: Arc<ChannelConfigRegistry> = Arc::clone(&h.channel_configs);
629 Box::into_raw(Box::new(cloned))
630}
631
632#[unsafe(no_mangle)]
635pub unsafe extern "C" fn net_mesh_arc_free(p: *mut Arc<MeshNode>) {
636 if p.is_null() {
637 return;
638 }
639 unsafe {
640 drop(Box::from_raw(p));
641 }
642}
643
644#[unsafe(no_mangle)]
647pub unsafe extern "C" fn net_mesh_channel_configs_arc_free(p: *mut Arc<ChannelConfigRegistry>) {
648 if p.is_null() {
649 return;
650 }
651 unsafe {
652 drop(Box::from_raw(p));
653 }
654}
655
656#[unsafe(no_mangle)]
659pub unsafe extern "C" fn net_mesh_public_key_hex(
660 handle: *mut MeshNodeHandle,
661 out_ptr: *mut *mut c_char,
662 out_len: *mut usize,
663) -> c_int {
664 if handle.is_null() || out_ptr.is_null() || out_len.is_null() {
665 return NetError::NullPointer.into();
666 }
667 let h = unsafe { &*handle };
668 let _op = match h.guard.try_enter() {
669 Some(op) => op,
670 None => return NetError::ShuttingDown.into(),
671 };
672 let s = hex::encode(h.inner.public_key());
673 write_string_out(s, out_ptr, out_len)
674}
675
676#[unsafe(no_mangle)]
677pub unsafe extern "C" fn net_mesh_node_id(handle: *mut MeshNodeHandle) -> u64 {
678 if handle.is_null() {
679 return 0;
680 }
681 let h = unsafe { &*handle };
682 let _op = match h.guard.try_enter() {
684 Some(op) => op,
685 None => return 0,
686 };
687 h.inner.node_id()
688}
689
690#[unsafe(no_mangle)]
694pub unsafe extern "C" fn net_mesh_entity_id(handle: *mut MeshNodeHandle, out: *mut u8) -> c_int {
695 if handle.is_null() || out.is_null() {
696 return NetError::NullPointer.into();
697 }
698 let h = unsafe { &*handle };
699 let _op = match h.guard.try_enter() {
700 Some(op) => op,
701 None => return NetError::ShuttingDown.into(),
702 };
703 let bytes = h.inner.entity_id().as_bytes();
704 unsafe {
705 std::ptr::copy_nonoverlapping(bytes.as_ptr(), out, 32);
706 }
707 0
708}
709
710#[unsafe(no_mangle)]
712pub unsafe extern "C" fn net_mesh_connect(
713 handle: *mut MeshNodeHandle,
714 peer_addr: *const c_char,
715 peer_pubkey_hex: *const c_char,
716 peer_node_id: u64,
717) -> c_int {
718 if handle.is_null() || peer_addr.is_null() || peer_pubkey_hex.is_null() {
719 return NetError::NullPointer.into();
720 }
721 let h = unsafe { &*handle };
722 let _op = match h.guard.try_enter() {
723 Some(op) => op,
724 None => return NetError::ShuttingDown.into(),
725 };
726 let Some(addr_s) = (unsafe { c_str_to_string(peer_addr) }) else {
727 return NetError::InvalidUtf8.into();
728 };
729 let addr: std::net::SocketAddr = match addr_s.parse() {
730 Ok(a) => a,
731 Err(_) => return NET_ERR_MESH_HANDSHAKE,
732 };
733 let Some(pk_s) = (unsafe { c_str_to_string(peer_pubkey_hex) }) else {
734 return NetError::InvalidUtf8.into();
735 };
736 let pk_bytes = match hex::decode(pk_s) {
737 Ok(b) => b,
738 Err(_) => return NET_ERR_MESH_HANDSHAKE,
739 };
740 if pk_bytes.len() != 32 {
741 return NET_ERR_MESH_HANDSHAKE;
742 }
743 let mut pk = [0u8; 32];
744 pk.copy_from_slice(&pk_bytes);
745
746 let node = h.inner.clone();
747 match block_on(async move { node.connect(addr, &pk, peer_node_id).await }) {
748 Ok(_) => 0,
749 Err(e) => adapter_err_to_code(&e),
750 }
751}
752
753#[unsafe(no_mangle)]
756pub unsafe extern "C" fn net_mesh_accept(
757 handle: *mut MeshNodeHandle,
758 peer_node_id: u64,
759 out_addr: *mut *mut c_char,
760 out_len: *mut usize,
761) -> c_int {
762 if handle.is_null() || out_addr.is_null() || out_len.is_null() {
763 return NetError::NullPointer.into();
764 }
765 let h = unsafe { &*handle };
766 let _op = match h.guard.try_enter() {
767 Some(op) => op,
768 None => return NetError::ShuttingDown.into(),
769 };
770 let node = h.inner.clone();
771 match block_on(async move { node.accept(peer_node_id).await }) {
772 Ok((addr, _)) => write_string_out(addr.to_string(), out_addr, out_len),
773 Err(e) => adapter_err_to_code(&e),
774 }
775}
776
777#[unsafe(no_mangle)]
778pub unsafe extern "C" fn net_mesh_start(handle: *mut MeshNodeHandle) -> c_int {
779 if handle.is_null() {
780 return NetError::NullPointer.into();
781 }
782 let h = unsafe { &*handle };
783 let _op = match h.guard.try_enter() {
784 Some(op) => op,
785 None => return NetError::ShuttingDown.into(),
786 };
787 let node = h.inner.clone();
788 block_on(async move { node.start() });
791 0
792}
793
794#[unsafe(no_mangle)]
806pub unsafe extern "C" fn net_mesh_shutdown(handle: *mut MeshNodeHandle) -> c_int {
807 if handle.is_null() {
808 return NetError::NullPointer.into();
809 }
810 let h = unsafe { &*handle };
811 let _op = match h.guard.try_enter() {
812 Some(op) => op,
813 None => return NetError::ShuttingDown.into(),
814 };
815 match block_on(async { h.inner.shutdown().await }) {
816 Ok(()) => 0,
817 Err(e) => adapter_err_to_code(&e),
818 }
819}
820
821#[cfg(feature = "nat-traversal")]
843#[unsafe(no_mangle)]
844pub unsafe extern "C" fn net_mesh_nat_type(
845 handle: *mut MeshNodeHandle,
846 out_str: *mut *mut c_char,
847 out_len: *mut usize,
848) -> c_int {
849 if handle.is_null() || out_str.is_null() || out_len.is_null() {
850 return NetError::NullPointer.into();
851 }
852 let h = unsafe { &*handle };
853 let _op = match h.guard.try_enter() {
854 Some(op) => op,
855 None => return NetError::ShuttingDown.into(),
856 };
857 write_string_out(
858 nat_class_to_str(h.inner.nat_class()).to_string(),
859 out_str,
860 out_len,
861 )
862}
863
864#[cfg(feature = "nat-traversal")]
869#[unsafe(no_mangle)]
870pub unsafe extern "C" fn net_mesh_reflex_addr(
871 handle: *mut MeshNodeHandle,
872 out_str: *mut *mut c_char,
873 out_len: *mut usize,
874) -> c_int {
875 if handle.is_null() || out_str.is_null() || out_len.is_null() {
876 return NetError::NullPointer.into();
877 }
878 let h = unsafe { &*handle };
879 let _op = match h.guard.try_enter() {
880 Some(op) => op,
881 None => return NetError::ShuttingDown.into(),
882 };
883 let s = h
884 .inner
885 .reflex_addr()
886 .map(|a| a.to_string())
887 .unwrap_or_default();
888 write_string_out(s, out_str, out_len)
889}
890
891#[cfg(feature = "nat-traversal")]
895#[unsafe(no_mangle)]
896pub unsafe extern "C" fn net_mesh_peer_nat_type(
897 handle: *mut MeshNodeHandle,
898 peer_node_id: u64,
899 out_str: *mut *mut c_char,
900 out_len: *mut usize,
901) -> c_int {
902 if handle.is_null() || out_str.is_null() || out_len.is_null() {
903 return NetError::NullPointer.into();
904 }
905 let h = unsafe { &*handle };
906 let _op = match h.guard.try_enter() {
907 Some(op) => op,
908 None => return NetError::ShuttingDown.into(),
909 };
910 write_string_out(
911 nat_class_to_str(h.inner.peer_nat_class(peer_node_id)).to_string(),
912 out_str,
913 out_len,
914 )
915}
916
917#[cfg(feature = "nat-traversal")]
926#[unsafe(no_mangle)]
927pub unsafe extern "C" fn net_mesh_probe_reflex(
928 handle: *mut MeshNodeHandle,
929 peer_node_id: u64,
930 out_str: *mut *mut c_char,
931 out_len: *mut usize,
932) -> c_int {
933 if handle.is_null() || out_str.is_null() || out_len.is_null() {
934 return NetError::NullPointer.into();
935 }
936 let h = unsafe { &*handle };
937 let _op = match h.guard.try_enter() {
938 Some(op) => op,
939 None => return NetError::ShuttingDown.into(),
940 };
941 let node = h.inner.clone();
942 match block_on(async move { node.probe_reflex(peer_node_id).await }) {
943 Ok(addr) => write_string_out(addr.to_string(), out_str, out_len),
944 Err(e) => traversal_err_to_code(&e),
945 }
946}
947
948#[cfg(feature = "nat-traversal")]
953#[unsafe(no_mangle)]
954pub unsafe extern "C" fn net_mesh_reclassify_nat(handle: *mut MeshNodeHandle) -> c_int {
955 if handle.is_null() {
956 return NetError::NullPointer.into();
957 }
958 let h = unsafe { &*handle };
959 let _op = match h.guard.try_enter() {
960 Some(op) => op,
961 None => return NetError::ShuttingDown.into(),
962 };
963 let node = h.inner.clone();
964 block_on(async move { node.reclassify_nat().await });
965 0
966}
967
968#[cfg(feature = "nat-traversal")]
973#[unsafe(no_mangle)]
974pub unsafe extern "C" fn net_mesh_traversal_stats(
975 handle: *mut MeshNodeHandle,
976 out_punches_attempted: *mut u64,
977 out_punches_succeeded: *mut u64,
978 out_relay_fallbacks: *mut u64,
979) -> c_int {
980 if handle.is_null() {
981 return NetError::NullPointer.into();
982 }
983 let h = unsafe { &*handle };
984 let _op = match h.guard.try_enter() {
985 Some(op) => op,
986 None => return NetError::ShuttingDown.into(),
987 };
988 let snap = h.inner.traversal_stats();
989 unsafe {
990 if !out_punches_attempted.is_null() {
991 *out_punches_attempted = snap.punches_attempted;
992 }
993 if !out_punches_succeeded.is_null() {
994 *out_punches_succeeded = snap.punches_succeeded;
995 }
996 if !out_relay_fallbacks.is_null() {
997 *out_relay_fallbacks = snap.relay_fallbacks;
998 }
999 }
1000 0
1001}
1002
1003#[cfg(feature = "nat-traversal")]
1015#[unsafe(no_mangle)]
1016pub unsafe extern "C" fn net_mesh_connect_direct(
1017 handle: *mut MeshNodeHandle,
1018 peer_node_id: u64,
1019 peer_pubkey_hex: *const c_char,
1020 coordinator: u64,
1021) -> c_int {
1022 if handle.is_null() || peer_pubkey_hex.is_null() {
1023 return NetError::NullPointer.into();
1024 }
1025 let h = unsafe { &*handle };
1026 let _op = match h.guard.try_enter() {
1027 Some(op) => op,
1028 None => return NetError::ShuttingDown.into(),
1029 };
1030 let Some(pk_s) = (unsafe { c_str_to_string(peer_pubkey_hex) }) else {
1031 return NetError::InvalidUtf8.into();
1032 };
1033 let pk_bytes = match hex::decode(pk_s) {
1034 Ok(b) => b,
1035 Err(_) => return NET_ERR_MESH_HANDSHAKE,
1036 };
1037 if pk_bytes.len() != 32 {
1038 return NET_ERR_MESH_HANDSHAKE;
1039 }
1040 let mut pk = [0u8; 32];
1041 pk.copy_from_slice(&pk_bytes);
1042
1043 let node = h.inner.clone();
1044 match block_on(async move { node.connect_direct(peer_node_id, &pk, coordinator).await }) {
1045 Ok(_) => 0,
1046 Err(e) => traversal_err_to_code(&e),
1047 }
1048}
1049
1050#[cfg(feature = "nat-traversal")]
1058#[unsafe(no_mangle)]
1059pub unsafe extern "C" fn net_mesh_set_reflex_override(
1060 handle: *mut MeshNodeHandle,
1061 external: *const c_char,
1062) -> c_int {
1063 if handle.is_null() || external.is_null() {
1064 return NetError::NullPointer.into();
1065 }
1066 let h = unsafe { &*handle };
1067 let _op = match h.guard.try_enter() {
1068 Some(op) => op,
1069 None => return NetError::ShuttingDown.into(),
1070 };
1071 let Some(s) = (unsafe { c_str_to_string(external) }) else {
1072 return NetError::InvalidUtf8.into();
1073 };
1074 let Ok(addr) = s.parse::<std::net::SocketAddr>() else {
1075 return NET_ERR_MESH_INIT;
1076 };
1077 h.inner.set_reflex_override(addr);
1078 0
1079}
1080
1081#[cfg(feature = "nat-traversal")]
1089#[unsafe(no_mangle)]
1090pub unsafe extern "C" fn net_mesh_clear_reflex_override(handle: *mut MeshNodeHandle) -> c_int {
1091 if handle.is_null() {
1092 return NetError::NullPointer.into();
1093 }
1094 let h = unsafe { &*handle };
1095 let _op = match h.guard.try_enter() {
1096 Some(op) => op,
1097 None => return NetError::ShuttingDown.into(),
1098 };
1099 h.inner.clear_reflex_override();
1100 0
1101}
1102
1103#[cfg(not(feature = "nat-traversal"))]
1126#[unsafe(no_mangle)]
1127pub unsafe extern "C" fn net_mesh_nat_type(
1128 _handle: *mut MeshNodeHandle,
1129 _out_str: *mut *mut c_char,
1130 _out_len: *mut usize,
1131) -> c_int {
1132 NET_ERR_TRAVERSAL_UNSUPPORTED
1133}
1134
1135#[cfg(not(feature = "nat-traversal"))]
1136#[unsafe(no_mangle)]
1137pub unsafe extern "C" fn net_mesh_reflex_addr(
1138 _handle: *mut MeshNodeHandle,
1139 _out_str: *mut *mut c_char,
1140 _out_len: *mut usize,
1141) -> c_int {
1142 NET_ERR_TRAVERSAL_UNSUPPORTED
1143}
1144
1145#[cfg(not(feature = "nat-traversal"))]
1146#[unsafe(no_mangle)]
1147pub unsafe extern "C" fn net_mesh_peer_nat_type(
1148 _handle: *mut MeshNodeHandle,
1149 _peer_node_id: u64,
1150 _out_str: *mut *mut c_char,
1151 _out_len: *mut usize,
1152) -> c_int {
1153 NET_ERR_TRAVERSAL_UNSUPPORTED
1154}
1155
1156#[cfg(not(feature = "nat-traversal"))]
1157#[unsafe(no_mangle)]
1158pub unsafe extern "C" fn net_mesh_probe_reflex(
1159 _handle: *mut MeshNodeHandle,
1160 _peer_node_id: u64,
1161 _out_str: *mut *mut c_char,
1162 _out_len: *mut usize,
1163) -> c_int {
1164 NET_ERR_TRAVERSAL_UNSUPPORTED
1165}
1166
1167#[cfg(not(feature = "nat-traversal"))]
1168#[unsafe(no_mangle)]
1169pub unsafe extern "C" fn net_mesh_reclassify_nat(_handle: *mut MeshNodeHandle) -> c_int {
1170 NET_ERR_TRAVERSAL_UNSUPPORTED
1171}
1172
1173#[cfg(not(feature = "nat-traversal"))]
1174#[unsafe(no_mangle)]
1175pub unsafe extern "C" fn net_mesh_traversal_stats(
1176 _handle: *mut MeshNodeHandle,
1177 _out_punches_attempted: *mut u64,
1178 _out_punches_succeeded: *mut u64,
1179 _out_relay_fallbacks: *mut u64,
1180) -> c_int {
1181 NET_ERR_TRAVERSAL_UNSUPPORTED
1182}
1183
1184#[cfg(not(feature = "nat-traversal"))]
1185#[unsafe(no_mangle)]
1186pub unsafe extern "C" fn net_mesh_connect_direct(
1187 _handle: *mut MeshNodeHandle,
1188 _peer_node_id: u64,
1189 _peer_pubkey_hex: *const c_char,
1190 _coordinator: u64,
1191) -> c_int {
1192 NET_ERR_TRAVERSAL_UNSUPPORTED
1193}
1194
1195#[cfg(not(feature = "nat-traversal"))]
1196#[unsafe(no_mangle)]
1197pub unsafe extern "C" fn net_mesh_set_reflex_override(
1198 _handle: *mut MeshNodeHandle,
1199 _external: *const c_char,
1200) -> c_int {
1201 NET_ERR_TRAVERSAL_UNSUPPORTED
1202}
1203
1204#[cfg(not(feature = "nat-traversal"))]
1205#[unsafe(no_mangle)]
1206pub unsafe extern "C" fn net_mesh_clear_reflex_override(_handle: *mut MeshNodeHandle) -> c_int {
1207 NET_ERR_TRAVERSAL_UNSUPPORTED
1208}
1209
1210#[derive(Deserialize, Default)]
1215struct StreamOpenConfig {
1216 reliability: Option<String>,
1218 window_bytes: Option<u32>,
1221 fairness_weight: Option<u8>,
1222}
1223
1224pub struct MeshStreamHandle {
1239 stream: ManuallyDrop<CoreStream>,
1240 _node: ManuallyDrop<Arc<MeshNode>>,
1243 guard: HandleGuard,
1244}
1245
1246#[unsafe(no_mangle)]
1247pub unsafe extern "C" fn net_mesh_open_stream(
1248 handle: *mut MeshNodeHandle,
1249 peer_node_id: u64,
1250 stream_id: u64,
1251 config_json: *const c_char,
1252 out_stream: *mut *mut MeshStreamHandle,
1253) -> c_int {
1254 if handle.is_null() || out_stream.is_null() {
1255 return NetError::NullPointer.into();
1256 }
1257 let h = unsafe { &*handle };
1258 let _op = match h.guard.try_enter() {
1259 Some(op) => op,
1260 None => return NetError::ShuttingDown.into(),
1261 };
1262 let cfg_json: StreamOpenConfig = if config_json.is_null() {
1263 StreamOpenConfig::default()
1264 } else {
1265 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1266 return NetError::InvalidUtf8.into();
1267 };
1268 match serde_json::from_str(&s) {
1269 Ok(v) => v,
1270 Err(_) => return NetError::InvalidJson.into(),
1271 }
1272 };
1273 let reliability = match cfg_json.reliability.as_deref() {
1274 None | Some("fire_and_forget") => Reliability::FireAndForget,
1275 Some("reliable") => Reliability::Reliable,
1276 Some(_) => return NET_ERR_MESH_TRANSPORT,
1277 };
1278 let window = cfg_json.window_bytes.unwrap_or(DEFAULT_STREAM_WINDOW_BYTES);
1279 let weight = cfg_json.fairness_weight.unwrap_or(1);
1280 let cfg = StreamConfig::new()
1281 .with_reliability(reliability)
1282 .with_window_bytes(window)
1283 .with_fairness_weight(weight);
1284 match h.inner.open_stream(peer_node_id, stream_id, cfg) {
1285 Ok(stream) => {
1286 let node_clone: Arc<MeshNode> = Arc::clone(&h.inner);
1287 let sh = Box::new(MeshStreamHandle {
1288 stream: ManuallyDrop::new(stream),
1289 _node: ManuallyDrop::new(node_clone),
1290 guard: HandleGuard::new(),
1291 });
1292 unsafe {
1293 *out_stream = Box::into_raw(sh);
1294 }
1295 0
1296 }
1297 Err(e) => adapter_err_to_code(&e),
1298 }
1299}
1300
1301#[unsafe(no_mangle)]
1302pub unsafe extern "C" fn net_mesh_stream_free(handle: *mut MeshStreamHandle) {
1303 if handle.is_null() {
1304 return;
1305 }
1306 let h: &MeshStreamHandle = unsafe { &*handle };
1308 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
1309 unsafe {
1311 let _stream = ManuallyDrop::take(&mut (*handle).stream);
1315 let node = ManuallyDrop::take(&mut (*handle)._node);
1316 drop(node);
1317 }
1318 } else {
1319 tracing::warn!(
1320 "net_mesh_stream_free: in-flight ops did not drain within deadline; \
1321 leaking inner to avoid use-after-free"
1322 );
1323 }
1324}
1325
1326unsafe fn collect_payloads(
1336 payloads: *const *const u8,
1337 lens: *const usize,
1338 count: usize,
1339) -> Option<Vec<Bytes>> {
1340 let mut out = Vec::with_capacity(count);
1341 for i in 0..count {
1342 let ptr = *payloads.add(i);
1343 let len = *lens.add(i);
1344 if ptr.is_null() {
1345 if len == 0 {
1346 out.push(Bytes::new());
1347 continue;
1348 }
1349 return None;
1350 }
1351 if len > isize::MAX as usize {
1355 return None;
1356 }
1357 let slice = std::slice::from_raw_parts(ptr, len);
1358 out.push(Bytes::copy_from_slice(slice));
1359 }
1360 Some(out)
1361}
1362
1363#[inline]
1371fn handles_match(sh: &MeshStreamHandle, nh: &MeshNodeHandle) -> bool {
1372 Arc::ptr_eq(&sh._node, &nh.inner)
1373}
1374
1375#[unsafe(no_mangle)]
1376pub unsafe extern "C" fn net_mesh_send(
1377 handle: *mut MeshStreamHandle,
1378 payloads: *const *const u8,
1379 lens: *const usize,
1380 count: usize,
1381 node_handle: *mut MeshNodeHandle,
1382) -> c_int {
1383 if handle.is_null() || node_handle.is_null() {
1384 return NetError::NullPointer.into();
1385 }
1386 if count > 0 && (payloads.is_null() || lens.is_null()) {
1387 return NetError::NullPointer.into();
1388 }
1389 let sh = unsafe { &*handle };
1390 let nh = unsafe { &*node_handle };
1391 let _sh_op = match sh.guard.try_enter() {
1394 Some(op) => op,
1395 None => return NetError::ShuttingDown.into(),
1396 };
1397 let _nh_op = match nh.guard.try_enter() {
1398 Some(op) => op,
1399 None => return NetError::ShuttingDown.into(),
1400 };
1401 if !handles_match(sh, nh) {
1402 return NetError::MismatchedHandles.into();
1403 }
1404 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1405 Some(v) => v,
1406 None => return NetError::NullPointer.into(),
1407 };
1408 let node = nh.inner.clone();
1409 let stream = sh.stream.clone();
1410 match block_on(async move { node.send_on_stream(&stream, &payloads).await }) {
1411 Ok(()) => 0,
1412 Err(e) => stream_err_to_code(&e),
1413 }
1414}
1415
1416#[unsafe(no_mangle)]
1417pub unsafe extern "C" fn net_mesh_send_with_retry(
1418 handle: *mut MeshStreamHandle,
1419 payloads: *const *const u8,
1420 lens: *const usize,
1421 count: usize,
1422 max_retries: u32,
1423 node_handle: *mut MeshNodeHandle,
1424) -> c_int {
1425 if handle.is_null() || node_handle.is_null() {
1426 return NetError::NullPointer.into();
1427 }
1428 if count > 0 && (payloads.is_null() || lens.is_null()) {
1429 return NetError::NullPointer.into();
1430 }
1431 let sh = unsafe { &*handle };
1432 let nh = unsafe { &*node_handle };
1433 let _sh_op = match sh.guard.try_enter() {
1436 Some(op) => op,
1437 None => return NetError::ShuttingDown.into(),
1438 };
1439 let _nh_op = match nh.guard.try_enter() {
1440 Some(op) => op,
1441 None => return NetError::ShuttingDown.into(),
1442 };
1443 if !handles_match(sh, nh) {
1444 return NetError::MismatchedHandles.into();
1445 }
1446 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1447 Some(v) => v,
1448 None => return NetError::NullPointer.into(),
1449 };
1450 let node = nh.inner.clone();
1451 let stream = sh.stream.clone();
1452 match block_on(async move {
1453 node.send_with_retry(&stream, &payloads, max_retries as usize)
1454 .await
1455 }) {
1456 Ok(()) => 0,
1457 Err(e) => stream_err_to_code(&e),
1458 }
1459}
1460
1461#[unsafe(no_mangle)]
1462pub unsafe extern "C" fn net_mesh_send_blocking(
1463 handle: *mut MeshStreamHandle,
1464 payloads: *const *const u8,
1465 lens: *const usize,
1466 count: usize,
1467 node_handle: *mut MeshNodeHandle,
1468) -> c_int {
1469 if handle.is_null() || node_handle.is_null() {
1470 return NetError::NullPointer.into();
1471 }
1472 if count > 0 && (payloads.is_null() || lens.is_null()) {
1473 return NetError::NullPointer.into();
1474 }
1475 let sh = unsafe { &*handle };
1476 let nh = unsafe { &*node_handle };
1477 let _sh_op = match sh.guard.try_enter() {
1480 Some(op) => op,
1481 None => return NetError::ShuttingDown.into(),
1482 };
1483 let _nh_op = match nh.guard.try_enter() {
1484 Some(op) => op,
1485 None => return NetError::ShuttingDown.into(),
1486 };
1487 if !handles_match(sh, nh) {
1488 return NetError::MismatchedHandles.into();
1489 }
1490 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1491 Some(v) => v,
1492 None => return NetError::NullPointer.into(),
1493 };
1494 let node = nh.inner.clone();
1495 let stream = sh.stream.clone();
1496 match block_on(async move { node.send_blocking(&stream, &payloads).await }) {
1497 Ok(()) => 0,
1498 Err(e) => stream_err_to_code(&e),
1499 }
1500}
1501
1502#[derive(Serialize)]
1503struct StreamStatsJson {
1504 tx_seq: u64,
1505 rx_seq: u64,
1506 inbound_pending: u64,
1507 last_activity_ns: u64,
1508 active: bool,
1509 backpressure_events: u64,
1510 tx_credit_remaining: u32,
1511 tx_window: u32,
1512 credit_grants_received: u64,
1513 credit_grants_sent: u64,
1514}
1515
1516#[unsafe(no_mangle)]
1517pub unsafe extern "C" fn net_mesh_stream_stats(
1518 node_handle: *mut MeshNodeHandle,
1519 peer_node_id: u64,
1520 stream_id: u64,
1521 out_json: *mut *mut c_char,
1522 out_len: *mut usize,
1523) -> c_int {
1524 if node_handle.is_null() || out_json.is_null() || out_len.is_null() {
1525 return NetError::NullPointer.into();
1526 }
1527 let h = unsafe { &*node_handle };
1528 let _op = match h.guard.try_enter() {
1529 Some(op) => op,
1530 None => return NetError::ShuttingDown.into(),
1531 };
1532 match h.inner.stream_stats(peer_node_id, stream_id) {
1533 Some(s) => {
1534 let js = StreamStatsJson {
1535 tx_seq: s.tx_seq,
1536 rx_seq: s.rx_seq,
1537 inbound_pending: s.inbound_pending,
1538 last_activity_ns: s.last_activity_ns,
1539 active: s.active,
1540 backpressure_events: s.backpressure_events,
1541 tx_credit_remaining: s.tx_credit_remaining,
1542 tx_window: s.tx_window,
1543 credit_grants_received: s.credit_grants_received,
1544 credit_grants_sent: s.credit_grants_sent,
1545 };
1546 write_json_out(&js, out_json, out_len)
1547 }
1548 None => {
1549 write_string_out("null".to_string(), out_json, out_len)
1552 }
1553 }
1554}
1555
1556#[derive(Serialize)]
1561struct RecvEventJson {
1562 id: String,
1563 payload_b64: String,
1565 insertion_ts: u64,
1566 shard_id: u16,
1567}
1568
1569#[unsafe(no_mangle)]
1570pub unsafe extern "C" fn net_mesh_recv_shard(
1571 handle: *mut MeshNodeHandle,
1572 shard_id: u16,
1573 limit: u32,
1574 out_json: *mut *mut c_char,
1575 out_len: *mut usize,
1576) -> c_int {
1577 if handle.is_null() || out_json.is_null() || out_len.is_null() {
1578 return NetError::NullPointer.into();
1579 }
1580 let h = unsafe { &*handle };
1581 let _op = match h.guard.try_enter() {
1582 Some(op) => op,
1583 None => return NetError::ShuttingDown.into(),
1584 };
1585 let node = h.inner.clone();
1586 let result = block_on(async move { node.poll_shard(shard_id, None, limit as usize).await });
1587 let result = match result {
1588 Ok(r) => r,
1589 Err(e) => return adapter_err_to_code(&e),
1590 };
1591 let events: Vec<RecvEventJson> = result
1592 .events
1593 .into_iter()
1594 .map(|e| RecvEventJson {
1595 id: e.id,
1596 payload_b64: encode_b64(&e.raw),
1597 insertion_ts: e.insertion_ts,
1598 shard_id: e.shard_id,
1599 })
1600 .collect();
1601 write_json_out(&events, out_json, out_len)
1602}
1603
1604fn encode_b64(bytes: &[u8]) -> String {
1605 const ALPH: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1608 let mut s = String::with_capacity(bytes.len().div_ceil(3) * 4);
1609 let mut i = 0;
1610 while i + 3 <= bytes.len() {
1611 let chunk = &bytes[i..i + 3];
1612 s.push(ALPH[(chunk[0] >> 2) as usize] as char);
1613 s.push(ALPH[(((chunk[0] & 0b11) << 4) | (chunk[1] >> 4)) as usize] as char);
1614 s.push(ALPH[(((chunk[1] & 0b1111) << 2) | (chunk[2] >> 6)) as usize] as char);
1615 s.push(ALPH[(chunk[2] & 0b111111) as usize] as char);
1616 i += 3;
1617 }
1618 let rem = bytes.len() - i;
1619 if rem == 1 {
1620 let b = bytes[i];
1621 s.push(ALPH[(b >> 2) as usize] as char);
1622 s.push(ALPH[((b & 0b11) << 4) as usize] as char);
1623 s.push('=');
1624 s.push('=');
1625 } else if rem == 2 {
1626 let b0 = bytes[i];
1627 let b1 = bytes[i + 1];
1628 s.push(ALPH[(b0 >> 2) as usize] as char);
1629 s.push(ALPH[(((b0 & 0b11) << 4) | (b1 >> 4)) as usize] as char);
1630 s.push(ALPH[((b1 & 0b1111) << 2) as usize] as char);
1631 s.push('=');
1632 }
1633 s
1634}
1635
1636#[derive(Deserialize)]
1641struct ChannelConfigInput {
1642 name: String,
1643 visibility: Option<String>,
1644 reliable: Option<bool>,
1645 require_token: Option<bool>,
1646 priority: Option<u8>,
1647 max_rate_pps: Option<u32>,
1648 publish_caps: Option<CapabilityFilterJson>,
1652 subscribe_caps: Option<CapabilityFilterJson>,
1656}
1657
1658fn parse_visibility(s: &str) -> Option<InnerVisibility> {
1659 match s {
1660 "subnet-local" => Some(InnerVisibility::SubnetLocal),
1661 "parent-visible" => Some(InnerVisibility::ParentVisible),
1662 "exported" => Some(InnerVisibility::Exported),
1663 "global" => Some(InnerVisibility::Global),
1664 _ => None,
1665 }
1666}
1667
1668#[unsafe(no_mangle)]
1669pub unsafe extern "C" fn net_mesh_register_channel(
1670 handle: *mut MeshNodeHandle,
1671 config_json: *const c_char,
1672) -> c_int {
1673 if handle.is_null() || config_json.is_null() {
1674 return NetError::NullPointer.into();
1675 }
1676 let h = unsafe { &*handle };
1677 let _op = match h.guard.try_enter() {
1678 Some(op) => op,
1679 None => return NetError::ShuttingDown.into(),
1680 };
1681 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1682 return NetError::InvalidUtf8.into();
1683 };
1684 let input: ChannelConfigInput = match serde_json::from_str(&s) {
1685 Ok(v) => v,
1686 Err(_) => return NetError::InvalidJson.into(),
1687 };
1688 let name = match InnerChannelName::new(&input.name) {
1689 Ok(n) => n,
1690 Err(_) => return NET_ERR_CHANNEL,
1691 };
1692 let mut cfg = InnerChannelConfig::new(ChannelId::new(name));
1693 if let Some(v) = input.visibility {
1694 let Some(vis) = parse_visibility(&v) else {
1695 return NET_ERR_CHANNEL;
1696 };
1697 cfg = cfg.with_visibility(vis);
1698 }
1699 if let Some(r) = input.reliable {
1700 cfg = cfg.with_reliable(r);
1701 }
1702 if let Some(t) = input.require_token {
1703 cfg = cfg.with_require_token(t);
1704 }
1705 if let Some(p) = input.priority {
1706 cfg = cfg.with_priority(p);
1707 }
1708 if let Some(pps) = input.max_rate_pps {
1709 cfg = cfg.with_rate_limit(pps);
1710 }
1711 if let Some(filter_json) = input.publish_caps {
1712 cfg = cfg.with_publish_caps(capability_filter_from_json(filter_json));
1713 }
1714 if let Some(filter_json) = input.subscribe_caps {
1715 cfg = cfg.with_subscribe_caps(capability_filter_from_json(filter_json));
1716 }
1717 h.channel_configs.insert(cfg);
1718 0
1719}
1720
1721#[unsafe(no_mangle)]
1722pub unsafe extern "C" fn net_mesh_subscribe_channel(
1723 handle: *mut MeshNodeHandle,
1724 publisher_node_id: u64,
1725 channel: *const c_char,
1726) -> c_int {
1727 subscribe_or_unsubscribe(handle, publisher_node_id, channel, true)
1728}
1729
1730#[unsafe(no_mangle)]
1731pub unsafe extern "C" fn net_mesh_unsubscribe_channel(
1732 handle: *mut MeshNodeHandle,
1733 publisher_node_id: u64,
1734 channel: *const c_char,
1735) -> c_int {
1736 subscribe_or_unsubscribe(handle, publisher_node_id, channel, false)
1737}
1738
1739#[unsafe(no_mangle)]
1746pub unsafe extern "C" fn net_mesh_subscribe_channel_with_token(
1747 handle: *mut MeshNodeHandle,
1748 publisher_node_id: u64,
1749 channel: *const c_char,
1750 token: *const u8,
1751 token_len: usize,
1752) -> c_int {
1753 if handle.is_null() || channel.is_null() || token.is_null() {
1754 return NetError::NullPointer.into();
1755 }
1756 let h = unsafe { &*handle };
1757 let _op = match h.guard.try_enter() {
1758 Some(op) => op,
1759 None => return NetError::ShuttingDown.into(),
1760 };
1761 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
1762 return NetError::InvalidUtf8.into();
1763 };
1764 let name = match InnerChannelName::new(&s) {
1765 Ok(n) => n,
1766 Err(_) => return NET_ERR_CHANNEL,
1767 };
1768 if token_len > isize::MAX as usize {
1770 return NetError::InvalidJson.into();
1771 }
1772 let slice = unsafe { std::slice::from_raw_parts(token, token_len) };
1773 let parsed = match PermissionToken::from_bytes(slice) {
1774 Ok(t) => t,
1775 Err(e) => return token_err_to_code(&e),
1776 };
1777 let node = h.inner.clone();
1778 match block_on(async move {
1779 node.subscribe_channel_with_token(publisher_node_id, name, parsed)
1780 .await
1781 }) {
1782 Ok(()) => 0,
1783 Err(e) => adapter_err_to_channel_code(&e),
1784 }
1785}
1786
1787fn subscribe_or_unsubscribe(
1788 handle: *mut MeshNodeHandle,
1789 publisher_node_id: u64,
1790 channel: *const c_char,
1791 subscribe: bool,
1792) -> c_int {
1793 if handle.is_null() || channel.is_null() {
1794 return NetError::NullPointer.into();
1795 }
1796 let h = unsafe { &*handle };
1797 let _op = match h.guard.try_enter() {
1798 Some(op) => op,
1799 None => return NetError::ShuttingDown.into(),
1800 };
1801 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
1802 return NetError::InvalidUtf8.into();
1803 };
1804 let name = match InnerChannelName::new(&s) {
1805 Ok(n) => n,
1806 Err(_) => return NET_ERR_CHANNEL,
1807 };
1808 let node = h.inner.clone();
1809 let outcome = if subscribe {
1810 block_on(async move { node.subscribe_channel(publisher_node_id, name).await })
1811 } else {
1812 block_on(async move { node.unsubscribe_channel(publisher_node_id, name).await })
1813 };
1814 match outcome {
1815 Ok(()) => 0,
1816 Err(e) => adapter_err_to_channel_code(&e),
1817 }
1818}
1819
1820fn adapter_err_to_channel_code(err: &AdapterError) -> c_int {
1821 if let AdapterError::Connection(msg) = err {
1822 let prefix = "membership request rejected: ";
1823 if let Some(tail) = msg.strip_prefix(prefix) {
1824 if tail.trim() == "Some(Unauthorized)" {
1825 return NET_ERR_CHANNEL_AUTH;
1826 }
1827 }
1828 }
1829 NET_ERR_CHANNEL
1830}
1831
1832#[derive(Deserialize, Default)]
1833struct PublishConfigInput {
1834 reliability: Option<String>,
1835 on_failure: Option<String>,
1836 max_inflight: Option<u32>,
1837}
1838
1839#[derive(Serialize)]
1840struct PublishReportJson {
1841 attempted: u32,
1842 delivered: u32,
1843 errors: Vec<PublishFailureJson>,
1844}
1845
1846#[derive(Serialize)]
1847struct PublishFailureJson {
1848 node_id: u64,
1849 message: String,
1850}
1851
1852fn to_publish_report_json(r: InnerPublishReport) -> PublishReportJson {
1853 PublishReportJson {
1854 attempted: r.attempted as u32,
1855 delivered: r.delivered as u32,
1856 errors: r
1857 .errors
1858 .into_iter()
1859 .map(|(id, e)| PublishFailureJson {
1860 node_id: id,
1861 message: format!("{}", e),
1862 })
1863 .collect(),
1864 }
1865}
1866
1867#[unsafe(no_mangle)]
1868pub unsafe extern "C" fn net_mesh_publish(
1869 handle: *mut MeshNodeHandle,
1870 channel: *const c_char,
1871 payload: *const u8,
1872 len: usize,
1873 config_json: *const c_char,
1874 out_json: *mut *mut c_char,
1875 out_len: *mut usize,
1876) -> c_int {
1877 if handle.is_null() || channel.is_null() || out_json.is_null() || out_len.is_null() {
1878 return NetError::NullPointer.into();
1879 }
1880 let h = unsafe { &*handle };
1881 let _op = match h.guard.try_enter() {
1882 Some(op) => op,
1883 None => return NetError::ShuttingDown.into(),
1884 };
1885 let Some(ch) = (unsafe { c_str_to_string(channel) }) else {
1886 return NetError::InvalidUtf8.into();
1887 };
1888 let name = match InnerChannelName::new(&ch) {
1889 Ok(n) => n,
1890 Err(_) => return NET_ERR_CHANNEL,
1891 };
1892 let cfg_in: PublishConfigInput = if config_json.is_null() {
1893 PublishConfigInput::default()
1894 } else {
1895 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1896 return NetError::InvalidUtf8.into();
1897 };
1898 match serde_json::from_str(&s) {
1899 Ok(v) => v,
1900 Err(_) => return NetError::InvalidJson.into(),
1901 }
1902 };
1903 let reliability = match cfg_in.reliability.as_deref() {
1904 None | Some("fire_and_forget") => Reliability::FireAndForget,
1905 Some("reliable") => Reliability::Reliable,
1906 Some(_) => return NET_ERR_CHANNEL,
1907 };
1908 let on_failure = match cfg_in.on_failure.as_deref() {
1909 None | Some("best_effort") => InnerOnFailure::BestEffort,
1910 Some("fail_fast") => InnerOnFailure::FailFast,
1911 Some("collect") => InnerOnFailure::Collect,
1912 Some(_) => return NET_ERR_CHANNEL,
1913 };
1914 let max_inflight = cfg_in.max_inflight.unwrap_or(32) as usize;
1915 let publish_cfg = InnerPublishConfig {
1916 reliability,
1917 on_failure,
1918 max_inflight,
1919 };
1920 let publisher = ChannelPublisher::new(name, publish_cfg);
1921
1922 let bytes = if len == 0 {
1924 Bytes::new()
1925 } else if payload.is_null() {
1926 return NetError::NullPointer.into();
1927 } else if len > isize::MAX as usize {
1928 return NetError::InvalidJson.into();
1930 } else {
1931 Bytes::copy_from_slice(unsafe { std::slice::from_raw_parts(payload, len) })
1932 };
1933
1934 let node = h.inner.clone();
1935 match block_on(async move { node.publish(&publisher, bytes).await }) {
1936 Ok(report) => {
1937 let js = to_publish_report_json(report);
1938 write_json_out(&js, out_json, out_len)
1939 }
1940 Err(e) => adapter_err_to_channel_code(&e),
1941 }
1942}
1943
1944pub struct IdentityHandle {
1958 keypair: ManuallyDrop<Arc<EntityKeypair>>,
1959 cache: ManuallyDrop<Arc<TokenCache>>,
1960 guard: HandleGuard,
1961}
1962
1963fn alloc_bytes(src: &[u8], out_ptr: *mut *mut u8, out_len: *mut usize) -> c_int {
1977 if out_ptr.is_null() || out_len.is_null() {
1978 return NetError::NullPointer.into();
1979 }
1980 let len = src.len();
1981 if len == 0 {
1982 unsafe {
1983 *out_ptr = std::ptr::null_mut();
1984 *out_len = 0;
1985 }
1986 return 0;
1987 }
1988 let layout = match std::alloc::Layout::array::<u8>(len) {
1997 Ok(l) => l,
1998 Err(_) => return NET_ERR_IDENTITY,
2004 };
2005 let ptr = unsafe { std::alloc::alloc(layout) };
2006 if ptr.is_null() {
2007 std::alloc::handle_alloc_error(layout);
2008 }
2009 unsafe {
2010 std::ptr::copy_nonoverlapping(src.as_ptr(), ptr, len);
2011 *out_ptr = ptr;
2012 *out_len = len;
2013 }
2014 0
2015}
2016
2017#[unsafe(no_mangle)]
2032pub unsafe extern "C" fn net_free_bytes(ptr: *mut u8, len: usize) {
2033 if ptr.is_null() || len == 0 {
2034 return;
2035 }
2036 let layout = match std::alloc::Layout::array::<u8>(len) {
2042 Ok(l) => l,
2043 Err(_) => return,
2044 };
2045 unsafe {
2046 std::alloc::dealloc(ptr, layout);
2047 }
2048}
2049
2050fn entity_id_from_bytes(bytes: *const u8, len: usize) -> Option<EntityId> {
2051 if bytes.is_null() || len != 32 {
2052 return None;
2053 }
2054 let slice = unsafe { std::slice::from_raw_parts(bytes, 32) };
2055 let mut arr = [0u8; 32];
2056 arr.copy_from_slice(slice);
2057 Some(EntityId::from_bytes(arr))
2058}
2059
2060fn parse_scope_list(raw: &str) -> Option<TokenScope> {
2061 let values: Vec<String> = serde_json::from_str(raw).ok()?;
2065 let mut acc = TokenScope::NONE;
2066 for s in &values {
2067 acc = acc.union(match s.as_str() {
2068 "publish" => TokenScope::PUBLISH,
2069 "subscribe" => TokenScope::SUBSCRIBE,
2070 "admin" => TokenScope::ADMIN,
2071 "delegate" => TokenScope::DELEGATE,
2072 _ => return None,
2073 });
2074 }
2075 Some(acc)
2076}
2077
2078fn scope_to_strings(scope: TokenScope) -> Vec<&'static str> {
2079 let mut out = Vec::new();
2080 if scope.contains(TokenScope::PUBLISH) {
2081 out.push("publish");
2082 }
2083 if scope.contains(TokenScope::SUBSCRIBE) {
2084 out.push("subscribe");
2085 }
2086 if scope.contains(TokenScope::ADMIN) {
2087 out.push("admin");
2088 }
2089 if scope.contains(TokenScope::DELEGATE) {
2090 out.push("delegate");
2091 }
2092 out
2093}
2094
2095fn channel_name_to_hash(channel: &str) -> Option<ChannelHash> {
2096 InnerChannelName::new(channel).ok().map(|n| n.hash())
2097}
2098
2099#[unsafe(no_mangle)]
2102pub unsafe extern "C" fn net_identity_generate(out_handle: *mut *mut IdentityHandle) -> c_int {
2103 if out_handle.is_null() {
2104 return NetError::NullPointer.into();
2105 }
2106 let handle = Box::new(IdentityHandle {
2107 keypair: ManuallyDrop::new(Arc::new(EntityKeypair::generate())),
2108 cache: ManuallyDrop::new(Arc::new(TokenCache::new())),
2109 guard: HandleGuard::new(),
2110 });
2111 unsafe {
2112 *out_handle = Box::into_raw(handle);
2113 }
2114 0
2115}
2116
2117#[unsafe(no_mangle)]
2121pub unsafe extern "C" fn net_identity_from_seed(
2122 seed: *const u8,
2123 seed_len: usize,
2124 out_handle: *mut *mut IdentityHandle,
2125) -> c_int {
2126 if seed.is_null() || out_handle.is_null() {
2127 return NetError::NullPointer.into();
2128 }
2129 if seed_len != 32 {
2130 return NET_ERR_IDENTITY;
2131 }
2132 let mut arr = [0u8; 32];
2133 arr.copy_from_slice(unsafe { std::slice::from_raw_parts(seed, 32) });
2134 let handle = Box::new(IdentityHandle {
2135 keypair: ManuallyDrop::new(Arc::new(EntityKeypair::from_bytes(arr))),
2136 cache: ManuallyDrop::new(Arc::new(TokenCache::new())),
2137 guard: HandleGuard::new(),
2138 });
2139 unsafe {
2140 *out_handle = Box::into_raw(handle);
2141 }
2142 0
2143}
2144
2145#[unsafe(no_mangle)]
2146pub unsafe extern "C" fn net_identity_free(handle: *mut IdentityHandle) {
2147 if handle.is_null() {
2148 return;
2149 }
2150 let h: &IdentityHandle = unsafe { &*handle };
2152 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
2153 unsafe {
2155 let mh = &mut *handle;
2156 let kp = ManuallyDrop::take(&mut mh.keypair);
2157 let cache = ManuallyDrop::take(&mut mh.cache);
2158 drop(kp);
2159 drop(cache);
2160 }
2161 } else {
2162 tracing::warn!(
2163 "net_identity_free: in-flight ops did not drain within deadline; \
2164 leaking inner to avoid use-after-free"
2165 );
2166 }
2167}
2168
2169#[unsafe(no_mangle)]
2172pub unsafe extern "C" fn net_identity_to_seed(handle: *mut IdentityHandle, out: *mut u8) -> c_int {
2173 if handle.is_null() || out.is_null() {
2174 return NetError::NullPointer.into();
2175 }
2176 let h = unsafe { &*handle };
2177 let _op = match h.guard.try_enter() {
2178 Some(op) => op,
2179 None => return NetError::ShuttingDown.into(),
2180 };
2181 let seed = h.keypair.secret_bytes();
2182 unsafe {
2183 std::ptr::copy_nonoverlapping(seed.as_ptr(), out, 32);
2184 }
2185 0
2186}
2187
2188#[unsafe(no_mangle)]
2190pub unsafe extern "C" fn net_identity_entity_id(
2191 handle: *mut IdentityHandle,
2192 out: *mut u8,
2193) -> c_int {
2194 if handle.is_null() || out.is_null() {
2195 return NetError::NullPointer.into();
2196 }
2197 let h = unsafe { &*handle };
2198 let _op = match h.guard.try_enter() {
2199 Some(op) => op,
2200 None => return NetError::ShuttingDown.into(),
2201 };
2202 let id = h.keypair.entity_id().as_bytes();
2203 unsafe {
2204 std::ptr::copy_nonoverlapping(id.as_ptr(), out, 32);
2205 }
2206 0
2207}
2208
2209#[unsafe(no_mangle)]
2210pub unsafe extern "C" fn net_identity_node_id(handle: *mut IdentityHandle) -> u64 {
2211 if handle.is_null() {
2212 return 0;
2213 }
2214 let h = unsafe { &*handle };
2215 let _op = match h.guard.try_enter() {
2217 Some(op) => op,
2218 None => return 0,
2219 };
2220 h.keypair.node_id()
2221}
2222
2223#[unsafe(no_mangle)]
2224pub unsafe extern "C" fn net_identity_origin_hash(handle: *mut IdentityHandle) -> u64 {
2225 if handle.is_null() {
2226 return 0;
2227 }
2228 let h = unsafe { &*handle };
2229 let _op = match h.guard.try_enter() {
2231 Some(op) => op,
2232 None => return 0,
2233 };
2234 h.keypair.origin_hash()
2235}
2236
2237#[unsafe(no_mangle)]
2240pub unsafe extern "C" fn net_identity_sign(
2241 handle: *mut IdentityHandle,
2242 msg: *const u8,
2243 len: usize,
2244 out_sig: *mut u8,
2245) -> c_int {
2246 if handle.is_null() || out_sig.is_null() {
2247 return NetError::NullPointer.into();
2248 }
2249 if len > 0 && msg.is_null() {
2250 return NetError::NullPointer.into();
2251 }
2252 let h = unsafe { &*handle };
2253 let _op = match h.guard.try_enter() {
2254 Some(op) => op,
2255 None => return NetError::ShuttingDown.into(),
2256 };
2257 let slice = if len == 0 {
2258 &[][..]
2259 } else if len > isize::MAX as usize {
2260 return NetError::InvalidJson.into();
2262 } else {
2263 unsafe { std::slice::from_raw_parts(msg, len) }
2264 };
2265 let sig = h.keypair.sign(slice).to_bytes();
2266 unsafe {
2267 std::ptr::copy_nonoverlapping(sig.as_ptr(), out_sig, 64);
2268 }
2269 0
2270}
2271
2272#[unsafe(no_mangle)]
2275pub unsafe extern "C" fn net_identity_issue_token(
2276 signer: *mut IdentityHandle,
2277 subject: *const u8,
2278 subject_len: usize,
2279 scope_json: *const c_char,
2280 channel: *const c_char,
2281 ttl_seconds: u32,
2282 delegation_depth: u8,
2283 out_token: *mut *mut u8,
2284 out_token_len: *mut usize,
2285) -> c_int {
2286 if signer.is_null() || out_token.is_null() || out_token_len.is_null() {
2287 return NetError::NullPointer.into();
2288 }
2289 let Some(subject_id) = entity_id_from_bytes(subject, subject_len) else {
2290 return NET_ERR_IDENTITY;
2291 };
2292 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
2293 return NetError::InvalidUtf8.into();
2294 };
2295 let Some(scope) = parse_scope_list(&scope_s) else {
2296 return NET_ERR_IDENTITY;
2297 };
2298 let Some(channel_s) = (unsafe { c_str_to_string(channel) }) else {
2299 return NetError::InvalidUtf8.into();
2300 };
2301 let Some(channel_hash) = channel_name_to_hash(&channel_s) else {
2302 return NET_ERR_IDENTITY;
2303 };
2304 let h = unsafe { &*signer };
2305 let _op = match h.guard.try_enter() {
2309 Some(op) => op,
2310 None => return NetError::ShuttingDown.into(),
2311 };
2312 let token = match PermissionToken::try_issue(
2318 &h.keypair,
2319 subject_id,
2320 scope,
2321 channel_hash,
2322 u64::from(ttl_seconds),
2323 delegation_depth,
2324 ) {
2325 Ok(t) => t,
2326 Err(e) => return token_err_to_code(&e),
2327 };
2328 alloc_bytes(&token.to_bytes(), out_token, out_token_len)
2329}
2330
2331#[unsafe(no_mangle)]
2335pub unsafe extern "C" fn net_identity_install_token(
2336 handle: *mut IdentityHandle,
2337 token: *const u8,
2338 len: usize,
2339) -> c_int {
2340 if handle.is_null() || token.is_null() {
2341 return NetError::NullPointer.into();
2342 }
2343 if len > isize::MAX as usize {
2345 return NetError::InvalidJson.into();
2346 }
2347 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2348 let parsed = match PermissionToken::from_bytes(slice) {
2349 Ok(t) => t,
2350 Err(e) => return token_err_to_code(&e),
2351 };
2352 let h = unsafe { &*handle };
2353 let _op = match h.guard.try_enter() {
2354 Some(op) => op,
2355 None => return NetError::ShuttingDown.into(),
2356 };
2357 match h.cache.insert(parsed) {
2358 Ok(()) => 0,
2359 Err(e) => token_err_to_code(&e),
2360 }
2361}
2362
2363#[unsafe(no_mangle)]
2367pub unsafe extern "C" fn net_identity_lookup_token(
2368 handle: *mut IdentityHandle,
2369 subject: *const u8,
2370 subject_len: usize,
2371 channel: *const c_char,
2372 out_token: *mut *mut u8,
2373 out_token_len: *mut usize,
2374) -> c_int {
2375 if handle.is_null() || out_token.is_null() || out_token_len.is_null() {
2376 return NetError::NullPointer.into();
2377 }
2378 let Some(subject_id) = entity_id_from_bytes(subject, subject_len) else {
2379 return NET_ERR_IDENTITY;
2380 };
2381 let Some(channel_s) = (unsafe { c_str_to_string(channel) }) else {
2382 return NetError::InvalidUtf8.into();
2383 };
2384 let Some(channel_hash) = channel_name_to_hash(&channel_s) else {
2385 return NET_ERR_IDENTITY;
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.get(&subject_id, channel_hash) {
2393 Some(token) => alloc_bytes(&token.to_bytes(), out_token, out_token_len),
2394 None => {
2395 unsafe {
2396 *out_token = std::ptr::null_mut();
2397 *out_token_len = 0;
2398 }
2399 0
2400 }
2401 }
2402}
2403
2404#[unsafe(no_mangle)]
2405pub unsafe extern "C" fn net_identity_token_cache_len(handle: *mut IdentityHandle) -> u32 {
2406 if handle.is_null() {
2407 return 0;
2408 }
2409 let h = unsafe { &*handle };
2410 let _op = match h.guard.try_enter() {
2412 Some(op) => op,
2413 None => return 0,
2414 };
2415 h.cache.len() as u32
2416}
2417
2418#[derive(Serialize)]
2423struct ParsedTokenJson {
2424 issuer_hex: String,
2425 subject_hex: String,
2426 scope: Vec<&'static str>,
2427 channel_hash: ChannelHash,
2428 not_before: u64,
2429 not_after: u64,
2430 delegation_depth: u8,
2431 nonce: u64,
2432 signature_hex: String,
2433}
2434
2435#[unsafe(no_mangle)]
2440pub unsafe extern "C" fn net_parse_token(
2441 token: *const u8,
2442 len: usize,
2443 out_json: *mut *mut c_char,
2444 out_len: *mut usize,
2445) -> c_int {
2446 if token.is_null() || out_json.is_null() || out_len.is_null() {
2447 return NetError::NullPointer.into();
2448 }
2449 if len > isize::MAX as usize {
2451 return NetError::InvalidJson.into();
2452 }
2453 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2454 let parsed = match PermissionToken::from_bytes(slice) {
2455 Ok(t) => t,
2456 Err(e) => return token_err_to_code(&e),
2457 };
2458 let out = ParsedTokenJson {
2459 issuer_hex: hex::encode(parsed.issuer.as_bytes()),
2460 subject_hex: hex::encode(parsed.subject.as_bytes()),
2461 scope: scope_to_strings(parsed.scope),
2462 channel_hash: parsed.channel_hash,
2463 not_before: parsed.not_before,
2464 not_after: parsed.not_after,
2465 delegation_depth: parsed.delegation_depth,
2466 nonce: parsed.nonce,
2467 signature_hex: hex::encode(parsed.signature),
2468 };
2469 write_json_out(&out, out_json, out_len)
2470}
2471
2472#[unsafe(no_mangle)]
2476pub unsafe extern "C" fn net_verify_token(
2477 token: *const u8,
2478 len: usize,
2479 out_ok: *mut c_int,
2480) -> c_int {
2481 if token.is_null() || out_ok.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 unsafe {
2494 *out_ok = if parsed.verify().is_ok() { 1 } else { 0 };
2495 }
2496 0
2497}
2498
2499#[unsafe(no_mangle)]
2504pub unsafe extern "C" fn net_token_is_expired(
2505 token: *const u8,
2506 len: usize,
2507 out_expired: *mut c_int,
2508) -> c_int {
2509 if token.is_null() || out_expired.is_null() {
2510 return NetError::NullPointer.into();
2511 }
2512 if len > isize::MAX as usize {
2514 return NetError::InvalidJson.into();
2515 }
2516 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2517 let parsed = match PermissionToken::from_bytes(slice) {
2518 Ok(t) => t,
2519 Err(e) => return token_err_to_code(&e),
2520 };
2521 unsafe {
2522 *out_expired = if parsed.is_expired() { 1 } else { 0 };
2523 }
2524 0
2525}
2526
2527#[unsafe(no_mangle)]
2530pub unsafe extern "C" fn net_delegate_token(
2531 signer: *mut IdentityHandle,
2532 parent: *const u8,
2533 parent_len: usize,
2534 new_subject: *const u8,
2535 new_subject_len: usize,
2536 restricted_scope_json: *const c_char,
2537 out_token: *mut *mut u8,
2538 out_token_len: *mut usize,
2539) -> c_int {
2540 if signer.is_null()
2541 || parent.is_null()
2542 || new_subject.is_null()
2543 || restricted_scope_json.is_null()
2544 || out_token.is_null()
2545 || out_token_len.is_null()
2546 {
2547 return NetError::NullPointer.into();
2548 }
2549 if parent_len > isize::MAX as usize {
2551 return NetError::InvalidJson.into();
2552 }
2553 let parent_slice = unsafe { std::slice::from_raw_parts(parent, parent_len) };
2554 let parent_tok = match PermissionToken::from_bytes(parent_slice) {
2555 Ok(t) => t,
2556 Err(e) => return token_err_to_code(&e),
2557 };
2558 let Some(subject_id) = entity_id_from_bytes(new_subject, new_subject_len) else {
2559 return NET_ERR_IDENTITY;
2560 };
2561 let Some(scope_s) = (unsafe { c_str_to_string(restricted_scope_json) }) else {
2562 return NetError::InvalidUtf8.into();
2563 };
2564 let Some(scope) = parse_scope_list(&scope_s) else {
2565 return NET_ERR_IDENTITY;
2566 };
2567 let h = unsafe { &*signer };
2568 let _op = match h.guard.try_enter() {
2572 Some(op) => op,
2573 None => return NetError::ShuttingDown.into(),
2574 };
2575 match parent_tok.delegate(&h.keypair, subject_id, scope) {
2576 Ok(child) => alloc_bytes(&child.to_bytes(), out_token, out_token_len),
2577 Err(e) => token_err_to_code(&e),
2578 }
2579}
2580
2581#[unsafe(no_mangle)]
2586pub unsafe extern "C" fn net_channel_hash(channel: *const c_char, out_hash: *mut u64) -> c_int {
2587 if channel.is_null() || out_hash.is_null() {
2588 return NetError::NullPointer.into();
2589 }
2590 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
2591 return NetError::InvalidUtf8.into();
2592 };
2593 let Some(hash) = channel_name_to_hash(&s) else {
2594 return NET_ERR_IDENTITY;
2595 };
2596 unsafe {
2597 *out_hash = hash;
2598 }
2599 0
2600}
2601
2602use crate::adapter::net::behavior::capability::{
2609 AcceleratorInfo, AcceleratorType, CapabilityFilter, CapabilitySet, GpuInfo, GpuVendor,
2610 HardwareCapabilities, Modality, ModelCapability, ResourceLimits, SoftwareCapabilities,
2611 ToolCapability, TAG_SCOPE_REGION_PREFIX, TAG_SCOPE_SUBNET_LOCAL, TAG_SCOPE_TENANT_PREFIX,
2612};
2613
2614fn parse_gpu_vendor_cap(s: &str) -> GpuVendor {
2617 match s.to_ascii_lowercase().as_str() {
2618 "nvidia" => GpuVendor::Nvidia,
2619 "amd" => GpuVendor::Amd,
2620 "intel" => GpuVendor::Intel,
2621 "apple" => GpuVendor::Apple,
2622 "qualcomm" => GpuVendor::Qualcomm,
2623 _ => GpuVendor::Unknown,
2624 }
2625}
2626
2627fn gpu_vendor_to_string_cap(v: GpuVendor) -> &'static str {
2628 match v {
2629 GpuVendor::Nvidia => "nvidia",
2630 GpuVendor::Amd => "amd",
2631 GpuVendor::Intel => "intel",
2632 GpuVendor::Apple => "apple",
2633 GpuVendor::Qualcomm => "qualcomm",
2634 GpuVendor::Unknown => "unknown",
2635 }
2636}
2637
2638fn parse_modality_cap(s: &str) -> Option<Modality> {
2639 match s.to_ascii_lowercase().as_str() {
2640 "text" => Some(Modality::Text),
2641 "image" => Some(Modality::Image),
2642 "audio" => Some(Modality::Audio),
2643 "video" => Some(Modality::Video),
2644 "code" => Some(Modality::Code),
2645 "embedding" => Some(Modality::Embedding),
2646 "tool-use" | "tool_use" | "tooluse" => Some(Modality::ToolUse),
2647 _ => None,
2656 }
2657}
2658
2659fn parse_accelerator_type_cap(s: &str) -> AcceleratorType {
2660 match s.to_ascii_lowercase().as_str() {
2661 "tpu" => AcceleratorType::Tpu,
2662 "npu" => AcceleratorType::Npu,
2663 "fpga" => AcceleratorType::Fpga,
2664 "asic" => AcceleratorType::Asic,
2665 "dsp" => AcceleratorType::Dsp,
2666 _ => AcceleratorType::Unknown,
2667 }
2668}
2669
2670#[derive(Deserialize, Default)]
2673struct CapabilitySetJson {
2674 #[serde(default)]
2675 hardware: Option<HardwareJson>,
2676 #[serde(default)]
2677 software: Option<SoftwareJson>,
2678 #[serde(default)]
2679 models: Vec<ModelJson>,
2680 #[serde(default)]
2681 tools: Vec<ToolJson>,
2682 #[serde(default)]
2683 tags: Vec<String>,
2684 #[serde(default)]
2685 limits: Option<LimitsJson>,
2686}
2687
2688#[derive(Deserialize, Default)]
2689struct HardwareJson {
2690 cpu_cores: Option<u32>,
2691 cpu_threads: Option<u32>,
2692 memory_gb: Option<u32>,
2693 gpu: Option<GpuJson>,
2694 #[serde(default)]
2695 additional_gpus: Vec<GpuJson>,
2696 storage_gb: Option<u64>,
2697 network_gbps: Option<u32>,
2698 #[serde(default)]
2699 accelerators: Vec<AcceleratorJson>,
2700}
2701
2702#[derive(Deserialize)]
2703struct GpuJson {
2704 vendor: Option<String>,
2705 #[serde(default)]
2706 model: String,
2707 #[serde(default)]
2708 vram_gb: u32,
2709 compute_units: Option<u32>,
2710 tensor_cores: Option<u32>,
2711 fp16_tflops_x10: Option<u32>,
2712}
2713
2714#[derive(Deserialize)]
2715struct AcceleratorJson {
2716 #[serde(default)]
2717 kind: String,
2718 #[serde(default)]
2719 model: String,
2720 memory_gb: Option<u32>,
2721 tops_x10: Option<u32>,
2722}
2723
2724#[derive(Deserialize, Default)]
2725struct SoftwareJson {
2726 os: Option<String>,
2727 os_version: Option<String>,
2728 #[serde(default)]
2729 runtimes: Vec<Vec<String>>,
2730 #[serde(default)]
2731 frameworks: Vec<Vec<String>>,
2732 cuda_version: Option<String>,
2733 #[serde(default)]
2734 drivers: Vec<Vec<String>>,
2735}
2736
2737#[derive(Deserialize)]
2738struct ModelJson {
2739 #[serde(default)]
2740 model_id: String,
2741 #[serde(default)]
2742 family: String,
2743 parameters_b_x10: Option<u32>,
2744 context_length: Option<u32>,
2745 quantization: Option<String>,
2746 #[serde(default)]
2747 modalities: Vec<String>,
2748 tokens_per_sec: Option<u32>,
2749 loaded: Option<bool>,
2750}
2751
2752#[derive(Deserialize)]
2753struct ToolJson {
2754 #[serde(default)]
2755 tool_id: String,
2756 #[serde(default)]
2757 name: String,
2758 version: Option<String>,
2759 input_schema: Option<String>,
2760 output_schema: Option<String>,
2761 #[serde(default)]
2762 requires: Vec<String>,
2763 estimated_time_ms: Option<u32>,
2764 stateless: Option<bool>,
2765}
2766
2767#[derive(Deserialize, Default)]
2768struct LimitsJson {
2769 max_concurrent_requests: Option<u32>,
2770 max_tokens_per_request: Option<u32>,
2771 rate_limit_rpm: Option<u32>,
2772 max_batch_size: Option<u32>,
2773 max_input_bytes: Option<u32>,
2774 max_output_bytes: Option<u32>,
2775}
2776
2777#[derive(Deserialize, Default)]
2778struct CapabilityFilterJson {
2779 #[serde(default)]
2780 require_tags: Vec<String>,
2781 #[serde(default)]
2782 require_models: Vec<String>,
2783 #[serde(default)]
2784 require_tools: Vec<String>,
2785 min_memory_gb: Option<u32>,
2786 require_gpu: Option<bool>,
2787 gpu_vendor: Option<String>,
2788 min_vram_gb: Option<u32>,
2789 min_context_length: Option<u32>,
2790 #[serde(default)]
2791 require_modalities: Vec<String>,
2792}
2793
2794fn pair_vec(xs: Vec<Vec<String>>) -> Vec<(String, String)> {
2797 xs.into_iter()
2798 .filter_map(|mut p| {
2799 if p.len() >= 2 {
2800 Some((std::mem::take(&mut p[0]), std::mem::take(&mut p[1])))
2801 } else {
2802 None
2803 }
2804 })
2805 .collect()
2806}
2807
2808#[inline]
2814fn saturating_u16_cap(v: u32) -> u16 {
2815 v.min(u16::MAX as u32) as u16
2816}
2817
2818fn gpu_info_from_json(g: GpuJson) -> GpuInfo {
2819 let vendor = g
2820 .vendor
2821 .as_deref()
2822 .map(parse_gpu_vendor_cap)
2823 .unwrap_or(GpuVendor::Unknown);
2824 let mut info = GpuInfo::new(vendor, g.model, g.vram_gb);
2825 if let Some(cu) = g.compute_units {
2826 info = info.with_compute_units(saturating_u16_cap(cu));
2827 }
2828 if let Some(tc) = g.tensor_cores {
2829 info = info.with_tensor_cores(saturating_u16_cap(tc));
2830 }
2831 if let Some(tf) = g.fp16_tflops_x10 {
2832 let tf_capped = saturating_u16_cap(tf);
2846 info = info.with_fp16_tflops(tf_capped as f32 / 10.0);
2847 }
2848 info
2849}
2850
2851fn accelerator_from_json(a: AcceleratorJson) -> AcceleratorInfo {
2852 AcceleratorInfo {
2853 accel_type: parse_accelerator_type_cap(&a.kind),
2854 model: a.model,
2855 memory_gb: a.memory_gb.unwrap_or(0),
2856 tops_x10: a.tops_x10.map(saturating_u16_cap).unwrap_or(0),
2857 }
2858}
2859
2860fn hardware_from_json(h: HardwareJson) -> HardwareCapabilities {
2861 let mut hw = HardwareCapabilities::new();
2862 match (h.cpu_cores, h.cpu_threads) {
2863 (Some(c), Some(t)) => hw = hw.with_cpu(saturating_u16_cap(c), saturating_u16_cap(t)),
2864 (Some(c), None) => {
2865 let c16 = saturating_u16_cap(c);
2866 hw = hw.with_cpu(c16, c16);
2867 }
2868 _ => {}
2869 }
2870 if let Some(mb) = h.memory_gb {
2871 hw = hw.with_memory(mb);
2872 }
2873 if let Some(g) = h.gpu {
2874 hw = hw.with_gpu(gpu_info_from_json(g));
2875 }
2876 for g in h.additional_gpus {
2877 hw = hw.add_gpu(gpu_info_from_json(g));
2878 }
2879 if let Some(mb) = h.storage_gb {
2880 hw = hw.with_storage(mb);
2881 }
2882 if let Some(gbps) = h.network_gbps {
2883 hw = hw.with_network(gbps);
2884 }
2885 for a in h.accelerators {
2886 hw = hw.add_accelerator(accelerator_from_json(a));
2887 }
2888 hw
2889}
2890
2891fn software_from_json(s: SoftwareJson) -> SoftwareCapabilities {
2892 let mut sw = SoftwareCapabilities::new()
2893 .with_os(s.os.unwrap_or_default(), s.os_version.unwrap_or_default());
2894 for (k, v) in pair_vec(s.runtimes) {
2895 sw = sw.add_runtime(k, v);
2896 }
2897 for (k, v) in pair_vec(s.frameworks) {
2898 sw = sw.add_framework(k, v);
2899 }
2900 if let Some(c) = s.cuda_version {
2901 sw = sw.with_cuda(c);
2902 }
2903 sw.drivers = pair_vec(s.drivers);
2904 sw
2905}
2906
2907fn model_from_json(m: ModelJson) -> ModelCapability {
2908 let mut mc = ModelCapability::new(m.model_id, m.family);
2909 if let Some(p) = m.parameters_b_x10 {
2910 mc.parameters_b_x10 = p;
2911 }
2912 if let Some(c) = m.context_length {
2913 mc = mc.with_context_length(c);
2914 }
2915 if let Some(q) = m.quantization {
2916 mc = mc.with_quantization(q);
2917 }
2918 for modality in m.modalities {
2919 match parse_modality_cap(&modality) {
2920 Some(parsed) => mc = mc.add_modality(parsed),
2921 None => {
2922 tracing::warn!(
2923 modality = %modality,
2924 "announce_capabilities: unknown modality string (typo?), \
2925 skipping rather than the pre-fix silent fallback to Text — \
2926 advertising a Text capability the node doesn't actually \
2927 have produced wrong scheduling decisions on the receiver",
2928 );
2929 }
2930 }
2931 }
2932 if let Some(t) = m.tokens_per_sec {
2933 mc = mc.with_tokens_per_sec(t);
2934 }
2935 if let Some(l) = m.loaded {
2936 mc = mc.with_loaded(l);
2937 }
2938 mc
2939}
2940
2941fn tool_from_json(t: ToolJson) -> ToolCapability {
2942 let mut tc = ToolCapability::new(t.tool_id, t.name);
2943 if let Some(v) = t.version {
2944 tc = tc.with_version(v);
2945 }
2946 if let Some(s) = t.input_schema {
2947 tc = tc.with_input_schema(s);
2948 }
2949 if let Some(s) = t.output_schema {
2950 tc = tc.with_output_schema(s);
2951 }
2952 for r in t.requires {
2953 tc = tc.requires(r);
2954 }
2955 if let Some(ms) = t.estimated_time_ms {
2956 tc = tc.with_estimated_time(ms);
2957 }
2958 if let Some(st) = t.stateless {
2959 tc = tc.with_stateless(st);
2960 }
2961 tc
2962}
2963
2964fn limits_from_json(l: LimitsJson) -> ResourceLimits {
2965 let mut rl = ResourceLimits::new();
2966 if let Some(n) = l.max_concurrent_requests {
2967 rl = rl.with_max_concurrent(n);
2968 }
2969 if let Some(n) = l.max_tokens_per_request {
2970 rl = rl.with_max_tokens(n);
2971 }
2972 if let Some(n) = l.rate_limit_rpm {
2973 rl = rl.with_rate_limit(n);
2974 }
2975 if let Some(n) = l.max_batch_size {
2976 rl = rl.with_max_batch(n);
2977 }
2978 if let Some(n) = l.max_input_bytes {
2979 rl.max_input_bytes = n;
2980 }
2981 if let Some(n) = l.max_output_bytes {
2982 rl.max_output_bytes = n;
2983 }
2984 rl
2985}
2986
2987fn capability_set_from_json(caps: CapabilitySetJson) -> CapabilitySet {
2988 let mut cs = CapabilitySet::new();
2989 if let Some(h) = caps.hardware {
2990 cs = cs.with_hardware(hardware_from_json(h));
2991 }
2992 if let Some(s) = caps.software {
2993 cs = cs.with_software(software_from_json(s));
2994 }
2995 for m in caps.models {
2996 cs = cs.add_model(model_from_json(m));
2997 }
2998 for t in caps.tools {
2999 cs = cs.add_tool(tool_from_json(t));
3000 }
3001 for tag in caps.tags {
3009 if tag == TAG_SCOPE_SUBNET_LOCAL {
3010 cs = cs.with_subnet_local_scope();
3011 } else if let Some(id) = tag.strip_prefix(TAG_SCOPE_TENANT_PREFIX) {
3012 cs = cs.with_tenant_scope(id);
3013 } else if let Some(name) = tag.strip_prefix(TAG_SCOPE_REGION_PREFIX) {
3014 cs = cs.with_region_scope(name);
3015 } else {
3016 cs = cs.add_tag(tag);
3017 }
3018 }
3019 if let Some(l) = caps.limits {
3020 cs = cs.with_limits(limits_from_json(l));
3021 }
3022 cs
3023}
3024
3025fn capability_filter_from_json(f: CapabilityFilterJson) -> CapabilityFilter {
3026 let mut cf = CapabilityFilter::new();
3027 for t in f.require_tags {
3028 cf = cf.require_tag(t);
3029 }
3030 for m in f.require_models {
3031 cf = cf.require_model(m);
3032 }
3033 for t in f.require_tools {
3034 cf = cf.require_tool(t);
3035 }
3036 if let Some(mb) = f.min_memory_gb {
3037 cf = cf.with_min_memory(mb);
3038 }
3039 if f.require_gpu.unwrap_or(false) {
3040 cf = cf.require_gpu();
3041 }
3042 if let Some(v) = f.gpu_vendor {
3043 cf = cf.with_gpu_vendor(parse_gpu_vendor_cap(&v));
3044 }
3045 if let Some(mb) = f.min_vram_gb {
3046 cf = cf.with_min_vram(mb);
3047 }
3048 if let Some(n) = f.min_context_length {
3049 cf = cf.with_min_context(n);
3050 }
3051 for m in f.require_modalities {
3052 match parse_modality_cap(&m) {
3053 Some(parsed) => cf = cf.require_modality(parsed),
3054 None => {
3055 tracing::warn!(
3068 modality = %m,
3069 "find_nodes: unknown modality string in require_modalities \
3070 filter (typo?), dropping the constraint; the resulting \
3071 filter is too permissive — pre-fix it was silently \
3072 re-interpreted as `require Text`, which returned the \
3073 wrong nodes",
3074 );
3075 }
3076 }
3077 }
3078 cf
3079}
3080
3081pub(crate) const NET_ERR_CAPABILITY: c_int = -128;
3084
3085#[unsafe(no_mangle)]
3092pub unsafe extern "C" fn net_mesh_announce_capabilities(
3093 handle: *mut MeshNodeHandle,
3094 caps_json: *const c_char,
3095) -> c_int {
3096 if handle.is_null() || caps_json.is_null() {
3097 return NetError::NullPointer.into();
3098 }
3099 let h = unsafe { &*handle };
3100 let _op = match h.guard.try_enter() {
3101 Some(op) => op,
3102 None => return NetError::ShuttingDown.into(),
3103 };
3104 let Some(s) = (unsafe { c_str_to_string(caps_json) }) else {
3105 return NetError::InvalidUtf8.into();
3106 };
3107 let parsed: CapabilitySetJson = match serde_json::from_str(&s) {
3108 Ok(v) => v,
3109 Err(_) => return NetError::InvalidJson.into(),
3110 };
3111 let caps = capability_set_from_json(parsed);
3112 let node = h.inner.clone();
3113 match block_on(async move { node.announce_capabilities(caps).await }) {
3114 Ok(()) => 0,
3115 Err(_) => NET_ERR_CAPABILITY,
3116 }
3117}
3118
3119#[unsafe(no_mangle)]
3122pub unsafe extern "C" fn net_mesh_find_nodes(
3123 handle: *mut MeshNodeHandle,
3124 filter_json: *const c_char,
3125 out_json: *mut *mut c_char,
3126 out_len: *mut usize,
3127) -> c_int {
3128 if handle.is_null() || filter_json.is_null() || out_json.is_null() || out_len.is_null() {
3129 return NetError::NullPointer.into();
3130 }
3131 let h = unsafe { &*handle };
3132 let _op = match h.guard.try_enter() {
3133 Some(op) => op,
3134 None => return NetError::ShuttingDown.into(),
3135 };
3136 let Some(s) = (unsafe { c_str_to_string(filter_json) }) else {
3137 return NetError::InvalidUtf8.into();
3138 };
3139 let parsed: CapabilityFilterJson = match serde_json::from_str(&s) {
3140 Ok(v) => v,
3141 Err(_) => return NetError::InvalidJson.into(),
3142 };
3143 let filter = capability_filter_from_json(parsed);
3144 let ids = h.inner.find_nodes_by_filter(&filter);
3145 write_json_out(&ids, out_json, out_len)
3146}
3147
3148#[derive(serde::Deserialize)]
3165struct ScopeFilterJson {
3166 kind: String,
3167 #[serde(default)]
3168 tenant: Option<String>,
3169 #[serde(default)]
3170 tenants: Option<Vec<String>>,
3171 #[serde(default)]
3172 region: Option<String>,
3173 #[serde(default)]
3174 regions: Option<Vec<String>>,
3175}
3176
3177enum ScopeFilterOwned {
3183 Any,
3184 GlobalOnly,
3185 SameSubnet,
3186 Tenant(String),
3187 Tenants(Vec<String>),
3188 Region(String),
3189 Regions(Vec<String>),
3190}
3191
3192fn scope_filter_from_json(f: ScopeFilterJson) -> ScopeFilterOwned {
3193 match f.kind.as_str() {
3194 "any" => ScopeFilterOwned::Any,
3195 "global_only" | "globalOnly" => ScopeFilterOwned::GlobalOnly,
3196 "same_subnet" | "sameSubnet" => ScopeFilterOwned::SameSubnet,
3197 "tenant" => match f.tenant {
3198 Some(t) if !t.is_empty() => ScopeFilterOwned::Tenant(t),
3199 _ => ScopeFilterOwned::Any,
3200 },
3201 "tenants" => match f.tenants {
3202 Some(ts) => {
3208 let cleaned: Vec<String> = ts.into_iter().filter(|t| !t.is_empty()).collect();
3209 if cleaned.is_empty() {
3210 ScopeFilterOwned::Any
3211 } else {
3212 ScopeFilterOwned::Tenants(cleaned)
3213 }
3214 }
3215 None => ScopeFilterOwned::Any,
3216 },
3217 "region" => match f.region {
3218 Some(r) if !r.is_empty() => ScopeFilterOwned::Region(r),
3219 _ => ScopeFilterOwned::Any,
3220 },
3221 "regions" => match f.regions {
3222 Some(rs) => {
3224 let cleaned: Vec<String> = rs.into_iter().filter(|r| !r.is_empty()).collect();
3225 if cleaned.is_empty() {
3226 ScopeFilterOwned::Any
3227 } else {
3228 ScopeFilterOwned::Regions(cleaned)
3229 }
3230 }
3231 None => ScopeFilterOwned::Any,
3232 },
3233 _ => ScopeFilterOwned::Any,
3234 }
3235}
3236
3237fn with_scope_filter<R>(
3242 owned: &ScopeFilterOwned,
3243 f: impl FnOnce(&crate::adapter::net::behavior::capability::ScopeFilter<'_>) -> R,
3244) -> R {
3245 use crate::adapter::net::behavior::capability::ScopeFilter as F;
3246 match owned {
3247 ScopeFilterOwned::Any => f(&F::Any),
3248 ScopeFilterOwned::GlobalOnly => f(&F::GlobalOnly),
3249 ScopeFilterOwned::SameSubnet => f(&F::SameSubnet),
3250 ScopeFilterOwned::Tenant(t) => f(&F::Tenant(t.as_str())),
3251 ScopeFilterOwned::Tenants(ts) => {
3252 let refs: Vec<&str> = ts.iter().map(|s| s.as_str()).collect();
3253 f(&F::Tenants(refs.as_slice()))
3254 }
3255 ScopeFilterOwned::Region(r) => f(&F::Region(r.as_str())),
3256 ScopeFilterOwned::Regions(rs) => {
3257 let refs: Vec<&str> = rs.iter().map(|s| s.as_str()).collect();
3258 f(&F::Regions(refs.as_slice()))
3259 }
3260 }
3261}
3262
3263#[unsafe(no_mangle)]
3286pub unsafe extern "C" fn net_mesh_find_nodes_scoped(
3287 handle: *mut MeshNodeHandle,
3288 filter_json: *const c_char,
3289 scope_json: *const c_char,
3290 out_json: *mut *mut c_char,
3291 out_len: *mut usize,
3292) -> c_int {
3293 if handle.is_null()
3294 || filter_json.is_null()
3295 || scope_json.is_null()
3296 || out_json.is_null()
3297 || out_len.is_null()
3298 {
3299 return NetError::NullPointer.into();
3300 }
3301 let h = unsafe { &*handle };
3302 let _op = match h.guard.try_enter() {
3303 Some(op) => op,
3304 None => return NetError::ShuttingDown.into(),
3305 };
3306 let Some(filter_s) = (unsafe { c_str_to_string(filter_json) }) else {
3307 return NetError::InvalidUtf8.into();
3308 };
3309 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
3310 return NetError::InvalidUtf8.into();
3311 };
3312 let parsed_filter: CapabilityFilterJson = match serde_json::from_str(&filter_s) {
3313 Ok(v) => v,
3314 Err(_) => return NetError::InvalidJson.into(),
3315 };
3316 let parsed_scope: ScopeFilterJson = match serde_json::from_str(&scope_s) {
3317 Ok(v) => v,
3318 Err(_) => return NetError::InvalidJson.into(),
3319 };
3320 let filter = capability_filter_from_json(parsed_filter);
3321 let owned = scope_filter_from_json(parsed_scope);
3322 let ids = with_scope_filter(&owned, |sf| {
3323 h.inner.find_nodes_by_filter_scoped(&filter, sf)
3324 });
3325 write_json_out(&ids, out_json, out_len)
3326}
3327
3328#[derive(serde::Deserialize)]
3342struct CapabilityRequirementJson {
3343 #[serde(default)]
3344 filter: CapabilityFilterJson,
3345 #[serde(default)]
3346 prefer_more_memory: f32,
3347 #[serde(default)]
3348 prefer_more_vram: f32,
3349 #[serde(default)]
3350 prefer_faster_inference: f32,
3351 #[serde(default)]
3352 prefer_loaded_models: f32,
3353}
3354
3355fn capability_requirement_from_json(
3356 j: CapabilityRequirementJson,
3357) -> crate::adapter::net::behavior::capability::CapabilityRequirement {
3358 crate::adapter::net::behavior::capability::CapabilityRequirement::from_filter(
3359 capability_filter_from_json(j.filter),
3360 )
3361 .prefer_memory(j.prefer_more_memory)
3362 .prefer_vram(j.prefer_more_vram)
3363 .prefer_speed(j.prefer_faster_inference)
3364 .prefer_loaded(j.prefer_loaded_models)
3365}
3366
3367#[unsafe(no_mangle)]
3377pub unsafe extern "C" fn net_mesh_find_best_node(
3378 handle: *mut MeshNodeHandle,
3379 requirement_json: *const c_char,
3380 out_node_id: *mut u64,
3381 out_has_match: *mut c_int,
3382) -> c_int {
3383 if handle.is_null()
3384 || requirement_json.is_null()
3385 || out_node_id.is_null()
3386 || out_has_match.is_null()
3387 {
3388 return NetError::NullPointer.into();
3389 }
3390 let h = unsafe { &*handle };
3391 let _op = match h.guard.try_enter() {
3392 Some(op) => op,
3393 None => return NetError::ShuttingDown.into(),
3394 };
3395 let Some(s) = (unsafe { c_str_to_string(requirement_json) }) else {
3396 return NetError::InvalidUtf8.into();
3397 };
3398 let parsed: CapabilityRequirementJson = match serde_json::from_str(&s) {
3399 Ok(v) => v,
3400 Err(_) => return NetError::InvalidJson.into(),
3401 };
3402 let req = capability_requirement_from_json(parsed);
3403 match h.inner.find_best_node(&req) {
3404 Some(node_id) => unsafe {
3405 *out_node_id = node_id;
3406 *out_has_match = 1;
3407 },
3408 None => unsafe {
3409 *out_has_match = 0;
3410 },
3411 }
3412 0
3413}
3414
3415#[unsafe(no_mangle)]
3424pub unsafe extern "C" fn net_mesh_find_best_node_scoped(
3425 handle: *mut MeshNodeHandle,
3426 requirement_json: *const c_char,
3427 scope_json: *const c_char,
3428 out_node_id: *mut u64,
3429 out_has_match: *mut c_int,
3430) -> c_int {
3431 if handle.is_null()
3432 || requirement_json.is_null()
3433 || scope_json.is_null()
3434 || out_node_id.is_null()
3435 || out_has_match.is_null()
3436 {
3437 return NetError::NullPointer.into();
3438 }
3439 let h = unsafe { &*handle };
3440 let _op = match h.guard.try_enter() {
3441 Some(op) => op,
3442 None => return NetError::ShuttingDown.into(),
3443 };
3444 let Some(req_s) = (unsafe { c_str_to_string(requirement_json) }) else {
3445 return NetError::InvalidUtf8.into();
3446 };
3447 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
3448 return NetError::InvalidUtf8.into();
3449 };
3450 let parsed_req: CapabilityRequirementJson = match serde_json::from_str(&req_s) {
3451 Ok(v) => v,
3452 Err(_) => return NetError::InvalidJson.into(),
3453 };
3454 let parsed_scope: ScopeFilterJson = match serde_json::from_str(&scope_s) {
3455 Ok(v) => v,
3456 Err(_) => return NetError::InvalidJson.into(),
3457 };
3458 let req = capability_requirement_from_json(parsed_req);
3459 let owned = scope_filter_from_json(parsed_scope);
3460 let result = with_scope_filter(&owned, |sf| h.inner.find_best_node_scoped(&req, sf));
3461 match result {
3462 Some(node_id) => unsafe {
3463 *out_node_id = node_id;
3464 *out_has_match = 1;
3465 },
3466 None => unsafe {
3467 *out_has_match = 0;
3468 },
3469 }
3470 0
3471}
3472
3473#[unsafe(no_mangle)]
3475pub unsafe extern "C" fn net_normalize_gpu_vendor(
3476 raw: *const c_char,
3477 out_json: *mut *mut c_char,
3478 out_len: *mut usize,
3479) -> c_int {
3480 if raw.is_null() || out_json.is_null() || out_len.is_null() {
3481 return NetError::NullPointer.into();
3482 }
3483 let Some(s) = (unsafe { c_str_to_string(raw) }) else {
3484 return NetError::InvalidUtf8.into();
3485 };
3486 let canonical = gpu_vendor_to_string_cap(parse_gpu_vendor_cap(&s));
3487 write_string_out(canonical.to_string(), out_json, out_len)
3488}
3489
3490#[cfg(test)]
3491mod tests {
3492 use super::*;
3493
3494 #[test]
3506 fn saturating_u16_cap_clamps_at_u16_max() {
3507 assert_eq!(saturating_u16_cap(0), 0);
3508 assert_eq!(saturating_u16_cap(42), 42);
3509 assert_eq!(saturating_u16_cap(u16::MAX as u32), u16::MAX);
3510 assert_eq!(saturating_u16_cap(u16::MAX as u32 + 1), u16::MAX);
3511 assert_eq!(saturating_u16_cap(u32::MAX), u16::MAX);
3512 }
3513
3514 #[test]
3523 fn parse_modality_cap_returns_none_on_unknown_strings() {
3524 for (s, expected) in [
3526 ("text", Modality::Text),
3527 ("Text", Modality::Text),
3528 ("TEXT", Modality::Text),
3529 ("image", Modality::Image),
3530 ("audio", Modality::Audio),
3531 ("video", Modality::Video),
3532 ("code", Modality::Code),
3533 ("embedding", Modality::Embedding),
3534 ("tool-use", Modality::ToolUse),
3535 ("tool_use", Modality::ToolUse),
3536 ("tooluse", Modality::ToolUse),
3537 ] {
3538 assert_eq!(
3539 parse_modality_cap(s),
3540 Some(expected),
3541 "known modality `{s}` must parse",
3542 );
3543 }
3544
3545 for s in ["audoi", "imageX", "vidoe", "embeding", "garbage", ""] {
3547 assert_eq!(
3548 parse_modality_cap(s),
3549 None,
3550 "unknown modality `{s}` must return None — pre-fix this \
3551 fell back to Modality::Text, advertising a capability \
3552 the node didn't actually have",
3553 );
3554 }
3555 }
3556
3557 #[test]
3567 fn gpu_info_from_json_saturates_fp16_tflops_to_u16_max() {
3568 let g = GpuJson {
3571 vendor: None,
3572 model: "test".to_string(),
3573 vram_gb: 0,
3574 compute_units: None,
3575 tensor_cores: None,
3576 fp16_tflops_x10: Some(1_000_000_000u32),
3577 };
3578 let info = gpu_info_from_json(g);
3579 assert_eq!(
3583 info.fp16_tflops_x10,
3584 u16::MAX as u32,
3585 "fp16_tflops_x10 must saturate at u16::MAX (65535) instead of \
3586 losing precision through the f32 round-trip; got {}",
3587 info.fp16_tflops_x10,
3588 );
3589
3590 let g_small = GpuJson {
3592 vendor: None,
3593 model: "test".to_string(),
3594 vram_gb: 0,
3595 compute_units: None,
3596 tensor_cores: None,
3597 fp16_tflops_x10: Some(425), };
3599 let info_small = gpu_info_from_json(g_small);
3600 assert_eq!(
3601 info_small.fp16_tflops_x10, 425,
3602 "small fp16_tflops_x10 must round-trip exactly"
3603 );
3604 }
3605
3606 #[test]
3619 fn alloc_bytes_round_trip_across_sizes() {
3620 for size in [0usize, 1, 15, 16, 17, 32, 64, 1024, 8192] {
3621 let src: Vec<u8> = (0..size).map(|i| (i as u8).wrapping_mul(37)).collect();
3622 let mut ptr: *mut u8 = std::ptr::null_mut();
3623 let mut len: usize = 0;
3624 let rc = alloc_bytes(&src, &mut ptr as *mut _, &mut len as *mut _);
3625 assert_eq!(rc, 0);
3626 assert_eq!(len, size);
3627 if size == 0 {
3628 assert!(ptr.is_null());
3629 } else {
3630 assert!(!ptr.is_null());
3631 let observed = unsafe { std::slice::from_raw_parts(ptr, len) };
3632 assert_eq!(observed, &src[..]);
3633 }
3634 unsafe { net_free_bytes(ptr, len) };
3637 }
3638 }
3639
3640 #[test]
3641 fn net_free_bytes_null_and_zero_len_are_noops() {
3642 unsafe { net_free_bytes(std::ptr::null_mut(), 0) };
3644 unsafe { net_free_bytes(std::ptr::null_mut(), 42) };
3645 let mut sentinel: u8 = 0;
3648 unsafe { net_free_bytes(&mut sentinel as *mut u8, 0) };
3649 }
3650
3651 #[test]
3663 fn net_free_bytes_does_not_panic_on_oversized_len() {
3664 let mut sentinel: u8 = 0;
3672 let ptr = &mut sentinel as *mut u8;
3673 unsafe { net_free_bytes(ptr, usize::MAX) };
3676 assert_eq!(sentinel, 0, "sentinel must not have been written through");
3679 }
3680
3681 #[test]
3690 fn net_mesh_shutdown_runs_even_with_outstanding_arc_refs() {
3691 let cfg = serde_json::json!({
3692 "bind_addr": "127.0.0.1:0",
3693 "psk_hex": "0".repeat(64),
3694 });
3695 let cfg_c = CString::new(cfg.to_string()).unwrap();
3696 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
3697 let rc = unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) };
3698 assert_eq!(rc, 0, "net_mesh_new failed: {rc}");
3699 assert!(!out.is_null());
3700
3701 let inner_clone = {
3704 let h = unsafe { &*out };
3705 Arc::clone(&h.inner)
3706 };
3707 assert!(Arc::strong_count(&inner_clone) >= 2);
3708 assert!(!inner_clone.is_shutdown());
3709
3710 let rc = unsafe { net_mesh_shutdown(out) };
3711 assert_eq!(rc, 0, "net_mesh_shutdown returned {rc}");
3712 assert!(
3713 inner_clone.is_shutdown(),
3714 "shutdown flag must be set even when extra Arc refs are outstanding"
3715 );
3716
3717 drop(inner_clone);
3718 unsafe { net_mesh_free(out) };
3722 }
3723
3724 #[test]
3736 fn handles_match_rejects_stream_node_mismatch() {
3737 fn make_node_handle() -> *mut MeshNodeHandle {
3738 let cfg = serde_json::json!({
3739 "bind_addr": "127.0.0.1:0",
3740 "psk_hex": "0".repeat(64),
3741 });
3742 let cfg_c = CString::new(cfg.to_string()).unwrap();
3743 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
3744 let rc = unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) };
3745 assert_eq!(rc, 0);
3746 assert!(!out.is_null());
3747 out
3748 }
3749
3750 let nh_a = make_node_handle();
3751 let nh_b = make_node_handle();
3752
3753 let sh_a = {
3761 let h = unsafe { &*nh_a };
3762 let node_clone: Arc<MeshNode> = Arc::clone(&h.inner);
3763 MeshStreamHandle {
3764 stream: ManuallyDrop::new(CoreStream {
3765 peer_node_id: 0xDEAD,
3766 stream_id: 1,
3767 epoch: 0,
3768 config: StreamConfig::new(),
3769 }),
3770 _node: ManuallyDrop::new(node_clone),
3771 guard: HandleGuard::new(),
3772 }
3773 };
3774
3775 assert!(
3777 handles_match(&sh_a, unsafe { &*nh_a }),
3778 "stream from node_a + node_a handle must match"
3779 );
3780 assert!(
3782 !handles_match(&sh_a, unsafe { &*nh_b }),
3783 "stream from node_a + node_b handle must be rejected (#19)"
3784 );
3785
3786 unsafe {
3795 let mut sh_a = sh_a;
3796 let _ = ManuallyDrop::take(&mut sh_a.stream);
3797 let _ = ManuallyDrop::take(&mut sh_a._node);
3798 }
3799 unsafe { net_mesh_free(nh_a) };
3800 unsafe { net_mesh_free(nh_b) };
3801 }
3802
3803 #[test]
3810 fn net_mesh_free_is_idempotent() {
3811 let cfg = serde_json::json!({
3812 "bind_addr": "127.0.0.1:0",
3813 "psk_hex": "0".repeat(64),
3814 });
3815 let cfg_c = CString::new(cfg.to_string()).unwrap();
3816 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
3817 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
3818 assert!(!nh.is_null());
3819
3820 unsafe { net_mesh_free(nh) };
3821 unsafe { net_mesh_free(nh) };
3825 }
3826
3827 #[test]
3831 fn net_identity_free_is_idempotent() {
3832 let mut h: *mut IdentityHandle = std::ptr::null_mut();
3833 assert_eq!(unsafe { net_identity_generate(&mut h) }, 0);
3834 assert!(!h.is_null());
3835
3836 unsafe { net_identity_free(h) };
3837 unsafe { net_identity_free(h) };
3839 }
3840
3841 #[test]
3853 fn net_mesh_free_waits_for_inflight_op() {
3854 use std::sync::atomic::{AtomicBool, Ordering};
3855 use std::time::{Duration, Instant};
3856
3857 let cfg = serde_json::json!({
3858 "bind_addr": "127.0.0.1:0",
3859 "psk_hex": "0".repeat(64),
3860 });
3861 let cfg_c = CString::new(cfg.to_string()).unwrap();
3862 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
3863 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
3864 assert!(!nh.is_null());
3865
3866 let nh_addr = nh as usize;
3869 let started = Arc::new(AtomicBool::new(false));
3870 let release = Arc::new(AtomicBool::new(false));
3871 let started_w = started.clone();
3872 let release_w = release.clone();
3873
3874 let worker = std::thread::spawn(move || {
3875 let h = unsafe { &*(nh_addr as *mut MeshNodeHandle) };
3876 let op = h.guard.try_enter().expect("entry must succeed pre-free");
3880 started_w.store(true, Ordering::SeqCst);
3881 while !release_w.load(Ordering::SeqCst) {
3882 std::thread::sleep(Duration::from_millis(1));
3883 }
3884 drop(op);
3885 });
3886
3887 while !started.load(Ordering::SeqCst) {
3889 std::thread::yield_now();
3890 }
3891
3892 let release_clone = release.clone();
3895 std::thread::spawn(move || {
3896 std::thread::sleep(Duration::from_millis(50));
3897 release_clone.store(true, Ordering::SeqCst);
3898 });
3899
3900 let t0 = Instant::now();
3902 unsafe { net_mesh_free(nh) };
3903 let elapsed = t0.elapsed();
3904 assert!(
3905 elapsed >= Duration::from_millis(40),
3906 "net_mesh_free returned in {:?} — pre-fix it would have proceeded \
3907 immediately and the worker's subsequent op would UAF",
3908 elapsed,
3909 );
3910 worker.join().unwrap();
3911 }
3912
3913 #[test]
3920 fn net_mesh_stream_stats_returns_shutting_down_after_free() {
3921 let cfg = serde_json::json!({
3922 "bind_addr": "127.0.0.1:0",
3923 "psk_hex": "0".repeat(64),
3924 });
3925 let cfg_c = CString::new(cfg.to_string()).unwrap();
3926 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
3927 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
3928 assert!(!nh.is_null());
3929
3930 unsafe { net_mesh_free(nh) };
3933
3934 let mut out_json: *mut c_char = std::ptr::null_mut();
3935 let mut out_len: usize = 0;
3936 let rc = unsafe { net_mesh_stream_stats(nh, 0xDEAD, 1, &mut out_json, &mut out_len) };
3937 assert_eq!(
3938 rc,
3939 NetError::ShuttingDown as c_int,
3940 "post-free stream_stats must surface ShuttingDown (got {rc})",
3941 );
3942 assert!(
3943 out_json.is_null(),
3944 "no payload may be written after the guard fires",
3945 );
3946 }
3947
3948 #[test]
3953 fn net_identity_issue_token_returns_shutting_down_after_free() {
3954 let mut signer: *mut IdentityHandle = std::ptr::null_mut();
3955 assert_eq!(unsafe { net_identity_generate(&mut signer) }, 0);
3956 assert!(!signer.is_null());
3957 unsafe { net_identity_free(signer) };
3958
3959 let subject = [0u8; 32];
3962 let scope = CString::new("[\"publish\"]").unwrap();
3963 let channel = CString::new("test-channel").unwrap();
3964 let mut out_token: *mut u8 = std::ptr::null_mut();
3965 let mut out_token_len: usize = 0;
3966 let rc = unsafe {
3967 net_identity_issue_token(
3968 signer,
3969 subject.as_ptr(),
3970 subject.len(),
3971 scope.as_ptr(),
3972 channel.as_ptr(),
3973 60,
3974 0,
3975 &mut out_token,
3976 &mut out_token_len,
3977 )
3978 };
3979 assert_eq!(
3980 rc,
3981 NetError::ShuttingDown as c_int,
3982 "post-free issue_token must surface ShuttingDown (got {rc})",
3983 );
3984 assert!(out_token.is_null(), "no token bytes may be allocated");
3985 }
3986
3987 #[test]
3993 fn net_delegate_token_returns_shutting_down_after_free() {
3994 let mut signer: *mut IdentityHandle = std::ptr::null_mut();
3995 assert_eq!(unsafe { net_identity_generate(&mut signer) }, 0);
3996 assert!(!signer.is_null());
3997
3998 let subject = [0u8; 32];
4000 let scope = CString::new("[\"publish\",\"delegate\"]").unwrap();
4001 let channel = CString::new("test-channel").unwrap();
4002 let mut parent_bytes: *mut u8 = std::ptr::null_mut();
4003 let mut parent_len: usize = 0;
4004 assert_eq!(
4005 unsafe {
4006 net_identity_issue_token(
4007 signer,
4008 subject.as_ptr(),
4009 subject.len(),
4010 scope.as_ptr(),
4011 channel.as_ptr(),
4012 60,
4013 1,
4014 &mut parent_bytes,
4015 &mut parent_len,
4016 )
4017 },
4018 0,
4019 );
4020 assert!(!parent_bytes.is_null());
4021
4022 unsafe { net_identity_free(signer) };
4024
4025 let new_subject = [1u8; 32];
4026 let restricted = CString::new("[\"publish\"]").unwrap();
4027 let mut child_bytes: *mut u8 = std::ptr::null_mut();
4028 let mut child_len: usize = 0;
4029 let rc = unsafe {
4030 net_delegate_token(
4031 signer,
4032 parent_bytes,
4033 parent_len,
4034 new_subject.as_ptr(),
4035 new_subject.len(),
4036 restricted.as_ptr(),
4037 &mut child_bytes,
4038 &mut child_len,
4039 )
4040 };
4041 assert_eq!(
4042 rc,
4043 NetError::ShuttingDown as c_int,
4044 "post-free delegate_token must surface ShuttingDown (got {rc})",
4045 );
4046 assert!(child_bytes.is_null(), "no child token may be allocated");
4047
4048 unsafe { net_free_bytes(parent_bytes, parent_len) };
4050 }
4051
4052 #[test]
4053 fn hardware_from_json_saturates_overflow_cpu_fields() {
4054 let h = HardwareJson {
4057 cpu_cores: Some(70_000),
4058 cpu_threads: Some(200_000),
4059 memory_gb: None,
4060 gpu: None,
4061 additional_gpus: Vec::new(),
4062 storage_gb: None,
4063 network_gbps: None,
4064 accelerators: Vec::new(),
4065 };
4066 let hw = hardware_from_json(h);
4067 assert_eq!(hw.cpu_cores, u16::MAX);
4068 assert_eq!(hw.cpu_threads, u16::MAX);
4069 }
4070
4071 #[test]
4078 fn token_entry_points_reject_oversize_len() {
4079 let invalid_json: c_int = NetError::InvalidJson.into();
4080 let mut sentinel: u8 = 0;
4081 let token = &mut sentinel as *mut u8 as *const u8;
4082
4083 let mut out_json: *mut c_char = std::ptr::null_mut();
4084 let mut out_len: usize = 0;
4085 assert_eq!(
4086 unsafe { net_parse_token(token, usize::MAX, &mut out_json, &mut out_len) },
4087 invalid_json,
4088 );
4089 assert!(out_json.is_null());
4090
4091 let mut out_ok: c_int = -42;
4092 assert_eq!(
4093 unsafe { net_verify_token(token, usize::MAX, &mut out_ok) },
4094 invalid_json,
4095 );
4096
4097 let mut out_expired: c_int = -42;
4098 assert_eq!(
4099 unsafe { net_token_is_expired(token, usize::MAX, &mut out_expired) },
4100 invalid_json,
4101 );
4102
4103 assert_eq!(
4104 sentinel, 0,
4105 "sentinel must not be touched: the length guard fires before any deref"
4106 );
4107 }
4108}
4109
4110#[cfg(all(test, not(feature = "nat-traversal")))]
4111mod nat_traversal_stub_tests {
4112 use super::*;
4129 use std::ptr;
4130
4131 #[test]
4132 fn nat_type_stub_returns_unsupported() {
4133 let mut out_str: *mut c_char = ptr::null_mut();
4134 let mut out_len: usize = 0;
4135 let code = net_mesh_nat_type(ptr::null_mut(), &mut out_str, &mut out_len);
4136 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4137 }
4138
4139 #[test]
4140 fn reflex_addr_stub_returns_unsupported() {
4141 let mut out_str: *mut c_char = ptr::null_mut();
4142 let mut out_len: usize = 0;
4143 let code = net_mesh_reflex_addr(ptr::null_mut(), &mut out_str, &mut out_len);
4144 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4145 }
4146
4147 #[test]
4148 fn peer_nat_type_stub_returns_unsupported() {
4149 let mut out_str: *mut c_char = ptr::null_mut();
4150 let mut out_len: usize = 0;
4151 let code = net_mesh_peer_nat_type(ptr::null_mut(), 0, &mut out_str, &mut out_len);
4152 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4153 }
4154
4155 #[test]
4156 fn probe_reflex_stub_returns_unsupported() {
4157 let mut out_str: *mut c_char = ptr::null_mut();
4158 let mut out_len: usize = 0;
4159 let code = net_mesh_probe_reflex(ptr::null_mut(), 0, &mut out_str, &mut out_len);
4160 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4161 }
4162
4163 #[test]
4164 fn reclassify_nat_stub_returns_unsupported() {
4165 let code = net_mesh_reclassify_nat(ptr::null_mut());
4166 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4167 }
4168
4169 #[test]
4170 fn traversal_stats_stub_returns_unsupported() {
4171 let mut a: u64 = 0;
4172 let mut b: u64 = 0;
4173 let mut c: u64 = 0;
4174 let code = net_mesh_traversal_stats(ptr::null_mut(), &mut a, &mut b, &mut c);
4175 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4176 }
4177
4178 #[test]
4179 fn connect_direct_stub_returns_unsupported() {
4180 let code = net_mesh_connect_direct(ptr::null_mut(), 0, ptr::null(), 0);
4181 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4182 }
4183
4184 #[test]
4185 fn set_reflex_override_stub_returns_unsupported() {
4186 let code = net_mesh_set_reflex_override(ptr::null_mut(), ptr::null());
4187 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4188 }
4189
4190 #[test]
4191 fn clear_reflex_override_stub_returns_unsupported() {
4192 let code = net_mesh_clear_reflex_override(ptr::null_mut());
4193 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4194 }
4195
4196 #[test]
4202 fn unsupported_code_is_stable() {
4203 assert_eq!(NET_ERR_TRAVERSAL_UNSUPPORTED, -137);
4204 }
4205
4206 #[test]
4210 fn capability_set_from_go_marshal_preserves_gpu_vendor() {
4211 let json = r#"{"hardware":{"cpu_cores":16,"memory_gb":64,"gpu":{"vendor":"nvidia","model":"h100","vram_gb":80}},"tags":["gpu"]}"#;
4212 let parsed: CapabilitySetJson = serde_json::from_str(json).expect("JSON should parse");
4213 let caps = capability_set_from_json(parsed);
4214 let views = caps.views();
4218 assert_eq!(
4219 views.hardware().gpu_vendor(),
4220 Some(super::GpuVendor::Nvidia),
4221 "vendor lost in conversion"
4222 );
4223 assert_eq!(views.hardware().memory_gb, 64);
4224 assert_eq!(views.hardware().total_vram_gb(), 80);
4225 assert!(caps.has_tag("gpu"));
4226 }
4227
4228 #[test]
4237 fn collect_payloads_rejects_null_entry_with_nonzero_length() {
4238 let buf_a = b"hello".as_slice();
4239 let buf_b = b"world".as_slice();
4240 let ptrs: [*const u8; 3] = [buf_a.as_ptr(), std::ptr::null(), buf_b.as_ptr()];
4241 let lens: [usize; 3] = [buf_a.len(), 4, buf_b.len()];
4242
4243 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 3) };
4244 assert!(
4245 result.is_none(),
4246 "null entry with non-zero length must reject the whole batch"
4247 );
4248 }
4249
4250 #[test]
4251 fn collect_payloads_allows_null_entry_with_zero_length() {
4252 let buf_a = b"hello".as_slice();
4253 let ptrs: [*const u8; 2] = [buf_a.as_ptr(), std::ptr::null()];
4254 let lens: [usize; 2] = [buf_a.len(), 0];
4255
4256 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 2) }
4257 .expect("zero-length null is treated as empty payload");
4258 assert_eq!(result.len(), 2);
4259 assert_eq!(&result[0][..], b"hello");
4260 assert!(result[1].is_empty());
4261 }
4262
4263 #[test]
4264 fn collect_payloads_happy_path() {
4265 let buf_a = b"abc".as_slice();
4266 let buf_b = b"defg".as_slice();
4267 let ptrs: [*const u8; 2] = [buf_a.as_ptr(), buf_b.as_ptr()];
4268 let lens: [usize; 2] = [buf_a.len(), buf_b.len()];
4269
4270 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 2) }
4271 .expect("non-null entries should succeed");
4272 assert_eq!(result.len(), 2);
4273 assert_eq!(&result[0][..], b"abc");
4274 assert_eq!(&result[1][..], b"defg");
4275 }
4276}