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